La réponse courte est oui, oui il y a un moyen de contourner mysql_real_escape_string()
.#Pour les cas de bords très obscurs !!!
La réponse longue n'est pas si facile. Il est basé sur une attaque démontrée 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écouvrons ce qui se passe ici :
-
Sélectionner 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 pour encoder
'
comme en ASCII, c'est-à-dire0x27
et avoir un caractère dont l'octet final est un\
ASCII c'est-à-dire0x5c
. Il s'avère que 5 encodages de ce type sont pris en charge par défaut dans MySQL 5.6 :big5
,cp932
,gb2312
,gbk
etsjis
. Nous allons sélectionnergbk
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 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
. Engbk
, c'est un caractère multioctet invalide ; enlatin1
, c'est la chaîne¿'
. Notez qu'enlatin1
etgbk
,0x27
seul est un'
littéral caractère.Nous avons choisi cette charge utile car, si nous appelions
addslashes()
dessus, nous insérerions un\
ASCII c'est-à-dire0x5c
, avant le'
personnage. Nous nous retrouverions donc 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'
sans échappement . Mais nous n'utilisons pasaddslashes()
. Alors passons à l'étape suivante... -
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. Ainsi, il peut 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
pour la connexion, car on ne lui a jamais dit le contraire. Nous avons dit au serveur nous utilisonsgbk
, mais le client pense toujours que c'estlatin1
.Donc l'appel à
mysql_real_escape_string()
insère la barre oblique inverse, et nous avons un'
suspendu libre personnage dans notre contenu "échappé" ! En fait, si nous devions regarder$var
dans legbk
jeu de caractères, nous verrions :縗' OR 1=1 /*
Qui est exactement quoi l'attaque l'exige.
-
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 en utilisant mysql_real_escape_string()
...
Le mauvais
Ça s'empire. PDO
par défaut, émulation déclarations préparées avec MySQL. Cela signifie que du côté client, il effectue essentiellement un sprintf via mysql_real_escape_string()
(dans la bibliothèque C), ce qui signifie que ce qui suit entraînera 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 convient de noter que vous pouvez empêcher cela en désactivant les instructions préparées émulées :
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Cela va habituellement aboutir à une véritable instruction préparée (c'est-à-dire que les données sont envoyées dans un paquet séparé de la requête). Cependant, sachez que PDO va silencieusement de secours à l'émulation d'instructions que MySQL ne peut pas préparer nativement :celles qu'il peut préparer sont répertorié dans le manuel, mais attention à sélectionner la version de serveur appropriée).
Le laid
J'ai dit au tout début que nous aurions pu empêcher tout cela si nous avions utilisé mysql_set_charset('gbk')
au lieu de SET NAMES gbk
. Et c'est vrai à condition que vous utilisiez une version de MySQL depuis 2006.
Si vous utilisez une version antérieure de MySQL, alors un bogue
dans mysql_real_escape_string()
signifiait que les caractères multi-octets non valides tels que ceux de notre charge utile étaient traités comme des octets uniques à 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 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
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 empêchez cette attaque pour chaque commande possible ! Elle est maintenant exposée 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 encodée à l'aide d'un jeu de caractères vulnérable. utf8mb4
n'est pas vulnérable et pourtant peut prendre en charge 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 n'est également pas 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 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 l'un des encodages vulnérables où ils n'existaient pas auparavant (c'est-à-dire 0xbf27
est toujours 0xbf27
etc.) - ainsi le serveur rejettera toujours la chaîne comme invalide. Cependant, voir la réponse de @eggyal
pour une vulnérabilité différente pouvant résulter de l'utilisation de 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 afin 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 instructions 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 correctement 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 tout le temps de vraies instructions préparées.
Conclusion
Si vous :
- Utiliser les versions modernes de MySQL (fin 5.1, toutes les 5.5, 5.6, etc.) ET
mysql_set_charset()
/$mysqli->set_charset()
/ Paramètre DSN charset du PDO (en PHP ≥ 5.3.6)
OU
- N'utilisez pas un jeu de caractères vulnérable pour l'encodage de connexion (vous n'utilisez que
utf8
/latin1
/ascii
/etc)
Vous êtes 100 % en sécurité.
Sinon, vous êtes vulnérable même si vous utilisez mysql_real_escape_string()
...