28 abril 2010

MySQL, tabuladores verticales y otros códigos de escape

La capacidad y fexibilidad de MySQL siguen sorprendiédome cada día: en esta entrada explicaré un vector de ataque, que nos podrá ser útil cuando utilicemos técnicas de inyección SQL avanzadas contra dicho motor, en entornos donde la entrada de datos sea filtrada.

Se han escrito muchos artículos, papers, y cheat-sheets sobre inyección SQL para MySQL, y parece que todo esté descubierto o inventado, pero, tras leer esta gran entrada del blog de Reiner, reparé en que no parece estar documentado en ningún sitio lo que aquí voy a contar. Seguro que mucha gente conoce esta peculiaridad (me consta), pero nadie (que yo haya podido encontrar) la ha descrito.

Para ilustrarlo, utilizaré este pequeño código PHP: “noticia.php
<?php

// ENTRADA
$id = $_GET [ "id" ];

// FILTROS
// 1. espacio, tab, etc.
if (preg_match( '/\s/' , $id ))
exit ( "chico malo!" ); 
// 2. comentario SQL de carácter único: #
if (preg_match( '/#/' , $id ))
exit ( "chico malo!" );

// CONSULTA INSEGURA
$sql = "SELECT * FROM noticias " .
"WHERE id=" . $id . " " .
"AND fhpublicacion<=NOW() " .
"LIMIT 1" ;

// SALIDA de RESULTADOS
$set = mysql_query( $sql );
...

?> 

El filtrado es “poco serio” pero suficiente para la demostración. Este filtrado descarta dos posibles vectores de ataque de la entrada, por un lado, los caracteres blancos (\s), y por otro, el carácter de “comentario SQL” propio de MySQL (#).

Aún así, en estas condiciones la inyección SQL es viable, y, si quisiéramos expoliar la base de datos y ver una noticia almacenada, por ejemplo la 45, con una fecha de publicación futura, podríamos utilizar el siguiente ataque:

/noticia.php?id=45-- 
Básicamente estamos poniendo el id que nos interesa y anulando las cláusulas adicionales del WHERE mediante un comentario. Si ahora no queremos depender de un id concreto para obtener la siguiente noticia no publicada, tendremos que elaborar una inyección más complicada debido a los filtros. Podríamos utilizar la técnica de los paréntesis explicada en el blog inicial:

/noticia.php?id=(-1)or(fhpublicacion>now())--
Pero también podríamos usar la técnica que quiero explicar, mucho más sencilla:
El patrón ('/\s/') de la expresión regular de PHP filtra, además del espacio en blanco, también el tabulador (\t), el retorno de carro (\r), el avance de línea (\n) y el salto de página (\f). Sin embargo, NO FILTRA otros caracteres:
  • códigos de %01 a %08
  • códigos de %0e a %19
  • y el más curioso: tabulador vertical (\v), código %0b

Pues bien, nuestro querido MySQL interpreta estos caracteres como separadores, es decir, como sustitutos válidos del espacio en blanco. Esto nos permite escribir una sentencia mucho más directa:

/noticia.php?id=-1%0bor%0bfhpublicacion>now()--/noticia.php?id=-1%01or%01fhpublicacion>now()--/noticia.php?id=-1%08or%08fhpublicacion>now()-- ...
e incluso poder jugar con LIMIT para recorrer todos los registros:
/noticia.php?id=-1%0bor%0bfhpublicacion>now()%0blimit%0b2,1--  
Mediante esta sencilla técnica es mucho más fácil explotar la vulnerabilidad de este código, sin necesidad de utilizar la, no siempre viable, técnica de los paréntesis (como ocurre con la cláusula LIMIT). Por supuesto también puede resultar muy útil para evadir IDSs/IPSs y sistemas similares.

Para finalizar, indicar que el alcance de este comportamiento es muy amplio, ya que esta técnica ha sido probada con MySQL 5.x y PHP 5.x, tanto con la extensión tradicional de mysql-php como con la nueva, en entorno Linux y Windows indistintamente. Amplio margen de maniobra, ¿verdad?

happy hacking! ;)

-Miguel Gesteiro (@mgesteiro)

16 comments :

palako dijo...

Y no te olvides del siempre versatil /**/:

id=-1/**/or/**/select/**/......

5qL1 dijo...

palako se me ha adelantado con el sustituto del espacio, pero /* también es un sustituto en MySQL para el %23 (#) o -- (comentarios), la diferencia está en comentarios sobre una línea (# y --) o comentararios para párrafo, texto, etc (como sucede en c, java, etc)

Por lo tanto, en principio filtrar sólo estos caracteres parece bastante insuficiente, incluso un filtrado de comas, es decir, filtar "," tampoco es una solución válida, pues BSQLi es posible sin este carácter. Pistas:

Fuente: http://dev.mysql.com/doc/refman/5.1/en/string-functions.html#function_substring

#

SUBSTR(str,pos), SUBSTR(str FROM pos), SUBSTR(str,pos,len), SUBSTR(str FROM pos FOR len)

SUBSTR() is a synonym for SUBSTRING().
#

SUBSTRING(str,pos), SUBSTRING(str FROM pos), SUBSTRING(str,pos,len), SUBSTRING(str FROM pos FOR len)

Yo dijo...

Y la forma de protegernos de esa técnica es.... ¿?

Creo que el artículo está chulo pero altratarse de un blog de seguridad, y no de hacking, creo que se debería incluir la solución al problema, pero sólo es una opinión.

mgesteiro dijo...

@palako efectivamente /**/ es un gran aliado. Sin embargo, si añadiéramos el (*) o la (/) al filtro, el (/v) seguiría siendo válido. La idea era usar algo "nuevo", lo "viejo" ya está descrito en 50.000 sitios :)

@5qL1 he leido (no sé en donde exactamente...) que la gente de MySQL va a cerrar esa vía, obligando a que el comentario de apertura y cierre esté cerrando correctamente para funcionar... de todas maneras, si puedes escribir un /* no cuesta nada escribirlo completo /**/ y te evitas problemas.

Anónimo dijo...

Una forma sencilla de validar y no complicarse la vida cuando recogemos valores numericos es usar la funcion is_numeric, lo suyo es usar phpids (http://php-ids.org/) y tenerlo actualizado.

Anónimo dijo...

Migueliño, no sabía que escribías en SbD :-) Le das a todo, hasta al twitter je je
Buen artículo

kitai dijo...

La cantidad de medidas de seguridad cada día a instalar en un sistema nuevo cada día se vuelve más y más grande.

A veces me dan ganas de volver a HTML puro y duro, y un grupo de chinos modificándolos a mano todo el rato.

Kitai

5qL1 dijo...

@Yo:

Tienes varias posibilidades.

a.- Si es un valor numérico, entero, float, etc.

1.- Lo mejor es un forzado:

Ex:
$var= (int) $_GET["var"];
$var= (float) $_GET["var"];

2.- Emplear funciones especificas como is_numeric, is_integer, is_float, etc. Tal y como dice anónimo1

b.- Si la variable es alfanumérica y teniendo en cuenta que usemos ' o " en nuestra query SQL, en mi opinión la mejor opción es mysql_real_escape_string(), o en su defecto, si la versión de PHP que empleemos no la implementa usar addslashes().

Todo esto como soluciones sin necesidad de implementar filtros propios.

Anónimo dijo...

Que buena!

Yo (otra vez) dijo...

Gracias @5qL1!

Jordi Prats dijo...

@5qL1: cuidadin con addslashes de PHP que hace no tantas versiones no era binary safe

5qL1 dijo...

@Jordi:

Cierto, addslashes no es demasiado segura para este fin que digamos, por así decirlo, fue una solución general (para multiples fines) de PHP sin tan siquiera pararse a pensar en la propia DB, o mejor dicho, en cómo securizar la DB. Es una solución débil, de ahí la implementación de una función más específica como mysql_real_escape_string.

Además cabe mencionar que no escapa ciertos caracteres susceptibles de emplearse en inyecciones.

Addslashes: ', ", \ y \x00.

mysql_real_escape_string: \x00, \n, \r, \, ', " y \x1a

Hablé sobre esta función pero tenía que haber aclarado un poco más el asunto.Jordi, gracias por sacar el tema xD

Para @Yo y cualquier interesado, cabe decir, que se puede emplear mysql_real_escape_string para PHP >= 4.3.0

5qL1 dijo...

@mgesteiro dijo:

"...si puedes escribir un /* no cuesta nada escribirlo completo /**/ y te evitas problemas."

Yo me refería al tema de comentarios, puesto que si empleo /**/ para el escape de la query final, no me sirve de mucho.

Sirva como ejemplo de lo que trato de decir, si la query es:

$query="SELECT * FROM user WHERE id='".$_GET["id"]."' AND activo=1";

Si quiero eliminar la parte " AND activo=1", no me sirve de mucho emplear /**/, pero sí /*, al igual que %23 o --.

De todas formas es interesante eso que comentas, de forzar el cierre de este tipo de comentarios.

Reiners dijo...

maybe I missed something while translating the document from spanish to german, but the following characters listed in my article are the only one which work for me on MySQL:

http://websec.wordpress.com/2007/11/11/mysql-syntax/

Characters like %01, %02, %19 gives me a syntax error.

btw: mysql does not accept "--" as comment when not followed by a whitespace and another character.

CHeers,
Reiners

mgesteiro dijo...

@5sL1 tienes razón. Mi respuesta fué poco clara y mezclé el uso de los comentarios como "separadores" y como "cierres". Puedes ver en otra entrada de Reiners justamente lo que explicaba de los comentarios incompletos: al final de esta entrada

@Reiners I have wrote an email to you :)

Anónimo dijo...

Hace unos días me tope con sitio que bloquea la palabra SELECT sin importar si era una palabra como SELECTo, SELECTion, etc... da igual si es mayúsculas o minúsculas.

Aparentemente alto total a las inyecciones SQL.

Saludos