Inyección SQL que evita mysql_real_escape_string()

Resuelto Richard Knop asked hace 54 años • 4 respuestas

¿Existe la posibilidad de inyección SQL incluso cuando se utiliza mysql_real_escape_string()la función?

Considere esta situación de muestra. SQL se construye en PHP así:

$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));

$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";

He escuchado a numerosas personas decirme que un código como ese sigue siendo peligroso y posible de piratear incluso con mysql_real_escape_string()la función utilizada. ¿Pero no puedo pensar en ningún posible exploit?

Inyecciones clásicas como esta:

aaa' OR 1=1 --

No funcionan.

¿Conoce alguna posible inyección que pasaría por el código PHP anterior?

Richard Knop avatar Jan 01 '70 08:01 Richard Knop
Aceptado

La respuesta corta es sí, sí, hay una manera de desplazarsemysql_real_escape_string() . #¡¡¡Para CASOS DE BORDE MUY OSCUROS!!!

La respuesta larga no es tan fácil. Se basa en un ataque demostrado aquí .

El ataque

Entonces, comencemos mostrando el ataque...

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");

En determinadas circunstancias, eso devolverá más de 1 fila. Analicemos lo que está pasando aquí:

  1. Seleccionar un conjunto de caracteres

    mysql_query('SET NAMES gbk');
    

    Para que este ataque funcione, necesitamos que la codificación que el servidor espera en la conexión esté codificada como 'en ASCII, es decir 0x27 , y que tenga algún carácter cuyo byte final sea ASCII, es \decir 0x5c. Resulta que MySQL 5.6 admite cinco codificaciones de este tipo de forma predeterminada : big5, cp932, y . Seleccionaremos aquí.gb2312gbksjisgbk

    Ahora bien, es muy importante tener en cuenta el uso de SET NAMESaquí. Esto establece el conjunto de caracteres EN EL SERVIDOR . Si usáramos la llamada a la función API de C mysql_set_charset(), estaríamos bien (en las versiones de MySQL desde 2006). Pero más sobre por qué en un minuto...

  2. La carga útil

    La carga útil que usaremos para esta inyección comienza con la secuencia de bytes 0xbf27. En gbk, ese es un carácter multibyte no válido; en latin1, es la cuerda ¿'. Tenga en cuenta que en latin1 y gbk , 0x27por sí solo es un 'carácter literal.

    Hemos elegido esta carga útil porque, si addslashes()la invocamos, insertaríamos un ASCII , \es decir 0x5c, antes del 'carácter. Entonces terminaríamos con 0xbf5c27, que gbkes una secuencia de dos caracteres: 0xbf5cseguido de 0x27. O en otras palabras, un carácter válido seguido de un archivo sin escape '. Pero no lo estamos usando addslashes(). Así que vamos al siguiente paso...

  3. mysql_real_escape_string()

    La llamada a la API de C se mysql_real_escape_string()diferencia addslashes()en que conoce el juego de caracteres de conexión. Por lo tanto, puede realizar el escape correctamente para el conjunto de caracteres que espera el servidor. Sin embargo, hasta este punto, el cliente piensa que todavía lo estamos usando latin1para la conexión, porque nunca le dijimos lo contrario. Le dijimos al servidor que estamos usando gbk, pero el cliente todavía piensa que es así latin1.

    Por lo tanto, la llamada a mysql_real_escape_string()inserta la barra invertida y tenemos un 'carácter colgante libre en nuestro contenido "escapado". De hecho, si miráramos $varel gbkconjunto de caracteres, veríamos:

    縗' OR 1=1 /*

    Que es exactamente lo que requiere el ataque.

  4. La consulta

    Esta parte es sólo una formalidad, pero aquí está la consulta representada:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

Felicitaciones, acaba de atacar con éxito un programa usando mysql_real_escape_string()...

El malo

Se pone peor. PDOEl valor predeterminado es emular declaraciones preparadas con MySQL. Eso significa que en el lado del cliente, básicamente realiza un sprintf mysql_real_escape_string()(en la biblioteca C), lo que significa que lo siguiente resultará en una inyección exitosa:

$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Ahora bien, vale la pena señalar que puedes evitar esto deshabilitando las declaraciones preparadas emuladas:

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

Esto generalmente dará como resultado una declaración preparada verdadera (es decir, los datos se envían en un paquete separado de la consulta). Sin embargo, tenga en cuenta que PDO recurrirá silenciosamente a la emulación de declaraciones que MySQL no puede preparar de forma nativa: las que sí puede se enumeran en el manual, pero tenga cuidado al seleccionar la versión de servidor adecuada).

El feo

Dije desde el principio que podríamos haber evitado todo esto si hubiéramos usado mysql_set_charset('gbk')en lugar de SET NAMES gbk. Y eso es cierto siempre que esté utilizando una versión de MySQL desde 2006.

Si está utilizando una versión anterior de MySQL, entonces un error significaba mysql_real_escape_string()que los caracteres multibyte no válidos, como los de nuestra carga útil, se trataban como bytes únicos para fines de escape, incluso si el cliente había sido informado correctamente de la codificación de la conexión , por lo que este ataque todavía tiene éxito. El error se solucionó en MySQL 4.1.20 , 5.0.22 y 5.1.11 .

Pero la peor parte es que PDOno expuso la API de C mysql_set_charset()hasta la versión 5.3.6, por lo que en versiones anteriores no puede evitar este ataque para cada comando posible. Ahora está expuesto como un parámetro DSN .

La gracia salvadora

Como decíamos al principio, para que este ataque funcione la conexión a la base de datos debe estar codificada mediante un conjunto de caracteres vulnerable. no utf8mb4es vulnerable y aun así puede admitir todos los caracteres Unicode: por lo que podría optar por usarlo en su lugar, pero solo ha estado disponible desde MySQL 5.5.3. Una alternativa es utf8, que tampoco es vulnerable y puede soportar todo el plano multilingüe básico Unicode .

Alternativamente, puede habilitar el NO_BACKSLASH_ESCAPESmodo SQL, que (entre otras cosas) altera el funcionamiento de mysql_real_escape_string(). Con este modo habilitado, 0x27será reemplazado por 0x2727en lugar de 0x5c27y, por lo tanto, el proceso de escape no puede crear caracteres válidos en ninguna de las codificaciones vulnerables donde no existían anteriormente (es decir, 0xbf27todavía está 0xbf27, etc.), por lo que el servidor seguirá rechazando la cadena como no válida. . Sin embargo, consulte la respuesta de @eggyal para conocer una vulnerabilidad diferente que puede surgir al usar este modo SQL.

Ejemplos seguros

Los siguientes ejemplos son seguros:

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");

Porque el servidor está esperando 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");

Porque hemos configurado correctamente el juego de caracteres para que el cliente y el servidor coincidan.

$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 /*"));

Porque hemos desactivado las declaraciones preparadas emuladas.

$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 /*"));

Porque hemos configurado el juego de caracteres correctamente.

$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();

Porque MySQLi hace declaraciones verdaderas preparadas todo el tiempo.

Terminando

Si usted:

  • Utilice versiones modernas de MySQL (últimas 5.1, todas 5.5, 5.6, etc.) Y mysql_set_charset() // $mysqli->set_charset()el parámetro de juego de caracteres DSN de PDO (en PHP ≥ 5.3.6)

O

  • No utilice un conjunto de caracteres vulnerables para la codificación de la conexión (solo utilice utf8/ latin1/ ascii/ etc.)

Estás 100% seguro.

De lo contrario, eres vulnerable aunque estés usandomysql_real_escape_string() ...

ircmaxell avatar Aug 25 '2012 02:08 ircmaxell

Considere la siguiente consulta:

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

mysql_real_escape_string()no te protegerá contra esto. El hecho de que utilice comillas simples ( ' ') alrededor de las variables dentro de su consulta es lo que le protege contra esto. La siguiente también es una opción:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
Wesley van Opdorp avatar Apr 21 '2011 08:04 Wesley van Opdorp

TL;DR

mysql_real_escape_string()no proporcionará protección alguna (y además podría manipular sus datos) si:

  • NO_BACKSLASH_ESCAPESEl modo SQL de MySQL está habilitado (lo cual podría estarlo, a menos que seleccione explícitamente otro modo SQL cada vez que se conecte ); y

  • los literales de sus cadenas SQL se citan entre comillas dobles ".

Esto se registró como error #72458 y se solucionó en MySQL v5.7.6 (consulte la sección titulada " La gracia salvadora ", a continuación).

¡¡¡Este es otro (¿quizás menos?) oscuro CASO EDGE !!!

En homenaje a la excelente respuesta de @ircmaxell (¡en realidad, se supone que esto es un halago y no un plagio!), adoptaré su formato:

El ataque

Empezando con una demostración...

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

Esto devolverá todos los registros de la testtabla. Una disección:

  1. Seleccionar un modo SQL

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

    Como se documenta en Literales de cadena :

    Hay varias formas de incluir comillas dentro de una cadena:

    • Un " '" dentro de una cadena citada con " '" puede escribirse como " ''".

    • Un " "" dentro de una cadena citada con " "" puede escribirse como " """.

    • Anteponga el carácter de comillas por un carácter de escape (“ \”).

    • Un " '" dentro de una cadena entrecomillada con " "" no necesita un tratamiento especial y no es necesario duplicarlo ni escaparlo. De la misma manera, " "" dentro de una cadena citada con " '" no necesita ningún tratamiento especial.

    Si el modo SQL del servidor incluye NO_BACKSLASH_ESCAPES, entonces la tercera de estas opciones (que es el enfoque habitual adoptado por mysql_real_escape_string()) no está disponible: en su lugar se debe utilizar una de las dos primeras opciones. Tenga en cuenta que el efecto del cuarto punto es que necesariamente se debe conocer el carácter que se utilizará para citar el literal para evitar alterar los datos.

  2. La carga útil

    " OR 1=1 -- 
    

    La carga útil inicia esta inyección literalmente con el "personaje. Sin codificación particular. Sin caracteres especiales. Sin bytes extraños.

  3. mysql_real_escape_string()

    $var = mysql_real_escape_string('" OR 1=1 -- ');
    

    Afortunadamente, mysql_real_escape_string()verifica el modo SQL y ajusta su comportamiento en consecuencia. Ver 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);
    }
    

    escape_quotes_for_mysql()Por lo tanto, se invoca una función subyacente diferente, si NO_BACKSLASH_ESCAPESse utiliza el modo SQL. Como se mencionó anteriormente, dicha función necesita saber qué carácter se utilizará para citar el literal para poder repetirlo sin que el otro carácter entre comillas se repita literalmente.

    Sin embargo, esta función supone arbitrariamente que la cadena se citará utilizando el 'carácter de comilla simple. Ver 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++= '\'';
        }
    

    Por lo tanto, deja "intactos los caracteres entre comillas dobles (y duplica todos los caracteres entre comillas simples ') independientemente del carácter real que se utilice para citar el literal . En nuestro caso $varsigue siendo exactamente el mismo que el argumento que se le proporcionó mysql_real_escape_string(): es como si no hubiera habido escape alguno .

  4. La consulta

    mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
    

    Algo así como una formalidad, la consulta representada es:

    SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
    

Como dijo mi erudito amigo: felicidades, acaba de atacar con éxito un programa usando mysql_real_escape_string()...

El malo

mysql_set_charset()No puedo ayudar, ya que esto no tiene nada que ver con los conjuntos de caracteres; ni puede mysqli::real_escape_string(), ya que es solo un contenedor diferente alrededor de esta misma función.

El problema, si aún no es obvio, es que la llamada a mysql_real_escape_string() no puede saber con qué carácter se citará el literal, ya que eso queda en manos del desarrollador para decidirlo más adelante. Entonces, en NO_BACKSLASH_ESCAPESmodo, literalmente no hay manera de que esta función pueda escapar de manera segura de cada entrada para usarla con comillas arbitrarias (al menos, no sin duplicar caracteres que no requieren duplicación y, por lo tanto, alterar sus datos).

El feo

Se pone peor. NO_BACKSLASH_ESCAPESPuede que no sea tan poco común en la naturaleza debido a la necesidad de su uso para la compatibilidad con SQL estándar (por ejemplo, consulte la sección 5.3 de la especificación SQL-92 , es decir, la <quote symbol> ::= <quote><quote>producción gramatical y la falta de un significado especial dado a la barra invertida). Además, su uso se recomendó explícitamente como solución alternativa al error (ya solucionado hace mucho tiempo) que describe la publicación de ircmaxell. Quién sabe, algunos administradores de bases de datos podrían incluso configurarlo para que esté activado de forma predeterminada como medio para desalentar el uso de métodos de escape incorrectos como addslashes().

Además, el servidor establece el modo SQL de una nueva conexiónSUPER de acuerdo con su configuración (que un usuario puede cambiar en cualquier momento); por lo tanto, para estar seguro del comportamiento del servidor, siempre debe especificar explícitamente el modo deseado después de conectarse.

La gracia salvadora

Siempre y cuando siempre configure explícitamente el modo SQL para no incluir NO_BACKSLASH_ESCAPES, o citar literales de cadena MySQL usando el carácter de comilla simple, este error no puede mostrar su fea cabeza: respectivamente escape_quotes_for_mysql()no se usará, o su suposición sobre qué caracteres de comillas requieren repetición será sea ​​correcto.

Por esta razón, recomiendo que cualquiera que use NO_BACKSLASH_ESCAPEStambién ANSI_QUOTESel modo enable, ya que forzará el uso habitual de cadenas literales entre comillas simples. Tenga en cuenta que esto no evita la inyección de SQL en caso de que se utilicen literales entre comillas dobles; simplemente reduce la probabilidad de que eso suceda (porque las consultas normales y no maliciosas fallarían).

En PDO, tanto su función equivalente PDO::quote()como su emulador de declaración preparada invocan mysql_handle_quoter(), lo que hace exactamente esto: garantiza que el literal escapado esté entre comillas simples, por lo que puede estar seguro de que PDO siempre es inmune a este error.

A partir de MySQL v5.7.6, este error se solucionó. Ver registro de cambios :

Funcionalidad agregada o cambiada

  • Cambio incompatible: se ha implementado una nueva función API de C,mysql_real_escape_string_quote(), como reemplazomysql_real_escape_string()porque esta última función puede no codificar correctamente los caracteres cuando elNO_BACKSLASH_ESCAPESmodo SQL está habilitado. En este caso,mysql_real_escape_string()no puede escapar de los caracteres de las comillas excepto duplicándolos y, para hacerlo correctamente, debe conocer más información sobre el contexto de las comillas de la que está disponible.mysql_real_escape_string_quote()toma un argumento adicional para especificar el contexto de la cita. Para obtener detalles de uso, consulte mysql_real_escape_string_quote() .

     Nota

    Las aplicaciones deben modificarse para usar mysql_real_escape_string_quote(), en lugar de mysql_real_escape_string(), que ahora falla y produce un CR_INSECURE_API_ERRerror si NO_BACKSLASH_ESCAPESestá habilitado.

    Referencias: consulte también el error n.º 19211994.

Ejemplos seguros

En conjunto con el error explicado por ircmaxell, los siguientes ejemplos son completamente seguros (suponiendo que uno esté usando MySQL posterior a 4.1.20, 5.0.22, 5.1.11; o que no esté usando una codificación de conexión 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');

...porque hemos seleccionado explícitamente un modo SQL que no incluye 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");

...porque estamos citando nuestro literal de cadena entre comillas simples.

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

...porque las declaraciones preparadas por PDO son inmunes a esta vulnerabilidad (y también a la de ircmaxell, siempre que esté usando PHP≥5.3.6 y el juego de caracteres se haya configurado correctamente en el DSN; o que la emulación de declaraciones preparadas haya sido deshabilitada) .

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

...porque quote()la función de PDO no sólo escapa del literal, sino que también lo cita (entre comillas simples '); tenga en cuenta que para evitar el error de ircmaxell en este caso, debe utilizar PHP≥5.3.6 y haber configurado correctamente el juego de caracteres en el DSN.

$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

...porque las declaraciones preparadas por MySQLi son seguras.

Terminando

Por lo tanto, si usted:

  • utilizar declaraciones preparadas nativas

O

  • utilizar MySQL v5.7.6 o posterior

O

  • Además de emplear una de las soluciones en el resumen de ircmaxell, use al menos una de:

    • DOP;
    • literales de cadena entre comillas simples; o
    • un modo SQL establecido explícitamente que no incluyeNO_BACKSLASH_ESCAPES

... entonces deberías estar completamente seguro (las vulnerabilidades fuera del alcance de la cadena se escapan a un lado).

eggyal avatar Apr 24 '2014 19:04 eggyal

Bueno, en realidad no hay nada que pueda pasar por eso, aparte del %comodín. Podría ser peligroso si estuviera usando LIKEuna declaración, ya que el atacante podría poner simplemente %un inicio de sesión si no lo filtra, y tendría que simplemente forzar la contraseña de cualquiera de sus usuarios. La gente suele sugerir el uso de declaraciones preparadas para que sea 100% seguro, ya que de esa manera los datos no pueden interferir con la consulta en sí. Pero para consultas tan simples probablemente sería más eficiente hacer algo como$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);

Slava avatar Apr 21 '2011 08:04 Slava