La réponse courte est oui, oui il y a un moyen de contourner mysql_real_escape_string()
. #Pour des CAS très OBSCURE EDGE !!!
La réponse longue n'est pas si facile. C'est basé sur une attaque démontré ici .
L'attaque
Alors, 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, cela renverra plus d'une ligne. Décortiquons 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 de la connexion pour encoder à la fois '
comme en ASCII, c'est-à-dire 0x27
et pour avoir un caractère dont le dernier octet est un ASCII \
c'est-à-dire 0x5c
. Il s'avère qu'il y a 5 encodages de ce type supportés par défaut dans MySQL 5.6 : big5
, cp932
, gb2312
, gbk
et sjis
. Nous choisirons gbk
ici.
Maintenant, il est très important de noter l'utilisation de SET NAMES
ici. Ceci définit le jeu de caractères SUR LE SERVEUR . Si nous avons utilisé l'appel à la fonction de l'API C mysql_set_charset()
nous n'avons rien à craindre (sur les versions de MySQL depuis 2006). Mais nous allons voir pourquoi dans une minute...
-
La charge utile
La charge utile que nous allons utiliser pour cette injection commence par une séquence d'octets 0xbf27
. Sur gbk
il s'agit d'un caractère multi-octet non valide ; en latin1
c'est la chaîne ¿'
. Notez que dans latin1
et gbk
, 0x27
par lui-même est un littéral '
caractère.
Nous avons choisi cette charge utile car, si nous appelions addslashes()
sur elle, nous insérons un ASCII \
c'est-à-dire 0x5c
avant que le '
caractère. Donc on se retrouvait avec 0xbf5c27
qui, en gbk
est une séquence de deux caractères : 0xbf5c
suivi par 0x27
. Ou en d'autres termes, un valide suivi d'un caractère non encodé '
. Mais nous n'utilisons pas addslashes()
. Passons donc à l'étape suivante...
-
mysql_real_escape_string()
L'appel de l'API C à mysql_real_escape_string()
diffère de addslashes()
en ce sens qu'il connaît le jeu de caractères de la connexion. Il peut donc effectuer l'échappement correctement pour le jeu de caractères attendu par le serveur. Cependant, jusqu'à ce point, le client pense que nous utilisons toujours latin1
pour la connexion, parce qu'on ne lui a jamais dit le contraire. Nous avons dit à la serveur nous utilisons gbk
mais le client pense toujours que c'est latin1
.
Par conséquent, l'appel à mysql_real_escape_string()
insère la barre oblique inversée, et nous avons une suspension libre. '
dans notre contenu "échappé" ! En fait, si l'on regarde $var
dans le gbk
le jeu de caractères, on verrait :
' OR 1=1 /\*
Ce qui est exactement ce que que l'attaque requiert.
-
La requête
Cette partie est juste une formalité, mais voici la requête rendue :
SELECT * FROM test WHERE name = '' OR 1=1 /*' LIMIT 1
Félicitations, vous venez de réussir à attaquer un programme en utilisant mysql_real_escape_string()
...
Le mauvais
C'est encore pire. PDO
La valeur par défaut est émulant les déclarations préparées avec MySQL. Cela signifie que du côté client, il s'agit essentiellement d'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 /*"));
Il convient de noter que vous pouvez éviter ce problème en désactivant les instructions préparées émulées :
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Cela permettra généralement donne lieu à 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, il faut savoir que PDO va silencieusement solution de repli pour émuler les instructions que MySQL ne peut pas préparer nativement : celles qu'il peut préparer sont les suivantes répertorié dans le manuel, mais attention à sélectionner la version appropriée du serveur).
L'Affreux
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 cela est vrai à condition que vous utilisiez une version de MySQL depuis 2006.
Si vous utilisez une version antérieure de MySQL, alors un fichier bogue sur mysql_real_escape_string()
signifie que les caractères multi-octets invalides, tels que ceux de notre charge utile, sont traités comme des octets simples à des fins d'échappement. même si le client avait été correctement informé de l'encodage de la connexion. et donc cette attaque réussirait quand même. Le bogue a été corrigé dans MySQL 4.1.20 , 5.0.22 et 5.1.11 .
Mais le pire c'est que PDO
n'a pas exposé l'API C pour mysql_set_charset()
jusqu'à la version 5.3.6, donc dans les versions antérieures il ne peut pas prévenir cette attaque pour toutes les commandes possibles ! C'est maintenant exposé comme un 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
est non vulnérable et pourtant peut soutenir chaque Caractère Unicode : vous pouvez donc choisir de l'utiliser à la place, mais il n'est disponible que depuis MySQL 5.5.3. Une alternative est utf8
qui est aussi non vulnérable et peut prendre en charge l'ensemble de l'Unicode Plan de base multilingue .
Vous pouvez également activer l'option NO_BACKSLASH_ESCAPES
le mode SQL, qui (entre autres) modifie le fonctionnement de mysql_real_escape_string()
. Avec ce mode activé, 0x27
sera remplacé par 0x2727
plutôt que 0x5c27
et donc le processus d'échappement ne peut pas créer des caractères valides dans n'importe lequel des encodages vulnérables où ils n'existaient pas auparavant (c'est-à-dire 0xbf27
est toujours 0xbf27
etc.) - donc le serveur rejettera toujours la chaîne comme étant invalide. Cependant, voir Réponse de @eggyal pour une vulnérabilité différente qui peut survenir en utilisant ce mode SQL.
Exemples sûrs
Les exemples suivants sont sûrs :
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 préparées é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 défini le jeu de caractères correctement.
$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.
Conclusion
Si vous :
- Utilisez les versions modernes de MySQL (dernières 5.1, toutes les 5.5, 5.6, etc.) ET
mysql_set_charset()
/ $mysqli->set_charset()
/ Le paramètre DSN charset de PDO (en PHP 5.3.6)
OU
- N'utilisez pas un jeu de caractères vulnérable pour le codage de la connexion (vous utilisez seulement
utf8
/ latin1
/ ascii
/ etc)
Vous êtes en sécurité à 100%.
Sinon, vous êtes vulnérable même si vous utilisez mysql_real_escape_string()
...
0 votes
En général, il est préférable d'effectuer la validation du mot de passe dans le code PHP afin de pouvoir afficher une erreur plus explicite (utilisateur invalide / mot de passe invalide).
3 votes
@ThiefMaster Je sais, ce qui précède n'est qu'un simple exemple pour faire passer mon message.
0 votes
Utilisez toujours des déclarations préparées. La sécurité, les avantages en termes de performances de la réutilisation des instructions, le codage standardisé et la maintenance de la bibliothèque l'emportent toujours (à mon avis) sur toute autre méthode alternative "raccourcie".
42 votes
@ThiefMaster - Je préfère ne pas donner des erreurs verbeuses comme utilisateur invalide / mot de passe invalide... cela indique aux marchands de force brute qu'ils ont un ID utilisateur valide, et que c'est juste le mot de passe qu'ils doivent deviner
24 votes
Mais c'est horrible du point de vue de l'ergonomie. Parfois, vous ne pouvez pas utiliser votre surnom/nom d'utilisateur/adresse électronique principal(e) et vous l'oubliez après un certain temps ou le site a supprimé votre compte pour inactivité. C'est alors extrêmement ennuyeux si vous continuez à essayer des mots de passe et peut-être même que votre IP est bloquée alors que c'est juste votre nom d'utilisateur qui est invalide.
65 votes
*S'il vous plaît, n'utilisez pas `mysql_` fonctions dans le nouveau code** . Ils ne sont plus entretenus et le processus de dépréciation a commencé à le faire. Voir le boîte rouge ? Apprendre déclarations préparées à la place, et utiliser AOP ou MySQLi - cet article vous aidera à choisir. Si vous choisissez PDO, voici un bon tutoriel .
7 votes
@tereško : Ils ne supprimeront pas la fonction mysql_* de php, du moins pas très prochainement. Peut-être en 2050. Pensez-y, s'ils la suppriment, tous les serveurs qui font des mises à jour automatiques de php auront tous les sites web non fonctionnels. C'est juste absurde.
18 votes
@machineaddict, depuis la version 5.5 (qui est sortie récemment) la
mysql_*
les fonctions produisent déjàE_DEPRECATED
avertissement. Le siteext/mysql
L'extension n'a pas été entretenue depuis plus de 10 ans. Es-tu vraiment si délirant ?1 votes
Comme la plupart des sites de production n'impriment pas les erreurs, E_DEPRECATED est inutile. Tant que "tous" les sites web n'abandonneront pas les fonctions mysql, il ne sera pas supprimé. Même là où je travaille, Je dois travailler avec l'extension mysql, car même eux ne pensent pas qu'elle sera supprimée très bientôt. Peut-être dans php 6.0. On verra bien...
3 votes
@machineaddict Il finira par être supprimé. Les serveurs ne font pas vraiment de mises à jour automatiques comme vous le prétendez. La plupart des serveurs utilisent des versions LTS de Linux et donc des versions relativement anciennes de PHP (beaucoup de serveurs utilisent encore PHP 5.1 ou 5.2). S'ils le suppriment dans la prochaine version majeure de PHP, il y aura suffisamment de temps pour arrêter d'utiliser les fonctions mysql_* (et sérieusement, personne ne les utilise depuis des années, c'est seulement dans le code hérité) car cela prendra du temps (probablement quelques années) jusqu'à ce que la nouvelle version soit intégrée dans les versions LTS.
0 votes
Il n'existe qu'un seul moyen ultime de vous protéger contre l'injection SQL. Vérifiez simplement que la variable contient ce que vous attendez. Si vous attendez un nombre entier, utilisez ctype_digit... Dans la plupart des cas, vous devriez l'entourer de "" ou de ''. et l'échapper dans des guillemets correspondant à la variable...
0 votes
Un caractère [espace] après les deux tirets ( -- ) dans le dernier peut rendre la requête valide. aaa' OR 1=1 -- [SPACE_HERE]
2 votes
@Loenix : Avec les ints, il pourrait y avoir un meilleur moyen que cela : plutôt que de vérifier, il suffit de le tourner en ce que vous attendez.
$value = (int) $value;
ou$value = intval($value);
. Il gère des choses comme les signes négatifs, quictype_digit
ne le fera pas.0 votes
@cHao Je ne suis pas d'accord avec vous. Le meilleur moyen est toujours de vérifier les valeurs car cela ne signifie pas que tous les entiers sont attendus, nous ne voulons pas insérer une valeur que l'utilisateur ne veut pas aussi. Si vous insérez des données, vous devez : vérifier le contenu que vous attendez et formater la valeur pour qu'elle soit standardisée ou exploitable. Ici, si vous vérifiez et que vous obtenez un nom d'utilisateur non bien formaté, vous pourriez renvoyer "Hey votre valeur n'est pas valide, veuillez la corriger".
9 votes
@Loenix : Si vous filtrez pour raisons commerciales (par exemple, s'assurer qu'un numéro de téléphone ressemble à un numéro de téléphone), c'est une chose. Mais filtrer pour des raisons techniques est une erreur. L'injection SQL n'est pas causée par de mauvaises données ; elle est causée par de mauvaises code . On devrait pouvoir avoir un nom de
<script>alert("'@\'½¶")
s'ils veulent vraiment le taper. Si ça casse votre application, alors votre application est déjà cassé . Au mieux, le rejet d'un tel nom pour des raisons techniques est un pansement ; au pire, c'est une fausse sécurité.1 votes
Mysqli_real_escape_string()
20 votes
@machineaddict Ils viennent de supprimer cette extension sur PHP 7.0 et nous ne sommes pas encore en 2050.