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?
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";
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í:
-
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
ysjis
. Seleccionaremosgbk
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 Cmysql_set_charset()
, estaríamos bien (en versiones de MySQL desde 2006). Pero más sobre por qué en un minuto... -
La Carga útil
La carga útil que vamos a usar para esta inyección comienza con la secuencia de bytes
0xbf27
. Engbk
, es un carácter multibyte no válido; enlatin1
, es la cadena¿'
. Tenga en cuenta que enlatin1
ygbk
,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 con0xbf5c27
, que engbk
es una secuencia de dos caracteres:0xbf5c
seguido de0x27
. O en otras palabras, un carácter válido seguido de un'
sin escapatoria. Pero no estamos usandoaddslashes()
. Así que al siguiente paso... -
Mysql_real_escape_string()
La llamada a la API C de
mysql_real_escape_string()
difiere deaddslashes()
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 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 eslatin1
.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 caracteresgbk
, veríamos:{[78]]}縗' OR 1=1 /*
Que es exactamente lo que el ataque requiere.
-
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)
De lo contrario, eres vulnerable aunque estés usando mysql_real_escape_string()
...
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 ); ySus 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:
-
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 pormysql_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. -
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. -
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éaselibmysql.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 SQLNO_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éasecharset.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ó amysql_real_escape_string()
-es como si no se hubiera producido ningún escape en absoluto. -
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
Cambio Incompatible: Una nueva función C API,
mysql_real_escape_string_quote()
, se ha implementado como un reemplazo paramysql_real_escape_string()
debido a que la última función puede fallar al codificar correctamente los caracteres cuando elNO_BACKSLASH_ESCAPES
El modo SQL está habilitado. En este caso,mysql_real_escape_string()
no puede escapar a los caracteres de 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 comillas. Para detalles de uso, vea mysql_real_escape_string_quote().Nota
Las aplicaciones deben modificarse para utilizar
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á activado.Referencias: Ver también Bug #19211994.
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).
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);
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