Existe-t-il une possibilité d'injection SQL même lors de l'utilisation de la mysql_real_escape_string()
fonction?
Considérez cet exemple de situation. SQL est construit en PHP comme ceci:
$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));
$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";
J'ai entendu de nombreuses personnes me dire qu'un code comme celui-ci est toujours dangereux et qu'il est possible de pirater même avec la mysql_real_escape_string()
fonction utilisée. Mais je ne peux penser à aucun exploit possible?
Injections classiques comme celle-ci:
aaa' OR 1=1 --
ne fonctionnent pas.
Connaissez-vous une injection possible qui passerait par le code PHP ci-dessus?
mysql_*
fonctions dans le nouveau code . Ils ne sont plus maintenus et le processus de dépréciation a commencé. Vous voyez la boîte rouge ? Découvrezplutôt les instructions préparées et utilisez PDO ou MySQLi - cet article vous aidera à décider lesquelles. Si vous choisissez PDO, voici un bon tutoriel .mysql_*
fonctions produisent déjà unE_DEPRECATED
avertissement. L'ext/mysql
extension n'a pas été maintenue depuis plus de 10 ans. Êtes-vous vraiment si délirant?Réponses:
Considérez la requête suivante:
mysql_real_escape_string()
ne vous protégera pas contre cela. Le fait que vous utilisez des guillemets simples (' '
) autour de vos variables dans votre requête est ce qui vous protège contre cela. Ce qui suit est également une option:la source
mysql_query()
n'exécute pas plusieurs instructions, non?DROP TABLE
, dans la pratique, l'attaquant est plus susceptible de le faireSELECT passwd FROM users
. Dans ce dernier cas, la deuxième requête est généralement exécutée à l'aide d'uneUNION
clause.(int)mysql_real_escape_string
- Cela n'a aucun sens. Cela ne diffère pas du(int)
tout. Et ils produiront le même résultat pour chaque entréemysql_real_escape_string
, nonmysql_real_escape_integer
. Ce n'est pas destiné à être utilisé avec des champs entiers.La réponse courte est oui, oui, il existe un moyen de se déplacer
mysql_real_escape_string()
.Pour les CAS TRÈS OBSCURES !!!
La réponse longue n'est pas si simple. C'est basé sur une attaque démontrée ici .
L'attaque
Commençons donc par montrer l'attaque ...
Dans certaines circonstances, cela renverra plus d'une ligne. Décortiquons ce qui se passe ici:
Sélection d'un jeu de caractères
Pour que cette attaque fonctionne, nous avons besoin du codage que le serveur attend sur la connexion à la fois pour coder
'
comme en ASCII ie0x27
et pour avoir un caractère dont l'octet final est un ASCII\
ie0x5c
. Comme il se trouve, il y a 5 ces codages pris en charge par MySQL 5.6 par défaut:big5
,cp932
,gb2312
,gbk
etsjis
. Nous allons sélectionnergbk
ici.Maintenant, il est très important de noter l'utilisation d'
SET NAMES
ici. Cela définit le jeu de caractères SUR LE SERVEUR . Si nous utilisions l'appel à la fonction API Cmysql_set_charset()
, tout irait bien (sur les versions de MySQL depuis 2006). Mais plus sur pourquoi dans une minute ...La charge utile
La charge utile que nous allons utiliser pour cette injection commence par la séquence d'octets
0xbf27
. Dansgbk
, c'est un caractère multi-octets invalide; danslatin1
, c'est la chaîne¿'
. Notez que danslatin1
etgbk
,0x27
en soi, est un'
caractère littéral .Nous avons choisi cette charge utile car, si nous l'appelions
addslashes()
, nous insérions un ASCII,\
c'est0x5c
-à- dire avant le'
caractère. Nous finirions donc avec0xbf5c27
, quigbk
est une séquence de deux caractères:0xbf5c
suivi de0x27
. Ou en d'autres termes, un caractère valide suivi d'un caractère non échappé'
. Mais nous n'utilisons pasaddslashes()
. Passons à l'étape suivante ...mysql_real_escape_string ()
L'appel de l'API C
mysql_real_escape_string()
diffère deaddslashes()
par le fait qu'il connaît le jeu de caractères de connexion. Il peut donc effectuer correctement l'échappement pour le jeu de caractères attendu par le serveur. Cependant, jusqu'à présent, le client pense que nous utilisons toujourslatin1
la connexion, car nous ne l'avons jamais dit autrement. Nous avons dit au serveur que nous utilisonsgbk
, mais le client pense toujours que c'est le caslatin1
.Par conséquent, l'appel à
mysql_real_escape_string()
insère la barre oblique inverse, et nous avons un'
caractère suspendu gratuit dans notre contenu "échappé"! En fait, si nous regardions$var
dans legbk
jeu de caractères, nous verrions:C'est exactement ce dont l'attaque a besoin.
La requête
Cette partie n'est qu'une formalité, mais voici la requête rendue:
Félicitations, vous venez d'attaquer avec succès un programme en utilisant
mysql_real_escape_string()
...Le mauvais
Ça s'empire.
PDO
par défaut pour émuler des instructions préparées avec MySQL. Cela signifie que du côté client, il effectue essentiellement un sprintf à traversmysql_real_escape_string()
(dans la bibliothèque C), ce qui signifie que ce qui suit se traduira par une injection réussie:Maintenant, il convient de noter que vous pouvez empêcher cela en désactivant les instructions préparées émulées:
Cela se traduira généralement par une véritable instruction préparée (c'est-à-dire que les données sont envoyées dans un paquet distinct de la requête). Cependant, sachez que PDO se repliera silencieusement sur des instructions d'émulation que MySQL ne peut pas préparer nativement: celles qu'il peut sont listées dans le manuel, mais attention à sélectionner la version de serveur appropriée).
Le moche
J'ai dit au tout début que nous aurions pu empêcher tout cela si nous avions utilisé à la
mysql_set_charset('gbk')
place deSET NAMES gbk
. Et c'est vrai à condition d'utiliser une version MySQL depuis 2006.Si vous utilisez une version de MySQL plus tôt, alors un bogue dans
mysql_real_escape_string()
signifie que les caractères multi - octets invalides tels que ceux de notre charge utile ont été traités comme les octets pour échapper à des fins même si le client avait été correctement informé de l'encodage de connexion et donc cette attaque serait réussir encore. Le bogue a été corrigé dans MySQL 4.1.20 , 5.0.22 et 5.1.11 .Mais le pire, c'est que
PDO
l'API C n'a pas étémysql_set_charset()
exposée avant la 5.3.6, donc dans les versions précédentes, elle ne peut pas empêcher cette attaque pour chaque commande possible! Il est maintenant exposé en tant que paramètre DSN .La grâce salvatrice
Comme nous l'avons dit au début, pour que cette attaque fonctionne, la connexion à la base de données doit être codée à l'aide d'un jeu de caractères vulnérable.
utf8mb4
n'est pas vulnérable et peut néanmoins prendre en charge tous les caractères Unicode: vous pouvez donc choisir de l'utiliser à la place, mais il n'est disponible que depuis MySQL 5.5.3. Une alternative estutf8
, qui n'est pas non plus vulnérable et peut prendre en charge l'ensemble du plan multilingue de base Unicode .Alternativement, vous pouvez activer le
NO_BACKSLASH_ESCAPES
mode SQL, qui (entre autres) modifie le fonctionnement demysql_real_escape_string()
. Avec ce mode activé,0x27
sera remplacé par0x2727
plutôt que0x5c27
et donc le processus d'échappement ne peut pas créer de caractères valides dans aucun des encodages vulnérables où ils n'existaient pas auparavant (c'est0xbf27
-à- dire est toujours0xbf27
etc.) - donc le serveur rejettera toujours la chaîne comme invalide . Cependant, voir la réponse de @ eggyal pour une vulnérabilité différente qui peut résulter de l'utilisation de ce mode SQL.Exemples sûrs
Les exemples suivants sont sûrs:
Parce que le serveur attend
utf8
...Parce que nous avons correctement défini le jeu de caractères afin que le client et le serveur correspondent.
Parce que nous avons désactivé les instructions préparées émulées.
Parce que nous avons correctement défini le jeu de caractères.
Parce que MySQLi fait tout le temps de véritables instructions préparées.
Emballer
Si vous:
mysql_set_charset()
/$mysqli->set_charset()
/ PDO paramètre de jeu de caractères DSN (en PHP ≥ 5.3.6)OU
utf8
/latin1
/ascii
/ etc)Vous êtes sûr à 100%.
Sinon, vous êtes vulnérable même si vous utilisez
mysql_real_escape_string()
...la source
addslashes
. J'ai basé cette vulnérabilité sur celle-là. Essayez-le vous-même. Allez chercher MySQL 5.0, exécutez cet exploit et voyez par vous-même. En ce qui concerne la façon de mettre cela dans PUT / GET / POST, c'est TRIVIAL. Les données d'entrée ne sont que des flux d'octets.char(0xBF)
est juste un moyen lisible de générer un octet. J'ai démontré cette vulnérabilité en direct devant plusieurs conférences. Faites-moi confiance à ce sujet ... Mais si vous ne le faites pas, essayez-le vous-même. Ça marche ...?var=%BF%27+OR+1=1+%2F%2A
dans l'URL,$var = $_GET['var'];
dans le code, et Bob est ton oncle.Ceci est un autre, (peut-être moins?) CAS DE BORD obscur !!!
En hommage à l'excellente réponse @ ircmaxell (vraiment, c'est censé être de la flatterie et non du plagiat!), J'adopterai son format:
L'attaque
Commençant par une démonstration ...
Cela renverra tous les enregistrements de la
test
table. Une dissection:Sélection d'un mode SQL
Comme documenté sous String Literals :
Si le mode SQL du serveur inclut
NO_BACKSLASH_ESCAPES
, alors la troisième de ces options - qui est l'approche habituelle adoptée parmysql_real_escape_string()
- n'est pas disponible: l'une des deux premières options doit être utilisée à la place. Notez que l'effet de la quatrième puce est que l'on doit nécessairement connaître le caractère qui sera utilisé pour citer le littéral afin d'éviter de fusionner ses données.La charge utile
La charge utile lance cette injection littéralement avec le
"
personnage. Pas d'encodage particulier. Pas de caractères spéciaux. Pas d'octets bizarres.mysql_real_escape_string ()
Heureusement,
mysql_real_escape_string()
vérifie le mode SQL et ajuste son comportement en conséquence. Voirlibmysql.c
:Ainsi, une fonction sous-jacente différente,,
escape_quotes_for_mysql()
est invoquée si leNO_BACKSLASH_ESCAPES
mode SQL est utilisé. Comme mentionné ci-dessus, une telle fonction doit savoir quel caractère sera utilisé pour citer le littéral afin de le répéter sans provoquer la répétition littérale de l'autre caractère de citation.Cependant, cette fonction suppose arbitrairement que la chaîne sera citée en utilisant le
'
caractère guillemet simple . Voircharset.c
:Ainsi, il laisse les
"
caractères entre guillemets intacts (et double tous les'
caractères entre guillemets ) quel que soit le caractère réel utilisé pour citer le littéral ! Dans notre cas, il$var
reste exactement le même que l'argument qui a été fourni àmysql_real_escape_string()
- c'est comme si aucune fuite n'avait eu lieu du tout .La requête
Quelque chose d'une formalité, la requête rendue est:
Comme l'a dit mon confrère: félicitations, vous venez d'attaquer avec succès un programme en utilisant
mysql_real_escape_string()
...Le mauvais
mysql_set_charset()
ne peut pas aider, car cela n'a rien à voir avec les jeux de caractères; pas plusmysqli::real_escape_string()
, car c'est juste un wrapper différent autour de cette même fonction.Le problème, s'il n'est pas déjà évident, est que l'appel à
mysql_real_escape_string()
ne peut pas savoir avec quel caractère le littéral sera cité, car il appartient au développeur de décider plus tard. Donc, enNO_BACKSLASH_ESCAPES
mode, il n'y a littéralement aucun moyen que cette fonction puisse échapper en toute sécurité à chaque entrée pour une utilisation avec des guillemets arbitraires (au moins, non sans doubler les caractères qui ne nécessitent pas de doubler et donc de fusionner vos données).Le moche
Ça s'empire.
NO_BACKSLASH_ESCAPES
peut ne pas être si rare dans la nature en raison de la nécessité de son utilisation pour la compatibilité avec le SQL standard (par exemple, voir la section 5.3 de la spécification SQL-92 , à savoir la<quote symbol> ::= <quote><quote>
production de grammaire et le manque de signification particulière donnée à la barre oblique inverse). De plus, son utilisation a été explicitement recommandée comme solution de contournement au bogue (corrigé depuis longtemps) décrit par la publication d'ircmaxell. Qui sait, certains administrateurs de base de données peuvent même le configurer pour qu'il soit activé par défaut afin de décourager l'utilisation de méthodes d'échappement incorrectes commeaddslashes()
.De plus, le mode SQL d'une nouvelle connexion est défini par le serveur en fonction de sa configuration (qu'un
SUPER
utilisateur peut modifier à tout moment); ainsi, pour être certain du comportement du serveur, vous devez toujours spécifier explicitement le mode souhaité après la connexion.La grâce salvatrice
Tant que vous définissez toujours explicitement le mode SQL pour qu'il n'inclue pas
NO_BACKSLASH_ESCAPES
ou ne cite pas les littéraux de chaîne MySQL en utilisant le caractère guillemet simple, ce bogue ne peut pasescape_quotes_for_mysql()
afficher sa tête laide: respectivement , ne sera pas utilisé, ou son hypothèse sur les caractères guillemet nécessitant une répétition sera être correct.Pour cette raison, je recommande que toute personne utilisant
NO_BACKSLASH_ESCAPES
également active leANSI_QUOTES
mode, car cela forcera l'utilisation habituelle des littéraux de chaîne entre guillemets simples. Notez que cela n'empêche pas l'injection SQL dans le cas où des littéraux entre guillemets sont utilisés - cela réduit simplement la probabilité que cela se produise (car les requêtes normales et non malveillantes échoueraient).Dans PDO, sa fonction équivalente
PDO::quote()
et son émulateur d'instructions préparé font appel àmysql_handle_quoter()
- ce qui fait exactement cela: il garantit que le littéral échappé est cité entre guillemets simples, de sorte que vous pouvez être certain que PDO est toujours à l'abri de ce bogue.Depuis MySQL v5.7.6, ce bug a été corrigé. Voir le journal des modifications :
Exemples sûrs
Pris ensemble avec le bogue expliqué par ircmaxell, les exemples suivants sont entièrement sûrs (en supposant que l'on utilise MySQL plus tard que 4.1.20, 5.0.22, 5.1.11; ou que l'on n'utilise pas un codage de connexion GBK / Big5) :
... car nous avons explicitement sélectionné un mode SQL qui ne comprend pas
NO_BACKSLASH_ESCAPES
.... parce que nous citons notre chaîne littérale avec des guillemets simples.
... parce que les instructions préparées par PDO sont immunisées contre cette vulnérabilité (et ircmaxell aussi, à condition que vous utilisiez PHP ≥5.3.6 et que le jeu de caractères ait été correctement défini dans le DSN; ou que l'émulation des instructions préparées ait été désactivée) .
... parce que la
quote()
fonction de PDO échappe non seulement au littéral, mais le cite également (en guillemets simples'
); notez que pour éviter le bogue d'ircmaxell dans ce cas, vous devez utiliser PHP≥5.3.6 et avoir correctement défini le jeu de caractères dans le DSN.... car les instructions préparées par MySQLi sont sûres.
Emballer
Ainsi, si vous:
OU
OU
en plus d'utiliser l'une des solutions du résumé d'ircmaxell, utilisez au moins l'une des solutions suivantes:
NO_BACKSLASH_ESCAPES
... alors vous devriez être complètement en sécurité (vulnérabilités en dehors de la portée de la chaîne s'échappant).
la source
"
de chaînes en premier lieu. SQL dit que c'est pour les identifiants. Mais eh ... juste un autre exemple de MySQL disant "vis standards, je ferai ce que je veux". (Heureusement, vous pouvez inclureANSI_QUOTES
dans le mode de correction des bris de citation. Le mépris ouvert des normes, cependant, est un problème plus important qui pourrait nécessiter des mesures plus sévères.)quote()
fonction de PDO - mais les instructions préparées sont un moyen beaucoup plus sûr et plus approprié d'éviter l'injection en général. Bien sûr, si vous avez directement concaténé des variables non échappées dans votre SQL, vous êtes très certainement vulnérable à l'injection, quelles que soient les méthodes que vous utilisez par la suite.Eh bien, il n'y a vraiment rien qui puisse passer par là, à part le
%
caractère générique. Il pourrait être dangereux si vous utilisiez uneLIKE
instruction car l'attaquant pourrait utiliser la même%
connexion si vous ne filtrez pas cela et devrait simplement forcer le mot de passe de l'un de vos utilisateurs. Les gens suggèrent souvent d'utiliser des instructions préparées pour le rendre sûr à 100%, car les données ne peuvent pas interférer avec la requête elle-même de cette façon. Mais pour des requêtes aussi simples, il serait probablement plus efficace de faire quelque chose comme$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);
la source
more efficient
plutôt que d'utiliser des déclarations préparées? (Les instructions préparées fonctionnent toujours, la bibliothèque peut être rapidement corrigée en cas d'attaques, n'expose pas les erreurs humaines [telles que la mauvaise saisie de la chaîne de remplacement complète] et présente des avantages importants en termes de performances si l'instruction est réutilisée.)