php - reales - Inyección SQL que se mueve alrededor de mysql_real_escape_string()
proteger php contra sql injection (4)
TL; DR
mysql_real_escape_string()
no proporcionará protección alguna (y además podría munge sus datos) si:
El modo SQL
NO_BACKSLASH_ESCAPES
de MySQL está habilitado (que podría estarlo, a menos que seleccione explícitamente otro modo SQL cada vez que se conecte ); ysus literales de cadena SQL se citan utilizando caracteres de
"
comillas dobles"
.Esto se archivó como error # 72458 y se ha corregido en MySQL v5.7.6 (consulte la sección titulada " La gracia de ahorro ", a continuación).
Esta es otra, (¿quizás menos?) Oscura EDGE CASE !!!
En homenaje a share (en realidad, ¡se supone que esto es un obsequio y no un 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 tabla de test
. Una disección:
Seleccionando un modo SQL
mysql_query(''SET SQL_MODE="NO_BACKSLASH_ESCAPES"'');
Como se documenta en Literales de cuerdas :
Hay varias formas de incluir caracteres de comillas dentro de una cadena:
Una “
''
” dentro de una cadena citada con “''
” puede escribirse como “''''
”.Un "
"
"dentro de una cadena citada con""
" se puede escribir como """
".Precede el carácter de cita por un carácter de escape ("
/
").Un "" dentro de una cadena citada con "
"
"no necesita un tratamiento especial y no necesita ser duplicado o escapado. De la misma manera,""
" dentro de una cadena citada con "''
" no necesita un 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: se debe usar una de las dos primeras opciones. 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 que se mezclen sus datos.La carga útil
" OR 1=1 --
La carga útil inicia esta inyección literalmente con el carácter
"
. 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); }
Por lo tanto, se invoca una función subyacente diferente,
escape_quotes_for_mysql()
, si el modoNO_BACKSLASH_ESCAPES
SQL está en uso. Como se mencionó anteriormente, tal función necesita saber qué carácter se utilizará para citar el literal para repetirlo sin hacer que el otro carácter de cita se repita literalmente.Sin embargo, esta función asume arbitrariamente que la cadena será citada usando 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 los caracteres de comillas dobles
"
intactos (y duplica todos''
caracteres de comillas simples) independientemente del carácter real que se usa 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.La consulta
mysql_query(''SELECT * FROM test WHERE name = "''.$var.''" LIMIT 1'');
Algo de una formalidad, la consulta renderizada es:
SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
Como lo expresó mi amigo aprendido: felicidades, 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()
tampoco puede mysqli::real_escape_string()
, ya que es solo una envoltura diferente de esta misma función.
El problema, si no es ya obvio, es que la llamada a mysql_real_escape_string()
no puede saber con qué carácter se citará el literal, ya que el desarrollador puede decidirlo más adelante. Por lo tanto, en el modo NO_BACKSLASH_ESCAPES
, no hay forma literal de que esta función pueda escapar de forma segura a cada entrada para usarla con citas arbitrarias (al menos, no sin duplicar los caracteres que no requieren duplicar y, por lo tanto, silenciar sus datos).
El feo
Se pone peor. NO_BACKSLASH_ESCAPES
posible que NO_BACKSLASH_ESCAPES
no sea tan infrecuente debido a la necesidad de su uso para la compatibilidad con SQL estándar (p. Ej., Consulte la sección 5.3 de la especificación SQL-92 , a saber, la gramática <quote symbol> ::= <quote><quote>
producción y falta de algún significado especial dado a la barra invertida). Además, su uso se 5.1.11 explícitamente 5.1.11 al bug (desde hace tiempo reparado) que describe la publicación de ircmaxell. Quién sabe, algunos DBA podrían incluso configurarlo para que esté addslashes()
de forma predeterminada como un medio para desalentar el uso de métodos de escape incorrectos, como las addslashes()
.
Además, el servidor configura el modo SQL de una nueva conexión 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, siempre debe especificar explícitamente el modo deseado después de conectarse.
La gracia salvadora
Siempre y cuando siempre establezcas explícitamente el modo SQL para no incluir NO_BACKSLASH_ESCAPES
, o NO_BACKSLASH_ESCAPES
literales de cadena MySQL usando el carácter de comilla simple, este error no puede escape_quotes_for_mysql()
su fea cabeza: respectivamente no se utilizará escape_quotes_for_mysql()
, o su suposición sobre qué comilla Los caracteres requieren repetición serán correctos.
Por esta razón, recomiendo que cualquiera que use NO_BACKSLASH_ESCAPES
también ANSI_QUOTES
modo ANSI_QUOTES
, ya que forzará el uso habitual de literales de cadena con comillas simples. Tenga en cuenta que esto no impide la inyección de SQL en el caso de que se utilicen literales entre comillas dobles, simplemente reduce la probabilidad de que esto suceda (porque las consultas normales y no malintencionadas fallarían).
En PDO, tanto su función equivalente PDO::quote()
como su emulador de declaración preparada llaman a mysql_handle_quoter()
que hace exactamente esto: asegura que el literal escapado se cita entre comillas simples, por lo que puede estar seguro de que la DOP es Siempre inmune a este error.
A partir de MySQL v5.7.6, este error ha sido corregido. Ver registro de cambios :
Funcionalidad agregada o cambiada
Cambio incompatible: una nueva función de la API de C,
mysql_real_escape_string_quote()
, se ha implementado como un reemplazo paramysql_real_escape_string()
porque la última función no puede codificar correctamente los caracteres cuando el modo SQLNO_BACKSLASH_ESCAPES
está habilitado. En este caso,mysql_real_escape_string()
no puede escapar de los caracteres de comillas excepto duplicándolos, y para hacer esto correctamente, debe conocer más información sobre el contexto de cotización de la que está disponible.mysql_real_escape_string_quote()
toma un argumento adicional para especificar el contexto de la cita. Para detalles de uso, vea 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 un errorNO_BACKSLASH_ESCAPES
siNO_BACKSLASH_ESCAPES
está habilitado.Referencias: Ver también Bug # 19211994.
Ejemplos seguros
Tomados junto con el error explicado por ircmaxell, los siguientes ejemplos son completamente seguros (asumiendo que uno usa MySQL más adelante que 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 nuestra cadena literal con comillas simples.
$stmt = $pdo->prepare(''SELECT * FROM test WHERE name = ? LIMIT 1'');
$stmt->execute(["'' OR 1=1 /*"]);
... porque las declaraciones preparadas de PDO son inmunes a esta vulnerabilidad (y también a las de ircmaxell, 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 declaración preparada haya sido desactivada) .
$var = $pdo->quote("'' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");
... porque la función quote()
PDO no solo escapa al literal, sino que también la cita (en comillas simples ''
caracteres); 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
Por lo tanto, si usted:
- utilizar declaraciones preparadas nativas
O
- usa 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 una sola cita; o
- un modo SQL establecido explícitamente que no incluye
NO_BACKSLASH_ESCAPES
... entonces debería estar completamente seguro (vulnerabilidades fuera del alcance de la cadena que se escapa a un lado).
¿Existe una posibilidad de inyección SQL incluso cuando se utiliza la función mysql_real_escape_string()
?
Considere esta situación de muestra. SQL se construye en PHP de esta 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 muchas personas que me dicen que un código como ese todavía es peligroso y es posible hackear incluso con la función mysql_real_escape_string()
utilizada. ¿Pero no puedo pensar en ninguna posible hazaña?
Inyecciones clásicas como esta:
aaa'' OR 1=1 --
No funcionan.
¿Sabe de alguna posible inyección que pueda obtenerse a través del código PHP anterior?
Bueno, no hay nada realmente que pueda pasar por eso, aparte de %
wildcard. Podría ser peligroso si utilizara la instrucción LIKE
, ya que el atacante podría poner solo %
como inicio de sesión si no lo filtra, y tendría que simplemente aplicar una contraseña a cualquiera de sus usuarios. Las personas a menudo sugieren usar declaraciones preparadas para que sean 100% seguras, ya que los datos no pueden interferir con la consulta 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);
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 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";
La respuesta corta es sí, sí, hay una manera de evitar mysql_real_escape_string()
.
Para los casos muy OBSCURE EDGE !!!
La respuesta larga no es tan fácil. Se basa en un ataque demostrado aquí .
El ataque
Entonces, vamos a empezar 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 analizar lo que está pasando aquí:
Selección de un conjunto de caracteres
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, es decir,
0x27
y tener algún carácter cuyo byte final sea un ASCII, es decir,0x5c
. Como resultado, hay 5 codificaciones de este tipo admitidas en MySQL 5.6 por defecto:big5
,cp932
,gb2312
,gbk
ysjis
. Seleccionaremosgbk
aquí.Ahora, es muy importante tener en cuenta el uso de
SET NAMES
aquí. Esto establece el conjunto de caracteres en el servidor . Simysql_set_charset()
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 vamos a utilizar para esta inyección comienza con la secuencia de bytes
0xbf27
. Engbk
, ese es un carácter multibyte no válido; enlatin1
, es la cadena¿''
. Tenga en cuenta que engbk
ygbk
,0x27
en sí mismo es un carácter literal''
.Hemos elegido esta carga útil porque, si
addslashes()
en ella, insertaríamos un ASCII/
ie0x5c
, antes del carácter''
. Así que terminamos 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 escape''
. Pero no estamos utilizandoaddslashes()
. Así que en el siguiente paso ...mysql_real_escape_string ()
La llamada de la API C a
mysql_real_escape_string()
difiere deaddslashes()
en que conoce el conjunto 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 estamos usandolatin1
para la conexión, porque nunca le dijimos lo contrario. Le dijimos al servidor que estamos usandogbk
, pero el cliente aún piensa que eslatin1
.Por lo tanto, la llamada a
mysql_real_escape_string()
inserta la barra invertida, ¡y tenemos un carácter colgado en nuestro contenido "escapado"! De hecho, sigbk
a$var
en elgbk
caracteresgbk
, veríamos:縗'' OR 1=1 /*
Que es exactamente lo que requiere el ataque.
La consulta
Esta parte es solo una formalidad, pero aquí está la consulta renderizada:
SELECT * FROM test WHERE name = ''縗'' OR 1=1 /*'' LIMIT 1
Enhorabuena, acaba de atacar con éxito un programa utilizando mysql_real_escape_string()
...
El malo
Se pone peor. PDO
predeterminada, la PDO
emula declaraciones 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);
Por lo general, esto resultará en una verdadera declaración preparada (es decir, los datos se envían en un paquete separado de la consulta). Sin embargo, tenga en cuenta que la DOP fallback silenciosamente para emular las declaraciones que MySQL no puede preparar de forma nativa: las que pueden listed en el manual, pero tenga cuidado de seleccionar la versión de servidor apropiada).
El feo
Dije desde el principio que podríamos haber evitado todo esto si hubiéramos usado mysql_set_charset(''gbk'')
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, un bug en mysql_real_escape_string()
significaba que los caracteres multibyte no válidos, como los de nuestra carga útil, se trataron como bytes individuales para fines de escape, incluso si el cliente había sido informado correctamente de la codificación de la conexión y Este ataque todavía tendría éxito. El error se corrigió 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 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 dijimos al principio, para que este ataque funcione, la conexión de la base de datos debe codificarse utilizando un conjunto de caracteres vulnerables. utf8mb4
no es vulnerable y, sin embargo, puede admitir todos los caracteres de Unicode: así que puede elegir usar eso en su lugar, pero solo está disponible desde MySQL 5.5.3. Una alternativa es utf8
, que tampoco es vulnerable y puede admitir todo el plano utf8
básico de Unicode.
Alternativamente, puede habilitar el modo SQL NO_BACKSLASH_ESCAPES
, que (entre otras cosas) altera el funcionamiento de mysql_real_escape_string()
. Con este modo habilitado, 0x27
se reemplazará con 0x2727
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 seguirá rechazar la cadena como inválida. Sin embargo, vea la respuesta de @ eggyal para una vulnerabilidad diferente que puede surgir del uso de 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 espera 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 establecido 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 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 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 tu:
- Utilice las versiones modernas de MySQL (finales de 5.1, todas las 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 vulnerables para la codificación de la conexión (solo use
utf8
/utf8
/ascii
/ etc)
Estás 100% seguro.
De lo contrario, eres vulnerable aunque estés utilizando mysql_real_escape_string()
...