Inyección SQL que evita mysql_real_escape_string()
¿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?
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í:
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 decir0x27
, y que tenga algún carácter cuyo byte final sea ASCII, es\
decir0x5c
. Resulta que MySQL 5.6 admite cinco codificaciones de este tipo de forma predeterminada :big5
,cp932
, y . Seleccionaremos aquí.gb2312
gbk
sjis
gbk
Ahora bien, es muy importante tener en cuenta el uso de
SET NAMES
aquí. Esto establece el conjunto de caracteres EN EL SERVIDOR . Si usáramos la llamada a la función API de Cmysql_set_charset()
, estaríamos bien (en las versiones de MySQL desde 2006). Pero más sobre por qué en un minuto...La carga útil
La carga útil que usaremos para esta inyección comienza con la secuencia de bytes
0xbf27
. Engbk
, ese es un carácter multibyte no válido; enlatin1
, es la cuerda¿'
. Tenga en cuenta que enlatin1
ygbk
,0x27
por sí solo es un'
carácter literal.Hemos elegido esta carga útil porque, si
addslashes()
la invocamos, insertaríamos un ASCII ,\
es decir0x5c
, antes del'
carácter. Entonces terminaríamos con0xbf5c27
, quegbk
es una secuencia de dos caracteres:0xbf5c
seguido de0x27
. O en otras palabras, un carácter válido seguido de un archivo sin escape'
. Pero no lo estamos usandoaddslashes()
. Así que vamos al siguiente paso...mysql_real_escape_string()
La llamada a la API de C se
mysql_real_escape_string()
diferenciaaddslashes()
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 usandolatin1
para la conexión, porque nunca le dijimos lo contrario. Le dijimos al servidor que estamos usandogbk
, 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$var
elgbk
conjunto de caracteres, veríamos:縗' OR 1=1 /*
Que es exactamente lo que requiere el ataque.
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. PDO
El 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 PDO
no 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 utf8mb4
es 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_ESCAPES
modo SQL, que (entre otras cosas) altera el funcionamiento de mysql_real_escape_string()
. Con este modo habilitado, 0x27
será reemplazado 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
todaví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()
...
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";
TL;DR
mysql_real_escape_string()
no proporcionará protección alguna (y además podría manipular sus datos) si:
NO_BACKSLASH_ESCAPES
El 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 ); ylos 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 test
tabla. Una disección:
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 pormysql_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.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.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. Verlibmysql.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, siNO_BACKSLASH_ESCAPES
se 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. Vercharset.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$var
sigue siendo exactamente el mismo que el argumento que se le proporcionómysql_real_escape_string()
: es como si no hubiera habido escape alguno .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_ESCAPES
modo, 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_ESCAPES
Puede 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_ESCAPES
también ANSI_QUOTES
el 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_ESCAPES
modo 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 demysql_real_escape_string()
, que ahora falla y produce unCR_INSECURE_API_ERR
error siNO_BACKSLASH_ESCAPES
está 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 incluye
NO_BACKSLASH_ESCAPES
... entonces deberías estar completamente seguro (las vulnerabilidades fuera del alcance de la cadena se escapan a un lado).
Bueno, en realidad no hay nada que pueda pasar por eso, aparte del %
comodín. Podría ser peligroso si estuviera usando LIKE
una 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);