Mysql
 sql >> Base de données >  >> RDS >> Mysql

Injection SQL qui contourne mysql_real_escape_string()

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 :

  1. 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-à-dire 0x27 et avoir un caractère dont l'octet final est un \ ASCII c'est-à-dire 0x5c . 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 et sjis . Nous allons sélectionner 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 API C mysql_set_charset() , tout irait bien (sur les versions de MySQL depuis 2006). Mais plus sur pourquoi dans une minute...

  2. La charge utile

    La charge utile que nous allons utiliser pour cette injection commence par la séquence d'octets 0xbf27 . En gbk , c'est un caractère multioctet invalide ; en latin1 , c'est la chaîne ¿' . Notez qu'en latin1 et gbk , 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-à-dire 0x5c , avant le ' personnage. Nous nous retrouverions donc avec 0xbf5c27 , qui dans gbk est une séquence de deux caractères :0xbf5c suivi de 0x27 . Ou en d'autres termes, un valide caractère suivi d'un ' sans échappement . Mais nous n'utilisons pas addslashes() . Alors passons à l'étape suivante...

  3. mysql_real_escape_string()

    L'appel de l'API C à mysql_real_escape_string() diffère de addslashes() 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 toujours latin1 pour la connexion, car on ne lui a jamais dit le contraire. Nous avons dit au serveur nous utilisons gbk , mais le client pense toujours que c'est latin1 .

    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 le gbk jeu de caractères, nous verrions :

    縗' OR 1=1 /*

    Qui est exactement quoi l'attaque l'exige.

  4. 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() ...