L'injection SQL qui contourne mysql la vraie chaîne d'échappement()

y a-t-il une possibilité D'injection SQL même en utilisant la fonction mysql_real_escape_string() ?

considérez cette situation type. 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-ci est encore dangereux et possible à pirater même avec la fonction mysql_real_escape_string() utilisée. Mais je ne vois aucun exploit possible?

injections classiques comme celle-ci:

aaa' OR 1=1 --

ne fonctionne pas.

connaissez-vous une injection possible qui pourrait passer par le code PHP ci-dessus?

516
demandé sur Brad Larson 2011-04-21 11:56:11

4 réponses

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 utilisez des guillemets simples ( ' ' ) autour de vos variables à l'intérieur de votre requête est ce qui vous protège contre cela. la mention suivante est aussi une option:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
318
répondu Wesley van Opdorp 2017-12-16 12:45:41

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

pour les bordures très obscures!!!

la longue réponse n'est pas si facile. Il est basé sur une attaque démontré ici .

L'Attaque

donc, 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, que sera de retour plus de 1 ligne. Voyons 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 de l'encodage que le serveur attend sur la connexion à la fois pour encoder ' comme dans ASCII i.e. 0x27 et pour avoir un caractère dont le octet final est un ASCII \ i.e. 0x5c . Comme il s'avère qu'il y a 5 codages de ce type pris en charge dans MySQL 5.6 par défaut: big5 , cp932 , gb2312 , gbk et sjis . Nous sélectionnerons gbk ici.

    maintenant, il est très important de noter l'utilisation de SET NAMES ici. Ce paramètre définit le jeu de caractères sur le serveur . Si nous utilisions l'appel à la fonction API C mysql_set_charset() , nous serions très bien (sur les versions MySQL depuis 2006). Mais pourquoi dans une minute...

  2. La Charge", 1519770920"

    La charge que nous allons utiliser pour cette injection commence avec la séquence d'octets 0xbf27 . Dans gbk , c'est un caractère multibyte invalide; dans latin1 , c'est la chaîne ¿' . Notez que dans latin1 et gbk , 0x27 en elle-même est un caractère littéral ' .

    nous avons choisi cette charge utile parce que, si nous y appelions addslashes() , nous y insérerions un ASCII \ i.e. 0x5c , avant le caractère ' . Donc, nous finirions 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 ' non enregistré . Mais on n'utilise 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 que le serveur attend. Cependant, jusqu'à présent, le client pense que nous utilisons toujours latin1 pour la connexion, parce que nous ne lui avons jamais dit le contraire. Nous avons dit le serveur nous utilisons gbk , mais le" client 1519980920 pense toujours que c'est latin1 .

    C'est pourquoi l'appel à mysql_real_escape_string() insère le caractère" backslash", et nous avons un caractère" free hanging "1519120920 dans notre contenu "Escape"! En fait, si nous devions regarder $var dans le jeu de caractères gbk , nous verrions:

    縗' OR 1=1 /*

    , Qui est exactement ce que l'attaque nécessite.

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

Le Mauvais

ça empire. PDO par défaut à émuler déclarations préparées avec MySQL. Cela signifie que du côté du client, il fait essentiellement 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 /*"));

maintenant, il est intéressant de noter que vous pouvez empêcher cela en désactivant émulé déclarations préparées:

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

cela donnera habituellement une véritable déclaration préparée (c'est-à-dire que les données sont envoyées sur dans un paquet distinct de la requête). Cependant, soyez conscient que PDO va silencieusement failback à l'émulation des déclarations que MySQL ne peut pas préparer nativement: ceux qu'il peut être énumérés dans le manuel, mais attention à sélectionner la version de serveur appropriée).

Le Truand

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 si vous utilisez une version MySQL depuis 2006.

si vous utilisez une version antérieure de MySQL, alors un bug 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 pour échapper à des fins même si le client avait été correctement informé du codage de connexion et donc cette attaque aurait tout de même réussi. 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 précédentes il ne peut pas prévenir cette attaque pour toutes les commandes possibles! Il est maintenant exposé comme un paramètre DSN .

La Grâce Salvatrice

comme nous cela dit, 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 non vulnérables et qui, pourtant, peuvent soutenir tous "les 1519990920" caractère Unicode: alors vous pourriez choisir d'utiliser à la place-mais il a seulement été disponible depuis MySQL 5.5.3. Une alternative est utf8 , qui est également non vulnérable et peut prendre en charge l'ensemble de L'Unicode plan de base multilingue .

alternativement, vous pouvez activer le mode NO_BACKSLASH_ESCAPES 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'évasion ne peut pas créer des caractères valides dans l'un des vulnérables les encodages où ils n'existaient pas auparavant (par exemple 0xbf27 est toujours 0xbf27 etc.-le serveur sera toujours rejeter la chaîne comme non valide. Cependant, voir la réponse de @eggyal pour une vulnérabilité différente qui peut résulter de l'utilisation de ce mode SQL.

Exemples De Sécurité

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 é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 bien 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 déclarations préparées tout le temps.

Enveloppant

si vous:

  • utiliser les Versions modernes de MySQL (fin 5.1, tous 5.5, 5.6, etc) et mysql_set_charset() / $mysqli->set_charset() / paramètre DSN de PDO (en PHP ≥ 5.3.6)

ou

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

Vous êtes 100% sûr.

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

548
répondu ircmaxell 2017-05-23 11:55:01

TL; DR

mysql_real_escape_string() sera n'offrent aucune protection que ce soit (et pourrait en outre munge vos données) si:

  • MySQL NO_BACKSLASH_ESCAPES SQL-mode est activé ( pourrait , à moins que vous explicitement sélectionnez un autre mode SQL à chaque fois que vous vous connectez ); et

  • votre chaîne de caractères SQL littérales sont citées en double citation " caractères.

ceci a été déposé comme bogue n ° 72458 et a été corrigé dans MySQL v5.7.6 (Voir la section intitulée " "the Saving Grace ", ci-dessous).

ceci est un autre, (peut-être moins?) obscur CAS de BORD!!!

en hommage à @ircmaxell's excellente réponse (en fait, c'est censé être flatteur et pas plagiat!), J'adopterai son format:

L'Attaque

commence 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');

ceci retournera tous les enregistrements de la table test . A dissection:

  1. sélection un mode SQL

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

    Comme indiqué sous Littéraux de Chaîne :

    il y a plusieurs façons d'inclure des guillemets dans une chaîne de caractères:

    • " ' "à l'intérieur d'une chaîne de cité" ' "peut être écrit" '' ".

    • " " " à l'intérieur d'un chaîne de cité " " "peut être écrit" "" ".

    • précède le caractère de citation par un caractère d'évasion (" \ ").

    • " ' "à l'intérieur d'une chaîne de cité" " " faut pas de 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 inclut NO_BACKSLASH_ESCAPES , alors 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. A noter que l'effet de la quatrième balle est que l'on doit nécessairement connaître le caractère qui sera utilisé pour la citation littérale, afin d'éviter munging des données.

  2. La Charge", 1519740920"

    " OR 1=1 -- 
    

    la charge utile initie littéralement cette injection avec le caractère " . Pas de codage particulier. Pas de personnages 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ée si le mode NO_BACKSLASH_ESCAPES SQL est utilisé. Comme mentionné ci-dessus, une telle fonction a besoin de savoir quel caractère sera utilisé pour citer le littéral afin de le répéter sans provoquer l'autre caractère de citation d'être répété littéralement.

    Toutefois, cette fonction arbitrairement suppose que la chaîne sera citée en utilisant le caractère ' . 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++= '\'';
        }
    

    ainsi, il laisse les caractères double-citation " Intacts (Et Double tous les caractères simple-citation ' ) indépendamment du caractère réel qui est utilisé pour citer le littéral ! Dans notre cas, $var reste exactement le même que l'argument cela a été fourni à mysql_real_escape_string() - c'est comme si aucune fuite n'avait 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 mon vieil ami l'a dit: Félicitations, vous venez d'attaquer avec succès un programme 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 ne peut mysqli::real_escape_string() , puisque c'est juste un emballage différent autour de cette même fonction.

le problème, si ce n'est déjà évident, est que l'appel à mysql_real_escape_string() ne peut pas savoir avec lequel le caractère littéral sera cité, comme c'est laissé au développeur de décider à un moment ultérieur. Ainsi, dans le mode NO_BACKSLASH_ESCAPES , il y a littéralement no way que cette fonction peut échapper en toute sécurité à chaque entrée pour une utilisation avec des citations arbitraires (du moins, pas sans doublage de caractères qui ne nécessitent pas de doublage et donc munging vos données).

Le Truand

ça empire. 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 la norme SQL (voir par exemple la section 5.3 de la spécification SQL-92 , à savoir la production grammaticale <quote symbol> ::= <quote><quote> et l'absence de tout sens particulier donné à backslash). En outre, son utilisation a été explicitement recommandé comme une solution de rechange à la (depuis longtemps fixe) bug que le post de ircmaxell décrit. Qui sait, certains ad peuvent même le configurer pour qu'il soit activé par défaut comme moyen de décourager l'utilisation de méthodes d'échappement incorrectes comme addslashes() .

en outre, le mode SQL d'une nouvelle connexion est défini par le serveur en fonction de sa configuration (qu'un utilisateur SUPER peut changer à tout moment); ainsi, pour être certain du comportement du serveur, vous devez toujours spécifier explicitement votre mode souhaité après la connexion.

La Grâce Salvatrice

aussi longtemps que vous toujours explicitement définit le mode SQL pour ne pas inclure NO_BACKSLASH_ESCAPES , ou cite des chaînes de caractères MySQL en utilisant le caractère mono-citation, ce bogue ne peut pas faire marche arrière: respectivement escape_quotes_for_mysql() ne sera pas utilisé, ou son hypothèse au sujet des caractères guillemets qui doivent être répétés sera correcte.

pour cette raison, je recommande que n'importe qui utilisant NO_BACKSLASH_ESCAPES permet aussi ANSI_QUOTES mode, car il forcera l'utilisation habituelle de unique cité des littéraux de chaîne. Notez que cela n'empêche pas L'injection de SQL dans le cas où des littérales doubles sont utilisées-cela réduit simplement la probabilité que cela se produise (parce que les requêtes normales et non malveillantes échoueraient).

en AOP, à la fois sa fonction équivalente PDO::quote() et son émulateur de déclaration préparé font appel à mysql_handle_quoter() -ce qui fait exactement cela: il assure que les évadés literal est cité En guillemets simples, de sorte que vous pouvez être certain que PDO est toujours à l'abri de ce bug.

à partir de MySQL v5.7.6, ce bug a été corrigé. Voir change log :

fonctionnalité ajoutée ou modifiée

Safe "Exemples D'15191080920"

Pris avec le bug expliqué par ircmaxell, les exemples suivants sont tout à fait sûrs (en supposant qu'on utilise MySQL après 4.1.20, 5.0.22, 5.1.11; ou qu'on n'utilise pas D'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 qu'on cite notre chaîne de caractères avec des guillemets.

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

...parce que les déclarations préparées par AOP sont immunisées contre cette vulnérabilité (et celle d'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 des déclarations préparées ait été désactivée.)

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

...parce que la fonction quote() de PDO échappe non seulement au littéral, mais le cite aussi (en caractères ' ); notez que pour éviter le bug de ircmaxell dans ce cas, vous doit utiliser PHP≥5.3.6 et ont 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 déclarations préparées par MySQLi sont sûres.

Enveloppant

ainsi, si vous:

  • natif déclarations préparées à l'avance

ou

  • utiliser MySQL v5.7.6 ou plus tard

ou

  • dans ajout à l'emploi d'une des solutions dans ircmaxell résumé, l'utilisation d'au moins l'un de:

    • AOP;
    • "1519770920 d'une" cité des littéraux de chaîne; ou
    • un mode SQL explicitement défini qui ne comprend pas NO_BACKSLASH_ESCAPES

...ensuite, vous devrait être complètement sûr (vulnérabilités en dehors de la portée de la chaîne s'échappant de côté).

142
répondu eggyal 2017-05-23 12:10:45

Eh bien, il n'y a vraiment rien qui puisse passer par là, à part le Joker % . Il pourrait être dangereux si vous utilisiez LIKE déclaration comme attaquant pourrait mettre juste % comme login si vous ne filtrez pas cela, et devrait juste bruteforce un mot de passe de l'un de vos utilisateurs. Les gens suggèrent souvent d'utiliser des déclarations préparées pour le rendre 100% sûr, car les données ne peuvent pas interférer avec la requête elle-même de cette façon. Mais pour des questions aussi simples il serait probablement plus efficace pour faire quelque chose comme $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);

19
répondu Slava 2011-04-21 08:15:22