Question Injection SQL qui contourne mysql_real_escape_string ()


Existe-t-il une possibilité d’injection SQL même en utilisant mysql_real_escape_string() fonction?

Considérez cet exemple de situation. 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-là est encore dangereux et possible de bidouiller même avec mysql_real_escape_string() fonction utilisée. Mais je ne peux penser à aucun exploit possible?

Des injections classiques comme celle-ci:

aaa' OR 1=1 --

ne fonctionnent pas.

Connaissez-vous une injection possible qui passerait par le code PHP ci-dessus?


495
2018-04-21 07:56


origine


Réponses:


Considérez 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 à l'intérieur de votre requête est ce qui vous protège contre cela. Ce qui suit est également une option:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";

307
2018-04-21 08:05



La réponse courte est oui, oui il y a un moyen de se déplacer mysql_real_escape_string().

Pour les cas très OBSCURE EDGE !!!

La longue réponse 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 retournera plus d'une rangée. Disons ce qui se passe ici:

  1. Sélection d'un jeu de caractères

    mysql_query('SET NAMES gbk');
    

    Pour que cette attaque fonctionne, nous avons besoin du codage que le serveur attend de la connexion pour encoder ' comme en ASCII, c'est-à-dire 0x27  et avoir un caractère dont le dernier octet est un ASCII \ c'est à dire. 0x5c. Il s'avère que 5 encodages de ce type sont supportés 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. Cela 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 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. Dans gbk, c'est un caractère multi-octet invalide; dans latin1c'est la ficelle ¿'. Notez que dans latin1  et  gbk, 0x27 à lui seul est un littéral ' personnage.

    Nous avons choisi cette charge parce que, si nous avons appelé addslashes() dessus, nous insérerions un ASCII \ c'est à dire. 0x5c, avant le ' personnage. Nous nous retrouverions avec 0xbf5c27, qui dans gbk est une séquence de deux caractères: 0xbf5c suivi par 0x27. Ou en d'autres termes, un valide caractère suivi d'un non échappé '. Mais nous n'utilisons pas addslashes(). Donc, à la prochaine étape ...

  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. Il peut donc effectuer l'échappement correctement pour le jeu de caractères attendu par le serveur. Cependant, jusqu'à présent, le client pense que nous utilisons encore latin1 pour la connexion, parce que nous ne l'avons jamais dit autrement. Nous avons dit au serveur nous utilisons gbk, mais le client pense toujours qu'il est latin1.

    Par conséquent, l'appel à mysql_real_escape_string() insère le backslash, et nous avons une suspension libre ' personnage dans notre contenu "échappé"! En fait, si nous devions regarder $var dans le gbk jeu de caractères, nous verrons:

    縗 'OU 1 = 1 / *

    Lequel est exactement quoi l'attaque nécessite.

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

Le mauvais

Ça a empiré. PDO par défaut émuler instructions préparées avec MySQL. Cela signifie que du côté du client, il fait essentiellement un sprint mysql_real_escape_string() (dans la bibliothèque C), ce qui signifie que l'injection suivante sera 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 les instructions préparées émulées:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Cette volonté d'habitude résulte en une instruction préparée réelle (c'est-à-dire que les données sont envoyées dans un paquet distinct de la requête). Cependant, sachez que PDO sera silencieusement se retirer à émuler des déclarations que MySQL ne peut pas préparer nativement: ceux qui peuvent être listé dans le manuel, mais attention à sélectionner la version de serveur appropriée).

Le moche

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 punaise 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 à des fins d'échappement même si le client a été correctement informé de l'encodage de la connexion et ainsi cette attaque réussirait toujours. 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 antérieures, il ne peux pas empêcher cette attaque pour toutes les commandes possibles!  Il est maintenant exposé 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 en utilisant un jeu de caractères vulnérable. utf8mb4 est pas vulnérable et peut encore soutenir chaque Caractère Unicode: vous pouvez donc l'utiliser à la place, mais il n'est disponible que depuis MySQL 5.5.3. Une alternative est utf8, qui est aussi pas vulnérable et peut supporter l'ensemble de l'Unicode Plan multilingue de base.

Alternativement, vous pouvez activer NO_BACKSLASH_ESCAPES Mode 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'échappement ne peux pas créer des caractères valides dans l'un des codages vulnérables où ils n'existaient pas auparavant (c.-à-d. 0xbf27est encore 0xbf27 etc.) - de sorte que le serveur rejettera toujours la chaîne comme invalide. Cependant, voir @ réponse eggyal pour une autre vulnérabilité pouvant 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 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 de vraies instructions préparées tout le temps.

Emballer

Si vous:

  • Utilisez les versions modernes de MySQL (fin 5.1, tous les 5.5, 5.6, etc) ET  mysql_set_charset() / $mysqli->set_charset() / Paramètre de jeu de caractères DSN de PDO (en PHP ≥ 5.3.6)

OU

  • N'utilisez pas de jeu de caractères vulnérable pour l'encodage de connexion (vous utilisez uniquement utf8 / latin1 / ascii / etc)

Vous êtes sûr à 100%.

Sinon, vous êtes vulnérable même si vous utilisez mysql_real_escape_string()...


528
2017-08-25 02:08



TL; DR

mysql_real_escape_string() volonté n'offrir aucune protection (et pourrait en outre recueillir vos données) si:

  • MySQL NO_BACKSLASH_ESCAPES Le mode SQL est activé (qu'il pourrait être, à moins que vous explicitement sélectionner un autre mode SQL chaque fois que vous vous connectez) et

  • vos littéraux de chaîne SQL sont cités en utilisant une double citation " personnages.

Cela a été déposé comme bug # 72458 et a été corrigé dans MySQL v5.7.6 (voir la section intitulée "La grâce salvatrice", au dessous de).

Ceci est un autre, (peut-être moins?) Obscur EDGE CASE !!!

En hommage à @ excellente réponse de ircmaxell (vraiment, c'est censé être de la flatterie et non du plagiat!), j'adopterai son format:

L'attaque

Commençant 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');

Cela retournera tous les enregistrements de la test table. Une dissection:

  1. Sélection d'un mode SQL

    mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
    

    Comme documenté sous Littéraux de chaîne:

    Il existe plusieurs manières d’inclure des guillemets dans une chaîne:

    • UNE "'"À l'intérieur d'une chaîne citée avec"'"Peut être écrit comme"''".

    • UNE """À l'intérieur d'une chaîne citée avec"""Peut être écrit comme"""".

    • Faire précéder le caractère de citation d'un caractère d'échappement ("\").

    • UNE "'"À l'intérieur d'une chaîne citée avec"""N'a pas besoin d'un 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 comprend NO_BACKSLASH_ESCAPES, puis la troisième de ces options - qui est l'approche habituelle adoptée par mysql_real_escape_string()- n'est pas disponible: l'une des deux premières options doit être utilisée à la place. Notez que l'effet de la quatrième puce est que l'on doit nécessairement connaître le caractère qui va être utilisé pour citer le littéral afin d'éviter de mélanger ses données.

  2. La charge utile

    " OR 1=1 -- 
    

    La charge utile initie cette injection très littéralement avec le " personnage. Pas de codage particulier. Pas de caractères spéciaux Pas d'octets bizarres.

  3. 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. Voir libmysql.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é si le NO_BACKSLASH_ESCAPESLe mode SQL est utilisé. Comme mentionné ci-dessus, une telle fonction doit savoir quel caractère sera utilisé pour citer le littéral afin de le répéter sans provoquer la répétition littérale de l’autre caractère de citation.

    Cependant, cette fonction arbitrairement suppose que la chaîne sera citée en utilisant la guillemets simples ' personnage. Voir charset.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++= '\'';
        }
    

    Donc, il laisse une double citation " caractères intacts (et double tous les guillemets simples ' personnages) quel que soit le caractère réel utilisé pour citer le littéral! Dans notre cas $var reste exactement le même que l'argument qui a été fourni à mysql_real_escape_string()-il est comme si aucune fuite n'a eu lieu du tout.

  4. 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 l’a dit mon confrère: félicitations, vous venez d’attaquer avec succès un programme en 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 peut mysqli::real_escape_string(), puisque ce n'est qu'une enveloppe différente autour de cette même fonction.

Le problème, s'il n'est pas déjà évident, est que l'appel à mysql_real_escape_string()  ne peux pas savoir avec quel caractère le littéral sera cité, car cela est laissé au développeur pour décider plus tard. Donc, dans NO_BACKSLASH_ESCAPES mode, il y a littéralement en aucune façon que cette fonction peut échapper en toute sécurité à toutes les entrées à utiliser avec des citations arbitraires (du moins, pas sans doubler les caractères qui ne nécessitent pas de doubler et donc de brouiller vos données).

Le moche

Ça a empiré. 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 le standard SQL (par exemple, voir la section 5.3 de la Spécification SQL-92, à savoir le <quote symbol> ::= <quote><quote> la production de grammaire et l'absence de toute signification particulière donnée à la barre oblique inverse). De plus, son utilisation était explicitement recommandé comme solution de contournement au (depuis longtemps fixé) punaise que le message d'ircmaxell décrit. Qui sait, certains administrateurs de bases de données peuvent même le configurer pour être activé par défaut afin de décourager l'utilisation de méthodes d'échappement incorrectes comme addslashes().

Également Mode SQL d'une nouvelle connexion est défini par le serveur en fonction de sa configuration SUPER l'utilisateur peut changer à tout moment); donc, pour être certain du comportement du serveur, vous devez toujours spécifiez explicitement votre mode désiré après la connexion.

La grâce salvatrice

Tant que vous avez toujours explicitement mettre le mode SQL à ne pas inclure NO_BACKSLASH_ESCAPES, ou citer des littéraux de chaîne MySQL en utilisant le caractère à guillemet simple, ce bogue ne peut pas renvoyer sa tête laide: respectivement escape_quotes_for_mysql() ne sera pas utilisé, ou son hypothèse sur les guillemets à répéter sera correcte.

Pour cette raison, je recommande que quiconque utilise NO_BACKSLASH_ESCAPES permet également ANSI_QUOTES mode, car il va forcer l'utilisation habituelle des littéraux de chaîne entre guillemets simples. Notez que cela n'empêche pas l'injection SQL dans le cas où des littéraux entre guillemets seraient utilisés - cela réduit simplement la probabilité que cela se produise (car les requêtes normales et non malveillantes échoueraient).

Dans PDO, à la fois sa fonction équivalente PDO::quote() et son appel à l'émulateur d'instruction préparé mysql_handle_quoter()-qui fait exactement cela: il garantit que le littéral échappé est entre guillemets simples, donc vous pouvez être certain que PDO est toujours à l'abri de ce bug.

Depuis MySQL v5.7.6, ce bug a été corrigé. Voir modifier le journal:

Fonctionnalité ajoutée ou modifiée

Exemples sûrs

Pris ensemble avec le bogue expliqué par ircmaxell, les exemples suivants sont entièrement sûrs (en supposant que l'on utilise soit MySQL après 4.1.20, 5.0.22, 5.1.11; soit que l'on n'utilise pas un 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 que nous citons notre chaîne littérale avec des guillemets simples.

$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);

... parce que les instructions préparées par PDO sont à l'abri de cette vulnérabilité (et des 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 de la déclaration préparée ait été désactivée) .

$var  = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");

... parce que les AOP quote() La fonction non seulement échappe au littéral, mais aussi le cite (en guillemets simples ' personnages); notez que pour éviter le bogue d'ircmaxell dans ce cas, vous doit utiliser PHP≥5.3.6 et avez 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 instructions préparées MySQLi sont sûres.

Emballer

Ainsi, si vous:

  • utiliser des instructions préparées natives

OU

  • utiliser MySQL v5.7.6 ou ultérieur

OU

  • dans une addition pour utiliser l'une des solutions du résumé d'ircmaxell, utilisez au moins l'un des éléments suivants:

    • AOP;
    • littéraux de chaîne entre guillemets simples; ou
    • un mode SQL explicitement défini qui n'inclut pas NO_BACKSLASH_ESCAPES

...alors vous devrait être totalement sûr (vulnérabilités hors de la portée de la chaîne s'échappant).


134
2018-04-24 19:15



Eh bien, il n'y a vraiment rien qui puisse passer par là, à part % joker Cela pourrait être dangereux si vous utilisiez LIKE déclaration que l'attaquant pourrait mettre juste % en tant que connexion si vous ne filtrez pas cela, et que vous deviez juste renforcer par bruteforce un mot de passe de n'importe lequel de vos utilisateurs. Les utilisateurs suggèrent souvent d'utiliser des instructions préparées pour garantir la sécurité à 100%, car les données ne peuvent pas interférer avec la requête elle-même. 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);


17
2018-04-21 08:01