flawopen.com/AI Security/wazuh-mcp-server-webshell-check-ssrf-ghsa-pw2j-pj4h-f5vg
SSRF-уязвимость проверки webshell в Wazuh MCP — GHSA-pw2j-pj4h-f5vg
Имя хоста обходило проверку буквального IP-адреса в Wazuh-MCP-Server до того, как его запрашивал инструмент анализа инцидентов. GHSA указывает высокий уровень серьёзности и CWE-918; CVE не присвоен. Исправлено в коммите 2bbfe12.[1], [2], [3]
Сотрудник на стойке проверяет, нет ли пропуска посетителя в списке запрета, а затем позволяет ему выбрать кабинет. Проверка только напечатанного номера не показывает, что имя ведёт в закрытую комнату. Исправление проверяет, куда разрешается имя, и направляет соединение именно на проверенный адрес.[1], [3]
Ключевые понятия и термины
Server-side request forgery (SSRF)- Сервер отправляет сетевой запрос к адресу, на который влияет вызывающая сторона. Неполная проверка может позволить серверу обратиться к сетям, недоступным вызывающей стороне напрямую.[4], [1]
DNS-разрешение и rebinding- Имя хоста преобразуется в один или несколько IP-адресов. Если проверяется только текст или HTTP-клиент позднее разрешает имя заново, соединение может пойти на адрес, отличный от проверенного.[1], [3]
Фиксация адреса назначения- Проверенный IP связывается с исходящим соединением, а имя хоста сохраняется для проверок HTTP и TLS. Это закрывает разрыв между проверкой и соединением.[3]
Связи со стандартами
- Ссылка CWE для слабого места, описанного в этом руководстве: CWE-918
- D3-OTF Фильтрация исходящего трафика — Описанное в руководстве ограничение исходящего сетевого трафика сокращает список адресов, доступных сервису или хосту.
Пошаговый сценарий атаки
URL из криминалистических данных попадает в инструмент
Уязвимый инструмент blueteam_check_webshell принимает URL, чтобы аналитик мог проверить адрес, подозреваемый в размещении веб-шелла. В бюллетене проблема ограничена этим инструментом, а не всеми функциями Wazuh-MCP-Server.[1]
HTTP-клиент сам разрешает адрес и следует перенаправлениям
Затем проверенный URL передавался curl, который выполнял собственный DNS-запрос. В бюллетене также описано автоматическое следование перенаправлениям без повторной проверки адреса. Это создаёт риск SSRF, если сервер имеет доступ к внутренней сети, а аргумент URL контролируется недоверенными данными. Реальное воздействие зависит от сети и доступа к инструменту.[1], [3]
Исправление проверяет и фиксирует разрешённые адреса
Коммит 2bbfe12 получает записи IPv4 и IPv6, отклоняет непубличные адреса, кроме доменов из явного списка ALLOWED_INTERNAL_DOMAINS, и фиксирует выбранный адрес для запроса. Исправленный путь не следует перенаправлениям автоматически. GHSA указывает высокий уровень и CWE-918; CVE и диапазон версий пакета отсутствуют.[1], [3]
Исходный код: Уязвимый vs Защищённый вариант
Уязвимый фрагмент воспроизводит существенную логику WebshellCheckInput.validate_url из коммита 6bd1fa8; импорты и поля класса опущены. Он отклоняет непубличные IP-литералы, но трактует ValueError для имени хоста как домен и принимает его без DNS-разрешения. Исправленный фрагмент воспроизводит _pinned_curl_args из коммита 2bbfe12. Исправленный валидатор также вызывает _host_pins; цикл запросов передаёт curl аргументы --resolve и проверяет каждый адрес перенаправления до следующего перехода. Внутренние адреса разрешены только для явно настроенных доменов. Это отдельные фрагменты исходного кода, а не полная замена логики загрузки URL.[2], [3]
Фрагмент исходного кода: mcp_server/tools/webshell_check.py — проверка 6bd1fa8; исправление 2bbfe12
@field_validator("url")
@classmethod
def validate_url(cls, v: str) -> str:
v = v.strip()
if not v.startswith(("http://", "https://")):
raise ValueError(f"URL must start with http:// or https://, got: {v!r}")
# Отклонять частные/зарезервированные IP-адреса в хосте URL.
from urllib.parse import urlparse
import ipaddress
host = urlparse(v).hostname
if host:
try:
ip = ipaddress.ip_address(host)
except ValueError:
ip = None # not an IP - domain name, allowed
if ip is not None and (ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local):
raise ValueError(f"URL host is a private/reserved IP: {host}. This tool only accepts public hosts.")
return v
def _pinned_curl_args(url: str) -> tuple[list[str], str | None]:
"""Build curl --resolve args for an already-validated destination."""
import ipaddress
u = urlparse(url)
host = u.hostname
if not host:
return [], "URL has no host."
try:
ipaddress.ip_address(host)
return [], None # Literal IP: no DNS lookup to rebind.
except ValueError:
pass
port = u.port or (443 if u.scheme == "https" else 80)
ips, err = _host_pins(host, _allowed_internal_domains())
if err:
return [], err
args: list[str] = []
for ip in ips:
addr = f"[{ip}]" if ":" in ip else ip
args += ["--resolve", f"{host}:{port}:{addr}"]
return args, None
Чек-лист по защите системы для инженеров
- ✓Найдите развертывания с уязвимой реализацией
blueteam_check_webshell; бюллетень указывает коммиты, а не диапазон версий пакета.[1], [2] - ✓Обновитесь до коммита
2bbfe12или более поздней версии с исправлением и проверьте фактически развернутый код.[3] - ✓Разрешайте все записи A и AAAA и отклоняйте непубличные адреса, кроме узко заданного исключения для внутреннего домена.[3]
- ✓Связывайте проверенный IP с реальным соединением, чтобы повторный DNS-запрос не изменил назначение.[3]
- ✓Отключите автоматические перенаправления либо проверяйте и фиксируйте каждый новый адрес назначения.[1], [3]
- ✓Ограничьте доступ к инструменту и исходящий сетевой доступ сервера; выясните, могут ли недоверенные данные задавать аргумент URL.[1]
- ✓Проверяйте смешанные DNS-ответы, IPv4, IPv6, внутренние исключения, перенаправления и фиксацию адреса без обращения к реальным внутренним службам.[3]
Источники
- Уведомление безопасности Wazuh-MCP-Server GHSA-pw2j-pj4h-f5vg (без CVE)
- Уязвимый исходный код коммита 6bd1fa8 — WebshellCheckInput.validate_url
- Исправляющий коммит Wazuh-MCP-Server 2bbfe12
- MITRE CWE-918: Server-Side Request Forgery
- The Next Web: отдельные SSRF-ошибки в реализациях MCP
- Ars Technica: Vulnerability in agents from Google and others exposes structural flaw in MCP