Existe-t-il une possibilité d’injection SQL même lors de l’utilisation de la fonction mysql_real_escape_string()
?
Considérez cet exemple de situation. SQL est construit dans 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 tel code est toujours dangereux et qu'il est possible de pirater même avec la fonction mysql_real_escape_string()
utilisée. Mais je ne peux pas penser à un exploit possible?
Les injections classiques comme ceci:
aaa' OR 1=1 --
ne fonctionnent pas.
Connaissez-vous une éventuelle injection qui passerait par le code PHP ci-dessus?
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 utilisiez des guillemets simples (' '
) autour de vos variables dans votre requête vous protège contre cela. Ce qui suit 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 existe un moyen de contourner mysql_real_escape_string()
.
La réponse longue n'est pas si facile. C'est basé sur une attaque démontré ici .
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. Disséquons ce qui se passe ici:
Sélection d'un jeu de caractères
mysql_query('SET NAMES gbk');
Pour que cette attaque fonctionne, le codage attendu par le serveur sur la connexion doit coder '
comme dans ASCII c'est-à-dire 0x27
et afin que le caractère l'octet final est un ASCII \
c'est-à-dire 0x5c
. Il s’avère que 5 codages de ce type sont pris en charge dans MySQL 5.6 par défaut: 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 utilisions l'appel de la fonction mysql_set_charset()
de l'API C, tout irait bien (sur les versions de MySQL depuis 2006). Mais plus sur pourquoi dans une minute ...
La charge utile
La charge que nous allons utiliser pour cette injection commence par la séquence d'octets 0xbf27
. Dans gbk
, il s'agit d'un caractère multi-octets non valide; Dans latin1
, c'est la chaîne ¿'
. Notez que, dans latin1
et gbk
, 0x27
est un caractère littéral '
.
Nous avons choisi cette charge parce que, si nous appelions addslashes()
, nous insérerions un ASCII \
, c'est-à-dire 0x5c
, avant le caractère '
. Nous finirions donc avec 0xbf5c27
, qui dans gbk
est une séquence de deux caractères: 0xbf5c
suivi de 0x27
. Ou, en d'autres termes, un caractère valide suivi d'un '
non échappé. Mais nous n'utilisons pas addslashes()
. Passons à l'étape suivante ...
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 la connexion. Ainsi, il peut exécuter 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 nous ne lui avons jamais dit le contraire. Nous avons dit au serveur que 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 un caractère '
libre dans notre contenu "échappé"! En fait, si nous examinions $var
dans le jeu de caractères gbk
, nous verrions:
縗 'OR 1 = 1/*
Ce qui est exactement ce que l'attaque nécessite.
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 d’attaquer avec succès un programme utilisant mysql_real_escape_string()
...
Ça a empiré. PDO
par défaut émule les instructions préparées avec MySQL. Cela signifie que côté client, il exécute généralement un sprintf via mysql_real_escape_string()
(dans la bibliothèque C), ce qui signifie que les opérations suivantes aboutiront à 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 maintenant 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 aboutira généralement à une instruction réellement 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 utilisera silencieusement repli pour émuler des instructions que MySQL ne peut pas préparer en mode natif: celles qu’il peut utiliser sont répertoriées dans le manuel, mais faites bien attention à la sélection appropriée. version du serveur).
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, à condition que vous utilisiez une version de MySQL depuis 2006.
Si vous utilisez une version antérieure de MySQL, un bug dans mysql_real_escape_string()
signifiait que des caractères multi-octets non valides, tels que ceux de notre charge utile, étaient traités comme des octets simples à des fins d'échappement , même si le client avait été correctement informé du codage de la connexion et donc cette attaque réussirait toujours. Le bogue a été corrigé dans MySQL 4.1.2 , 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. Ainsi, dans les versions précédentes, il ne pouvait pas empêcher cette attaque pour chaque commande possible! Il est maintenant exposé en tant que paramètre DSN .
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 peut cependant supporter 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 est utf8
, qui est également non vulnérable et peut prendre en charge la totalité du Unicode plan multilingue de base .
Vous pouvez également activer le mode SQL NO_BACKSLASH_ESCAPES
, qui modifie (entre autres choses) le fonctionnement de mysql_real_escape_string()
. Lorsque ce mode est activé, 0x27
sera remplacé par 0x2727
plutôt que 0x5c27
et ainsi le processus d'échappement ne pourra pas créer des caractères valides dans aucun des codages vulnérables où ils n'existaient pas auparavant (à savoir 0xbf27
est toujours 0xbf27
etc.) - ainsi le serveur rejettera toujours la chaîne comme invalide. Cependant, voir réponse de @ eggyal pour obtenir une autre vulnérabilité pouvant découler de l’utilisation de ce mode SQL.
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 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 instructions préparées tout le temps.
Si vous:
mysql_set_charset()
/$mysqli->set_charset()
/paramètre de jeu de caractères DSN de PDO (dans PHP ≥ 5.3.6)OU
utf8
/latin1
/ascii
/etc)Vous êtes 100% en sécurité.
Sinon, vous êtes vulnérable même si vous utilisez mysql_real_escape_string()
...
Eh bien, il n'y a rien qui puisse vraiment passer à travers cela, à l'exception du caractère générique %
. Cela pourrait être dangereux si vous utilisiez l'instruction LIKE
, car l'attaquant pourrait mettre simplement %
comme identifiant de connexion si vous ne filtrez pas cette information et que vous devez simplement forcer brutalement le mot de passe de l'un de vos utilisateurs ..__ 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 de telles requêtes simples, il serait probablement plus efficace de faire quelque chose comme $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);
j'ai été confronté à cela et je vous suggérerais de travailler avec PDO, mais dans certains cas, vous voudrez peut-être essayer cette méthode. Cela fonctionne et très simple. Je me demande pourquoi les gens l'ont négligé.
Échantillon de code . // Utiliser mon framework Moorexa
il contient un ORM riche et bien plus encore .. .. Mais je devais effectuer ce test pour être sûr d’une alternative aux personnes qui écrivent des instructions SQL brutes.
exemple.
// checking from a user table
$check = DB::table('api_users')->get(['username' => "admin' or password='1'"])->run();
// expected output
SELECT * FROM api_users WHERE username='admin' or password='1'
//sql generated output
SELECT * FROM api_users WHERE username='admin\' or password=\'1\''
// lets try something heavy
$check = DB::table($table)->get(['username' => "admin' or 1=1 UNION SELECT password FROM api_users where id=1"])->run();
// expected output
SELECT * FROM api_users WHERE username='admin' or 1=1 UNION SELECT password FROM api_users where id=1
// this would pass and fail
SELECT * FROM api_users WHERE username='admin\' or 1=1 UNION SELECT password FROM api_users where id=1'
alors, quel est le Gist.
je vais vous montrer un exemple de code en effectuant une requête select.
// let's assume. would all work
$input = ['username' => "moorexa"]; //or $_POST or $_GET
$sql = 'SELECT * FROM '.$table.' ';
$safe = "";
// let's grab the user input from the array
foreach ($input as $key => $val)
{
switch($val)
{
case is_string($val):
$safe .= $key .'=\''.addslashes($val).'\' AND ';
break;
case is_int($val):
$safe .= $key .'='.((int) $val).' AND ';
break;
case is_float($val):
case is_double($val):
$safe .= $key .'='.(double) $val.' AND ';
break;
default:
// this failed
}
}
$safe = rtrim($safe, "AND ");
$sql .= ' WHERE '. $safe .' ';
// now sql contains a valid statement. and would only fail when terms are not met.
// Hope you can apply this and also use more test cases.