FlawOpen

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

● CWE-918 · Alta
Pesquisa · FlawOpen

Falha SSRF no verificador de webshell do Wazuh MCP — GHSA-pw2j-pj4h-f5vg

Um nome de host contornava a verificação de IP literal do Wazuh-MCP-Server antes de uma ferramenta forense acessá-lo. A GHSA atribui gravidade Alta e CWE-918; não há CVE. Corrigido no commit 2bbfe12.[1], [2], [3]

💡 Explicação em Linguagem Simples (ELI5)

Uma recepcionista confere se o crachá de uma pessoa não está numa lista de bloqueio e depois deixa que ela escolha o escritório. Conferir apenas o crachá impresso não revela que o nome da pessoa aponta para uma sala restrita. A correção verifica para onde o nome é resolvido e faz a conexão usar esse destino conferido.[1], [3]

Conceitos Centrais e Termos

Server-side request forgery (SSRF)
O servidor faz uma requisição de rede para um destino influenciado por quem o chama. Verificações incompletas podem permitir acesso a redes que a pessoa não alcança diretamente.[4], [1]
Resolução DNS e rebinding
Um nome de host é convertido em um ou mais endereços IP. Se a validação só verifica o texto ou o cliente HTTP resolve novamente depois, o endereço usado pode ser diferente do que foi verificado.[1], [3]
Fixação do destino
O IP validado é associado à conexão de saída, mantendo o nome de host para as verificações HTTP e TLS. Isso fecha a diferença entre validar e conectar.[3]

Mapeamentos de frameworks

Fluxo de Ataque Passo a Paso

Passo 1

Uma URL de dados forenses chega ao verificador

A ferramenta afetada blueteam_check_webshell recebe uma URL para que o analista examine um endereço suspeito de hospedar um webshell. O aviso limita a falha a essa ferramenta, não a todas as funções do Wazuh-MCP-Server.[1]

Passo 2

O validador verifica apenas IPs literais

O WebshellCheckInput.validate_url vulnerável analisa o host e chama ipaddress.ip_address. Um IP literal pode ser verificado, mas um nome DNS gera ValueError e é aceito sem consulta A ou AAAA.[2], [1]

Passo 3

O cliente HTTP resolve e pode seguir redirecionamentos

Depois, a URL validada era enviada ao curl, que fazia sua própria resolução DNS. O aviso também documenta redirecionamentos automáticos sem nova validação. Isso cria exposição a SSRF quando o servidor alcança endereços internos e a ferramenta recebe uma URL controlada por uma entrada não confiável. O impacto depende da rede e do acesso à ferramenta.[1], [3]

Passo 4

A correção valida e fixa os endereços resolvidos

O commit 2bbfe12 resolve registros IPv4 e IPv6, rejeita endereços não públicos salvo quando o domínio está em ALLOWED_INTERNAL_DOMAINS e fixa o endereço usado pela requisição. O caminho corrigido não segue redirecionamentos automaticamente. A GHSA atribui gravidade Alta e CWE-918, sem CVE ou faixa de versões de pacote.[1], [3]

Código-Fonte: Vulnerável vs. Seguro

O trecho vulnerável reproduz a lógica relevante de WebshellCheckInput.validate_url do commit 6bd1fa8; imports e campos da classe foram omitidos. Ele rejeita IPs literais não públicos, mas trata ValueError de um hostname como domínio e o aceita sem resolução DNS. O trecho corrigido reproduz _pinned_curl_args do commit 2bbfe12. O validador corrigido também chama _host_pins; o fluxo de busca passa os argumentos --resolve ao curl e valida cada destino de redirecionamento antes do próximo salto. O patch só permite destinos internos para domínios configurados explicitamente. São trechos focados, não uma substituição completa do fluxo de busca de URLs.[2], [3]

Trecho de origem: mcp_server/tools/webshell_check.py — validação 6bd1fa8; correção 2bbfe12

IMPLEMENTAÇÃO VULNERÁVEL
@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}")
    # Rejeita IPs privados/reservados no host da 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
PATCH SEGURO E ROBUSTO
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

Lista de Verificação de Segurança para Engenharia

Fontes

← AI Security Directory →