Inyección SQL que se mueve alrededor de mysql cadena de escape real()


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

Considere esta situación de ejemplo. SQL se construye en PHP de la siguiente manera:

$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 hackear incluso con la función mysql_real_escape_string() utilizada. Pero no puedo pensar en alguna posible explotar?

Inyecciones clásicas como esta:

aaa' OR 1=1 --

No funcionan.

¿Sabe usted de cualquier posible inyección que podría llegar a través ¿el código PHP anterior?

Author: Brad Larson, 2011-04-21

4 answers

Considere la siguiente pregunta:

$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 use comillas simples (' ') alrededor de sus variables dentro de su consulta es lo que lo protege contra esto. La siguiente es también una opción:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
 318
Author: Wesley van Opdorp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-16 12:45:41

La respuesta corta es sí, sí hay una manera de moverse mysql_real_escape_string().

Para CASOS EXTREMOS 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 ciertas circunstancias, eso devolverá más de 1 fila. Vamos a diseccionar lo que está pasando aquí:

  1. Selección de un Personaje Set

    mysql_query('SET NAMES gbk');
    

    Para que este ataque funcione, necesitamos la codificación que el servidor espera en la conexión tanto para codificar ' como en ASCII i. e.0x27 y para tener algún carácter cuyo byte final es un ASCII \ es decir, 0x5c. Resulta que hay 5 tales codificaciones soportadas en MySQL 5.6 por defecto: big5, cp932, gb2312, gbk y sjis. Seleccionaremos gbk aquí.

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

  2. La Carga útil

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

    Hemos elegido esta carga porque, si llamamos addslashes() en ella, insertaríamos un ASCII \, es decir, 0x5c, antes del carácter '. Así que terminaríamos con 0xbf5c27, que en gbk es una secuencia de dos caracteres: 0xbf5c seguido de 0x27. O en otras palabras, un carácter válido seguido de un ' sin escapatoria. Pero no estamos usando addslashes(). Así que al siguiente paso...

  3. Mysql_real_escape_string()

    La llamada a la API C de mysql_real_escape_string() difiere de addslashes() en que conoce el conjunto de caracteres de conexión. Por lo que puede realizar el escape correctamente para el conjunto de caracteres que el servidor está esperando. Sin embargo, hasta este punto, el cliente piensa que todavía estamos usando latin1 para 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 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 tuviéramos que mirar $var en el conjunto de caracteres gbk, veríamos:{[78]]}

    縗' OR 1=1 /*

    Que es exactamente lo que el ataque requiere.

  4. La Consulta

    Esta parte es solo una formalidad, pero aquí está la consulta renderizada:

    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. PDO por defecto emulando instrucciones preparadas con MySQL. Eso significa que en el lado del cliente, básicamente hace un sprintf a través de mysql_real_escape_string() (en la biblioteca de 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, vale la pena señalar que puede evitar esto deshabilitando las declaraciones preparadas emuladas:

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

Esto normalmente dará como resultado una verdadera instrucción preparada (es decir, los datos que se envían en un paquete separado de la consulta). Sin embargo, se consciente de que PDO silenciosamente recurrirá a la emulación de sentencias que MySQL no puede preparar de forma nativa: las que puede están listadas en el manual, pero tenga cuidado de seleccionar la versión del servidor apropiada).

El Feo

Dije al 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 una el error en mysql_real_escape_string() significaba que los caracteres multibyte no válidos, como los de nuestra carga útil, se trataban como bytes individuales para fines de escape incluso si el cliente había sido informado correctamente de la codificación de conexión y, por lo tanto, este ataque seguiría teniendo éxito. El error fue corregido en MySQL 4.1.20, 5.0.22 y 5.1.11.

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

La Gracia Salvadora

Como dijimos al principio, para que este ataque funcione, la conexión a la base de datos debe codificarse utilizando un conjunto de caracteres vulnerable. utf8mb4 es no vulnerable y, sin embargo, puede soportar cada carácter Unicode: por lo que podría elegir usar eso en su lugar, pero solo ha estado disponible desde MySQL 5.5.3. Alternativa es utf8, que también es no vulnerable y puede soportar la totalidad del Plano Multilingüe Básico Unicode .

Alternativamente, puede habilitar el NO_BACKSLASH_ESCAPES Modo SQL, que (entre otras cosas) altera el funcionamiento de mysql_real_escape_string(). Con este modo habilitado, 0x27 se reemplazará por 0x2727 en lugar de 0x5c27 y, 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, 0xbf27 sigue siendo 0xbf27 etc.)- por lo que el servidor rechazará la cadena como no válida. Sin embargo, ver @eggyal's answer para 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 conjunto 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 apagado emuló declaraciones preparadas.

$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 establecido el conjunto 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 verdaderas declaraciones preparadas todo el tiempo.

Terminando

Si usted:

  • Use Versiones modernas de MySQL (finales de 5.1, todas 5.5, 5.6, etc.) Y mysql_set_charset() / $mysqli->set_charset() / Parámetro de conjunto de caracteres DSN de PDO (en PHP ≥ 5.3.6)

O

  • No utilice un conjunto de caracteres vulnerable para la codificación de la conexión (solo se utiliza utf8 / latin1 / ascii / etc)
Estás 100% a salvo.

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

 548
Author: ircmaxell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:55:01

TL; DR

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

  • MySQL NO_BACKSLASH_ESCAPES El modo SQL está habilitado (que podría ser , a menos que explícitamente seleccione otro modo SQL cada vez que se conecte ); y

  • Sus literales de cadena SQL se citan usando caracteres de comilla doble ".

Esto fue archivado como bug #72458y se ha corregido en MySQL v5.7.6 (ver la sección titulada " La Gracia salvadora", más abajo).

Este es otro, (¿quizás menos?) caso BORDE oscuro!!!

En homenaje a La excelente respuesta de@ircmaxell (en realidad, se supone que esto es adulación y no plagio!), adoptaré su formato:

El Ataque

Comenzando 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 test tabla. 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 maneras de incluir caracteres de 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 """".

    • Precede a la cita carácter por un carácter de escape("\").

    • Un " '" dentro de una cadena citada con """ no necesita un tratamiento especial y no necesita duplicarse o escaparse. De la misma manera, """ dentro de una cadena citada con "'" no necesita 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: una de las dos primeras opciones debe ser usado en su lugar. Tenga en cuenta que el efecto de la cuarta viñeta es que uno debe conocer necesariamente el carácter que se utilizará para citar el literal con el fin de evitar masticar los datos de uno.

  2. La Carga útil

    " OR 1=1 -- 
    

    La carga útil inicia esta inyección literalmente con el carácter ". Ninguna codificación en particular. Sin caracteres especiales. No hay bytes extraños.

  3. Mysql_real_escape_string()

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

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

    Por lo tanto, una función subyacente diferente, escape_quotes_for_mysql(), se invoca si el modo SQL NO_BACKSLASH_ESCAPES está en uso. Como se mencionó anteriormente, tal función necesita saber qué carácter se utilizará para citar el literal con el fin de repetirlo sin causar que el otro carácter de la cita se repita literalmente.

    Sin embargo, esta función arbitrariamente asume que la cadena será citado usando el carácter de comilla simple '. Véase 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 caracteres de comillas dobles " intactos (y duplica todos los caracteres de comillas simples ') independientemente del carácter real que se use para citar el literal! En nuestro caso $var sigue siendo exactamente el mismo que el argumento que se proporcionó a mysql_real_escape_string()-es como si no se hubiera producido ningún escape en absoluto.

  4. Las Query

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

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

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

Como dijo mi sabio amigo: felicitaciones, acabas de atacar con éxito un programa usando mysql_real_escape_string()...

El Malo

mysql_set_charset() no puede ayudar, ya que esto no tiene nada que ver con los conjuntos de caracteres; mysqli::real_escape_string(), ya que es sólo una envoltura diferente alrededor de esta misma función.

El problema, si no es ya obvio, es que la llamada a mysql_real_escape_string() no se puede saber con qué carácter se citará el literal, ya que eso queda en manos del desarrollador para decidir en un momento posterior. Por lo tanto, en el modo NO_BACKSLASH_ESCAPES, existe literalmente ninguna manera de que esta función pueda escapar de forma segura de cada entrada para usarla con citas arbitrarias (al menos, no sin doblar caracteres que no requieren doblar y por lo tanto munging sus datos).

El Feo

Se pone peor. NO_BACKSLASH_ESCAPES no puede ser tan poco común en la naturaleza debido a la necesidad de su uso para la compatibilidad con SQL estándar (por ejemplo, véase la sección 5.3 de la especificación SQL-92, es decir, la producción de gramática <quote symbol> ::= <quote><quote> y la falta de un significado especial dado a la barra invertida). Además, su uso fue explícitamente recomendado como una solución para el (desde hace mucho tiempo arreglado) bug que describe el post de ircmaxell. Quién sabe, algunos DBA podrían incluso configurarlo para que esté activado por defecto como medio para desalentar el uso de métodos de escape incorrectos como addslashes().

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

La Gracia Salvadora

Siempre y cuando explícitamente establezca el modo SQL para no incluir NO_BACKSLASH_ESCAPES, o citar literales de cadena MySQL utilizando la comilla simple carácter, este error no puede levantar su fea cabeza: respectivamente escape_quotes_for_mysql() no se utilizará, o su suposición sobre qué caracteres de comillas requieren repetición será correcta.

Por esta razón, recomiendo que cualquiera que use NO_BACKSLASH_ESCAPES también habilite ANSI_QUOTES modo, ya que forzará el uso habitual de literales de cadena entre comillas simples. Tenga en cuenta que esto no impide la inyección SQL en el caso de que se utilicen literales entre comillas dobles, simplemente reduce la probabilidad de que eso suceda (porque las consultas normales no maliciosas fallarían).

En DOP, tanto su función equivalentePDO::quote() y su declaración preparada emulador llamado a mysql_handle_quoter()-lo que hace exactamente esto: asegura que el literal escapado se cite 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 ha sido corregido. Véase change log :

Funcionalidad añadida o cambiada

Seguro Ejemplos

Junto con el error explicado por ircmaxell, los siguientes ejemplos son completamente seguros (suponiendo que uno está usando MySQL después de 4.1.20, 5.0.22, 5.1.11; o que uno 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 con comillas simples.

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

...porque las declaraciones preparadas por la DOP son inmunes a partir de esta vulnerabilidad (y de ircmaxell también, siempre que esté usando PHP≥5.3.6 y el conjunto de caracteres se haya establecido correctamente en el DSN; o que la emulación de la instrucción preparada se haya deshabilitado).

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

...debido a que la función quote() de PDO no solo escapa al literal, sino que también lo cita (en caracteres de comilla simple '); tenga en cuenta que para evitar el error de ircmaxell en este caso, debe usar PHP≥5.3.6 y haber configurado correctamente el conjunto 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

Así, si:{[75]]}

  • use declaraciones preparadas nativas

O

  • use MySQL v5. 7. 6 o posterior

O

  • En adición para 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 explícitamente establecido que no incluye NO_BACKSLASH_ESCAPES

...entonces debería estar completamente seguro (dejando de lado las vulnerabilidades fuera del alcance del escape de cadenas).

 142
Author: eggyal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:45

Bueno, no hay nada realmente que pueda pasar a través de eso, aparte de % comodín. Podría ser peligroso si estuviera utilizando la instrucción LIKE ya que el atacante podría poner solo % como inicio de sesión si no filtra eso, y tendría que forzar brutalmente una contraseña de cualquiera de sus usuarios. Las personas a menudo sugieren usar declaraciones preparadas para que sea 100% seguro, ya que los datos no pueden interferir con la consulta en sí de esa manera. Pero para consultas tan simples, probablemente sería más eficiente hacer algo como $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);

 19
Author: Slava,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-21 08:15:22