La respuesta corta es sí, sí, hay una forma de sortear mysql_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 ciertas circunstancias, eso devolverá más de 1 fila. Analicemos 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 para codificar
'
como en ASCII, es decir,0x27
y tener algún carácter cuyo byte final sea un ASCII\
es decir,0x5c
. Resulta que MySQL 5.6 admite 5 codificaciones de este tipo de forma predeterminada:big5
,cp932
,gb2312
,gbk
ysjis
. Seleccionaremosgbk
aquí.Ahora, es muy importante tener en cuenta el uso de
SET NAMES
aquí. Esto establece el juego de caracteres EN EL SERVIDOR . Si usamos 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 usar 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 enlatin1
ygbk
,0x27
por sí solo es un'
literal personaje.Hemos elegido este payload porque, si llamamos a
addslashes()
en él, insertaríamos un ASCII\
es decir,0x5c
, antes del'
personaje. Entonces terminaríamos con0xbf5c27
, que engbk
es una secuencia de dos caracteres:0xbf5c
seguido de0x27
. O en otras palabras, un válido carácter seguido de un'
sin escape . Pero no estamos usandoaddslashes()
. Así que al siguiente paso... -
mysql_real_escape_string()
La llamada de la API de 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 cree que todavía estamos usandolatin1
por la conexión, porque nunca le dijimos lo contrario. Le dijimos al servidor 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'
colgante libre personaje en nuestro contenido "escapado"! De hecho, si tuviéramos que mirar$var
en elgbk
conjunto de caracteres, veríamos:縗' OR 1=1 /*
Que es exactamente qué requiere el ataque.
-
La consulta
Esta parte es solo una formalidad, pero aquí está la consulta procesada:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Felicitaciones, acaba de atacar con éxito un programa usando mysql_real_escape_string()
...
Lo malo
Se pone peor. PDO
el valor predeterminado es emular 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 C), lo que significa que lo siguiente dará como resultado 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 al deshabilitar las declaraciones preparadas emuladas:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Esto normalmente dar como resultado una declaración preparada verdadera (es decir, los datos que se envían en un paquete separado de la consulta). Sin embargo, tenga en cuenta que PDO silenciosamente fallback para emular declaraciones que MySQL no puede preparar de forma nativa:las que sí puede son listadas en el manual, pero tenga cuidado de 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
en mysql_real_escape_string()
significaba que los caracteres de varios bytes no válidos, como los de nuestra carga útil, se trataban como bytes individuales con fines de escape incluso si el cliente había sido informado correctamente de la codificación de la conexión y así este ataque todavía tendría éxito. El error se solucionó en MySQL 4.1.20
, 5.0.22 y 5.1.11 .
Pero lo peor es que PDO
no expuso la API de C para mysql_set_charset()
hasta 5.3.6, por lo que en versiones anteriores no ¡evite 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 a la base de datos debe codificarse utilizando un juego de caracteres vulnerable. utf8mb4
es no vulnerable y, sin embargo, puede admitir todos Carácter Unicode:por lo que podría elegir usarlo en su lugar, pero solo ha estado disponible desde MySQL 5.5.3. Una alternativa es utf8
, que tampoco es no vulnerable y puede admitir todo el Unicode Plano multilingüe básico
.
Alternativamente, puede habilitar 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 cualquiera de las codificaciones vulnerables donde no existían previamente (es decir, 0xbf27
sigue siendo 0xbf27
etc.), por lo que el servidor aún rechazará la cadena como no válida. Sin embargo, consulte 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 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 desactivado las sentencias 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 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.
Conclusión
Si tu:
- Usar versiones modernas de MySQL (últimas 5.1, todas las 5.5, 5.6, etc.) Y
mysql_set_charset()
/$mysqli->set_charset()
/ Parámetro de juego de caracteres DSN de PDO (en PHP ≥ 5.3.6)
O
- No use un juego de caracteres vulnerable para la codificación de la conexión (solo use
utf8
/latin1
/ascii
/ etc.)
Estás 100 % seguro.
De lo contrario, eres vulnerable aunque estés usando mysql_real_escape_string()
...