FlawOpen

flawopen.com/AI Security/wazuh-mcp-server-webshell-check-ssrf-ghsa-pw2j-pj4h-f5vg

● CWE-918 · Высокий
Исследования · FlawOpen

SSRF-уязвимость проверки webshell в Wazuh MCP — GHSA-pw2j-pj4h-f5vg

Имя хоста обходило проверку буквального IP-адреса в Wazuh-MCP-Server до того, как его запрашивал инструмент анализа инцидентов. GHSA указывает высокий уровень серьёзности и CWE-918; CVE не присвоен. Исправлено в коммите 2bbfe12.[1], [2], [3]

💡 Простыми словами (ELI5)

Сотрудник на стойке проверяет, нет ли пропуска посетителя в списке запрета, а затем позволяет ему выбрать кабинет. Проверка только напечатанного номера не показывает, что имя ведёт в закрытую комнату. Исправление проверяет, куда разрешается имя, и направляет соединение именно на проверенный адрес.[1], [3]

Ключевые понятия и термины

Server-side request forgery (SSRF)
Сервер отправляет сетевой запрос к адресу, на который влияет вызывающая сторона. Неполная проверка может позволить серверу обратиться к сетям, недоступным вызывающей стороне напрямую.[4], [1]
DNS-разрешение и rebinding
Имя хоста преобразуется в один или несколько IP-адресов. Если проверяется только текст или HTTP-клиент позднее разрешает имя заново, соединение может пойти на адрес, отличный от проверенного.[1], [3]
Фиксация адреса назначения
Проверенный IP связывается с исходящим соединением, а имя хоста сохраняется для проверок HTTP и TLS. Это закрывает разрыв между проверкой и соединением.[3]

Связи со стандартами

  • Ссылка CWE для слабого места, описанного в этом руководстве: CWE-918
  • D3-OTF Фильтрация исходящего трафика — Описанное в руководстве ограничение исходящего сетевого трафика сокращает список адресов, доступных сервису или хосту.

Пошаговый сценарий атаки

Шаг 1

URL из криминалистических данных попадает в инструмент

Уязвимый инструмент blueteam_check_webshell принимает URL, чтобы аналитик мог проверить адрес, подозреваемый в размещении веб-шелла. В бюллетене проблема ограничена этим инструментом, а не всеми функциями Wazuh-MCP-Server.[1]

Шаг 2

Валидатор проверяет только IP-литералы

Уязвимый WebshellCheckInput.validate_url извлекает имя хоста и передаёт его в ipaddress.ip_address. Буквальный IP можно проверить, но DNS-имя вызывает ValueError и принимается без запроса записей A или AAAA.[2], [1]

Шаг 3

HTTP-клиент сам разрешает адрес и следует перенаправлениям

Затем проверенный URL передавался curl, который выполнял собственный DNS-запрос. В бюллетене также описано автоматическое следование перенаправлениям без повторной проверки адреса. Это создаёт риск SSRF, если сервер имеет доступ к внутренней сети, а аргумент URL контролируется недоверенными данными. Реальное воздействие зависит от сети и доступа к инструменту.[1], [3]

Шаг 4

Исправление проверяет и фиксирует разрешённые адреса

Коммит 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

Чек-лист по защите системы для инженеров

Источники

← AI Security Directory →