L'injection SQL qui contourne mysql la vraie chaîne d'échappement()
y a-t-il une possibilité D'injection SQL même en utilisant la fonction mysql_real_escape_string()
?
considérez cette situation type. 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 encore dangereux et possible à pirater même avec la fonction mysql_real_escape_string()
utilisée. Mais je ne vois aucun exploit possible?
injections classiques comme celle-ci:
aaa' OR 1=1 --
ne fonctionne pas.
connaissez-vous une injection possible qui pourrait passer par le code PHP ci-dessus?
4 réponses
Considérons la requête suivante:
$iId = mysql_real_escape_string("1 OR 1=1");
$sSql = "SELECT * FROM table WHERE id = $iId";
mysql_real_escape_string()
ne vous protégera pas contre cela.
le fait que vous utilisez des guillemets simples ( ' '
) autour de vos variables à l'intérieur de votre requête est ce qui vous protège contre cela. la mention suivante est aussi une option:
$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
La réponse courte est oui, oui, il y a un moyen de contourner mysql_real_escape_string()
.
pour les bordures très obscures!!!
la longue réponse n'est pas si facile. Il est basé sur une attaque démontré ici .
L'Attaque
donc, commençons par montrer l'attaque...
mysql_query('SET NAMES gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
Dans certaines circonstances, que sera de retour plus de 1 ligne. Voyons ce qui se passe ici:
-
sélection d'un jeu de caractères
mysql_query('SET NAMES gbk');
pour que cette attaque fonctionne, nous avons besoin de l'encodage que le serveur attend sur la connexion à la fois pour encoder
'
comme dans ASCII i.e.0x27
et pour avoir un caractère dont le octet final est un ASCII\
i.e.0x5c
. Comme il s'avère qu'il y a 5 codages de ce type pris en charge dans MySQL 5.6 par défaut:big5
,cp932
,gb2312
,gbk
etsjis
. Nous sélectionneronsgbk
ici.maintenant, il est très important de noter l'utilisation de
SET NAMES
ici. Ce paramètre définit le jeu de caractères sur le serveur . Si nous utilisions l'appel à la fonction API Cmysql_set_charset()
, nous serions très bien (sur les versions MySQL depuis 2006). Mais pourquoi dans une minute... -
La Charge", 1519770920"
La charge que nous allons utiliser pour cette injection commence avec la séquence d'octets
0xbf27
. Dansgbk
, c'est un caractère multibyte invalide; danslatin1
, c'est la chaîne¿'
. Notez que danslatin1
etgbk
,0x27
en elle-même est un caractère littéral'
.nous avons choisi cette charge utile parce que, si nous y appelions
addslashes()
, nous y insérerions un ASCII\
i.e.0x5c
, avant le caractère'
. Donc, nous finirions avec0xbf5c27
, qui dansgbk
est une séquence de deux caractères:0xbf5c
suivi de0x27
. Ou en d'autres termes, un valide caractère suivi d'un'
non enregistré . Mais on n'utilise pasaddslashes()
. Donc à la prochaine étape... -
mysql_real_escape_string ()
l'appel de L'API C à
mysql_real_escape_string()
diffère deaddslashes()
en ce qu'il connaît le jeu de caractères de connexion. Il peut donc effectuer l'échappement correctement pour le jeu de caractères que le serveur attend. Cependant, jusqu'à présent, le client pense que nous utilisons toujourslatin1
pour la connexion, parce que nous ne lui avons jamais dit le contraire. Nous avons dit le serveur nous utilisonsgbk
, mais le" client 1519980920 pense toujours que c'estlatin1
.C'est pourquoi l'appel à
mysql_real_escape_string()
insère le caractère" backslash", et nous avons un caractère" free hanging "1519120920 dans notre contenu "Escape"! En fait, si nous devions regarder$var
dans le jeu de caractèresgbk
, nous verrions:縗' OR 1=1 /*
, Qui est exactement ce que l'attaque nécessite.
-
La Requête
cette partie n'est qu'une formalité, mais voici la requête rendue:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
félicitations, vous venez d'attaquer avec succès un programme utilisant mysql_real_escape_string()
...
Le Mauvais
ça empire. PDO
par défaut à émuler déclarations préparées avec MySQL. Cela signifie que du côté du client, il fait essentiellement un sprintf à travers mysql_real_escape_string()
(dans la bibliothèque C), ce qui signifie que ce qui suit résultera en une injection réussie:
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
maintenant, il est intéressant de noter que vous pouvez empêcher cela en désactivant émulé déclarations préparées:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
cela donnera habituellement une véritable déclaration préparée (c'est-à-dire que les données sont envoyées sur dans un paquet distinct de la requête). Cependant, soyez conscient que PDO va silencieusement failback à l'émulation des déclarations que MySQL ne peut pas préparer nativement: ceux qu'il peut être énumérés dans le manuel, mais attention à sélectionner la version de serveur appropriée).
Le Truand
j'ai dit au tout début que nous aurions pu éviter tout cela si nous avions utilisé mysql_set_charset('gbk')
au lieu de SET NAMES gbk
. Et c'est vrai si vous utilisez une version MySQL depuis 2006.
si vous utilisez une version antérieure de MySQL, alors un bug 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 des octets uniques pour échapper à des fins même si le client avait été correctement informé du codage de connexion et donc cette attaque aurait tout de même réussi. Le bug a été corrigé dans MySQL 4.1.20 , 5.0.22 et 5.1.11 .
mais le pire est que PDO
n'a pas exposé l'API C pour mysql_set_charset()
jusqu'à 5.3.6, donc dans les versions précédentes il ne peut pas prévenir cette attaque pour toutes les commandes possibles!
Il est maintenant exposé comme un paramètre DSN .
La Grâce Salvatrice
comme nous cela dit, pour que cette attaque fonctionne, la connexion à la base de données doit être encodée en utilisant un jeu de caractères vulnérable. utf8mb4
est non vulnérables et qui, pourtant, peuvent soutenir tous "les 1519990920" caractère Unicode: alors vous pourriez choisir d'utiliser à la place-mais il a seulement été disponible depuis MySQL 5.5.3. Une alternative est utf8
, qui est également non vulnérable et peut prendre en charge l'ensemble de L'Unicode plan de base multilingue .
alternativement, vous pouvez activer le mode NO_BACKSLASH_ESCAPES
SQL, qui (entre autres choses) modifie le fonctionnement de mysql_real_escape_string()
. Avec ce mode activé, 0x27
sera remplacé par 0x2727
plutôt que 0x5c27
et ainsi le processus d'évasion ne peut pas créer des caractères valides dans l'un des vulnérables les encodages où ils n'existaient pas auparavant (par exemple 0xbf27
est toujours 0xbf27
etc.-le serveur sera toujours rejeter la chaîne comme non valide. 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 De Sécurité
les exemples suivants sont sans danger:
mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
parce que le serveur attend utf8
...
mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
parce que nous avons correctement défini le jeu de caractères pour que le client et le serveur correspondent.
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
parce que nous avons désactivé les déclarations émulées.
$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
parce que nous avons bien défini le jeu de caractères.
$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
parce que MySQLi fait de vraies déclarations préparées tout le temps.
Enveloppant
si vous:
- utiliser les Versions modernes de MySQL (fin 5.1, tous 5.5, 5.6, etc) et
mysql_set_charset()
/$mysqli->set_charset()
/ paramètre DSN de PDO (en PHP ≥ 5.3.6)
ou
- N'utilisez pas un jeu de caractères vulnérable pour l'encodage de la connexion (vous n'utilisez
utf8
/latin1
/ascii
/ etc)
Vous êtes 100% sûr.
sinon, vous êtes vulnérable même si vous utilisez mysql_real_escape_string()
...
TL; DR
mysql_real_escape_string()
sera n'offrent aucune protection que ce soit (et pourrait en outre munge vos données) si:
MySQL
NO_BACKSLASH_ESCAPES
SQL-mode est activé ( pourrait , à moins que vous explicitement sélectionnez un autre mode SQL à chaque fois que vous vous connectez ); etvotre chaîne de caractères SQL littérales sont citées en double citation
"
caractères.ceci a été déposé comme bogue n ° 72458 et a été corrigé dans MySQL v5.7.6 (Voir la section intitulée " "the Saving Grace ", ci-dessous).
ceci est un autre, (peut-être moins?) obscur CAS de BORD!!!
en hommage à @ircmaxell's excellente réponse (en fait, c'est censé être flatteur et pas plagiat!), J'adopterai son format:
L'Attaque
commence par une démonstration...
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- ');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
ceci retournera tous les enregistrements de la table test
. A dissection:
-
sélection un mode SQL
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
Comme indiqué sous Littéraux de Chaîne :
il y a plusieurs façons d'inclure des guillemets dans une chaîne de caractères:
-
"
'
"à l'intérieur d'une chaîne de cité"'
"peut être écrit"''
". -
"
"
" à l'intérieur d'un chaîne de cité ""
"peut être écrit"""
". -
précède le caractère de citation par un caractère d'évasion ("
\
"). -
"
'
"à l'intérieur d'une chaîne de cité""
" faut pas de traitement spécial et n'a pas besoin d'être doublé ou échappé. De la même manière, ""
"à l'intérieur d'une chaîne Citée avec"'
" n'a pas besoin de traitement spécial.
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. A noter que l'effet de la quatrième balle est que l'on doit nécessairement connaître le caractère qui sera utilisé pour la citation littérale, afin d'éviter munging des données. -
-
La Charge", 1519740920"
" OR 1=1 --
la charge utile initie littéralement cette injection avec le caractère
"
. Pas de codage particulier. Pas de personnages spéciaux. Pas d'octets bizarres. -
mysql_real_escape_string()
$var = mysql_real_escape_string('" OR 1=1 -- ');
heureusement,
mysql_real_escape_string()
vérifie le mode SQL et ajuste son comportement en conséquence. Voirlibmysql.c
:ulong STDCALL mysql_real_escape_string(MYSQL *mysql, char *to,const char *from, ulong length) { if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES) return escape_quotes_for_mysql(mysql->charset, to, 0, from, length); return escape_string_for_mysql(mysql->charset, to, 0, from, length); }
ainsi, une fonction sous-jacente différente,
escape_quotes_for_mysql()
, est invoquée si le modeNO_BACKSLASH_ESCAPES
SQL est utilisé. Comme mentionné ci-dessus, une telle fonction a besoin de savoir quel caractère sera utilisé pour citer le littéral afin de le répéter sans provoquer l'autre caractère de citation d'être répété littéralement.Toutefois, cette fonction arbitrairement suppose que la chaîne sera citée en utilisant le caractère
'
. Voircharset.c
:/* Escape apostrophes by doubling them up // [ deletia 839-845 ] DESCRIPTION This escapes the contents of a string by doubling up any apostrophes that it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in effect on the server. // [ deletia 852-858 ] */ size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info, char *to, size_t to_length, const char *from, size_t length) { // [ deletia 865-892 ] if (*from == '\'') { if (to + 2 > to_end) { overflow= TRUE; break; } *to++= '\''; *to++= '\''; }
ainsi, il laisse les caractères double-citation
"
Intacts (Et Double tous les caractères simple-citation'
) indépendamment du caractère réel qui est utilisé pour citer le littéral ! Dans notre cas,$var
reste exactement le même que l'argument cela a été fourni àmysql_real_escape_string()
- c'est comme si aucune fuite n'avait eu lieu du tout . -
La Requête
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
quelque chose d'une formalité, la requête rendue est:
SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
comme mon vieil ami l'a dit: Félicitations, vous venez d'attaquer avec succès un programme 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; ni ne peut mysqli::real_escape_string()
, puisque c'est juste un emballage différent autour de cette même fonction.
le problème, si ce n'est déjà évident, est que l'appel à mysql_real_escape_string()
ne peut pas savoir avec lequel le caractère littéral sera cité, comme c'est laissé au développeur de décider à un moment ultérieur. Ainsi, dans le mode NO_BACKSLASH_ESCAPES
, il y a littéralement no way que cette fonction peut échapper en toute sécurité à chaque entrée pour une utilisation avec des citations arbitraires (du moins, pas sans doublage de caractères qui ne nécessitent pas de doublage et donc munging vos données).
Le Truand
ça 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 la norme SQL (voir par exemple la section 5.3 de la spécification SQL-92 , à savoir la production grammaticale <quote symbol> ::= <quote><quote>
et l'absence de tout sens particulier donné à backslash). En outre, son utilisation a été explicitement recommandé comme une solution de rechange à la (depuis longtemps fixe) bug que le post de ircmaxell décrit. Qui sait, certains ad peuvent même le configurer pour qu'il soit activé par défaut comme moyen de décourager l'utilisation de méthodes d'échappement incorrectes comme addslashes()
.
en outre, le mode SQL d'une nouvelle connexion est défini par le serveur en fonction de sa configuration (qu'un utilisateur SUPER
peut changer à tout moment); ainsi, pour être certain du comportement du serveur, vous devez toujours spécifier explicitement votre mode souhaité après la connexion.
La Grâce Salvatrice
aussi longtemps que vous toujours explicitement définit le mode SQL pour ne pas inclure NO_BACKSLASH_ESCAPES
, ou cite des chaînes de caractères MySQL en utilisant le caractère mono-citation, ce bogue ne peut pas faire marche arrière: respectivement escape_quotes_for_mysql()
ne sera pas utilisé, ou son hypothèse au sujet des caractères guillemets qui doivent être répétés sera correcte.
pour cette raison, je recommande que n'importe qui utilisant NO_BACKSLASH_ESCAPES
permet aussi ANSI_QUOTES
mode, car il forcera l'utilisation habituelle de unique cité des littéraux de chaîne. Notez que cela n'empêche pas L'injection de SQL dans le cas où des littérales doubles sont utilisées-cela réduit simplement la probabilité que cela se produise (parce que les requêtes normales et non malveillantes échoueraient).
en AOP, à la fois sa fonction équivalente PDO::quote()
et son émulateur de déclaration préparé font appel à mysql_handle_quoter()
-ce qui fait exactement cela: il assure que les évadés literal est cité En guillemets simples, de sorte que vous pouvez être certain que PDO est toujours à l'abri de ce bug.
à partir de MySQL v5.7.6, ce bug a été corrigé. Voir change log :
fonctionnalité ajoutée ou modifiée
changement Incompatible: une nouvelle fonction de L'API C,
mysql_real_escape_string_quote()
, a été implémenté en remplacement demysql_real_escape_string()
parce que cette dernière fonction peut ne pas coder correctement les caractères lorsque le modeNO_BACKSLASH_ESCAPES
SQL mode est activé. Dans ce cas,mysql_real_escape_string()
ne peut pas échapper aux caractères de citation sauf en les doublant, et pour ce faire correctement, il doit connaître plus d'informations sur le contexte de citation que ce qui est disponible.mysql_real_escape_string_quote()
prend un argument supplémentaire pour spécifier le contexte de citation. Pour les détails d'utilisation, voir mysql_real_escape_string_quote () .Note
Les Applicationsdoivent être modifiées pour utiliser
mysql_real_escape_string_quote()
, au lieu demysql_real_escape_string()
, qui maintenant échoue et produit uneCR_INSECURE_API_ERR
erreur siNO_BACKSLASH_ESCAPES
est activé.Références: Voir Aussi Le bogue n ° 19211994.
Safe "Exemples D'15191080920"
Pris avec le bug expliqué par ircmaxell, les exemples suivants sont tout à fait sûrs (en supposant qu'on utilise MySQL après 4.1.20, 5.0.22, 5.1.11; ou qu'on n'utilise pas D'encodage de connexion GBK/Big5):
mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
...parce que nous avons explicitement sélectionné un mode SQL qui n'inclut pas NO_BACKSLASH_ESCAPES
.
mysql_set_charset($charset);
$var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
...parce qu'on cite notre chaîne de caractères avec des guillemets.
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);
...parce que les déclarations préparées par AOP sont immunisées contre cette vulnérabilité (et celle d'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 déclarations préparées ait été désactivée.)
$var = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");
...parce que la fonction quote()
de PDO échappe non seulement au littéral, mais le cite aussi (en caractères '
); notez que pour éviter le bug de ircmaxell dans ce cas, vous doit utiliser PHP≥5.3.6 et ont correctement défini le jeu de caractères dans le DSN.
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
...parce que les déclarations préparées par MySQLi sont sûres.
Enveloppant
ainsi, si vous:
- natif déclarations préparées à l'avance
ou
- utiliser MySQL v5.7.6 ou plus tard
ou
-
dans ajout à l'emploi d'une des solutions dans ircmaxell résumé, l'utilisation d'au moins l'un de:
- AOP; "1519770920 d'une" cité des littéraux de chaîne; ou
- un mode SQL explicitement défini qui ne comprend pas
NO_BACKSLASH_ESCAPES
...ensuite, vous devrait être complètement sûr (vulnérabilités en dehors de la portée de la chaîne s'échappant de côté).
Eh bien, il n'y a vraiment rien qui puisse passer par là, à part le Joker %
. Il pourrait être dangereux si vous utilisiez LIKE
déclaration comme attaquant pourrait mettre juste %
comme login si vous ne filtrez pas cela, et devrait juste bruteforce un mot de passe de l'un de vos utilisateurs.
Les gens suggèrent souvent d'utiliser des déclarations préparées pour le rendre 100% sûr, car les données ne peuvent pas interférer avec la requête elle-même de cette façon.
Mais pour des questions aussi simples il serait probablement plus efficace pour faire quelque chose comme $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);