FlawOpen

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

● CWE-918 · Alta
Investigación · FlawOpen

Fallo SSRF en el comprobador de webshell de Wazuh MCP — GHSA-pw2j-pj4h-f5vg

Un nombre de host eludía la comprobación de IP literal de Wazuh-MCP-Server antes de que una herramienta forense lo consultara. La GHSA asigna gravedad Alta y CWE-918; no hay CVE. Corregido en el commit 2bbfe12.[1], [2], [3]

💡 Explicación en Lenguaje Sencillo (ELI5)

Una recepcionista comprueba que la credencial de una persona no aparezca en una lista de bloqueo y luego le permite elegir una oficina. Revisar solo la credencial impresa no revela que el nombre de la persona apunta a una sala restringida. La corrección comprueba dónde resuelve el nombre y obliga a la conexión a usar ese destino verificado.[1], [3]

Conceptos Clave y Términos

Server-side request forgery (SSRF)
El servidor realiza una solicitud de red a un destino influido por quien lo invoca. Una validación incompleta puede permitirle alcanzar redes a las que la persona no llega directamente.[4], [1]
Resolución DNS y rebinding
Un nombre de host se traduce en una o varias direcciones IP. Si se valida solo el texto o el cliente HTTP vuelve a resolverlo, la conexión puede usar una dirección distinta de la comprobada.[1], [3]
Fijación del destino
La IP validada se vincula a la conexión saliente, mientras el nombre se conserva para las comprobaciones HTTP y TLS. Así se cierra la brecha entre validación y conexión.[3]

Mapeos de marcos

Flujo de Ataque Paso a Paso

Paso 1

Una URL de datos forenses llega al comprobador

La herramienta afectada blueteam_check_webshell recibe una URL para que un analista examine una dirección sospechosa de alojar un webshell. El aviso limita el fallo a esta herramienta, no a todas las funciones de Wazuh-MCP-Server.[1]

Paso 2

El validador solo comprueba IP literales

El WebshellCheckInput.validate_url vulnerable analiza el host y lo pasa a ipaddress.ip_address. Puede comprobar una IP literal, pero un nombre DNS produce ValueError y se acepta sin consultar registros A o AAAA.[2], [1]

Paso 3

El cliente HTTP resuelve el host y sigue redirecciones

Después, la URL validada se entregaba a curl, que realizaba su propia resolución DNS. El aviso también documenta el seguimiento automático de redirecciones sin volver a validar el destino. Esto expone a SSRF si el servidor puede alcanzar direcciones internas y la herramienta recibe una URL influida por contenido no confiable. El impacto depende de la red y del acceso a la herramienta.[1], [3]

Paso 4

El parche valida y fija las direcciones resueltas

El commit 2bbfe12 resuelve registros IPv4 e IPv6, rechaza direcciones no públicas salvo dominios configurados en ALLOWED_INTERNAL_DOMAINS y fija la dirección usada en la solicitud. La ruta corregida no sigue redirecciones automáticamente. La GHSA asigna gravedad Alta y CWE-918; no asigna CVE ni indica un rango de versiones.[1], [3]

Código Fuente: Vulnerable vs. Seguro

El fragmento vulnerable reproduce la lógica pertinente de WebshellCheckInput.validate_url del commit 6bd1fa8; se omiten imports y campos de la clase. Rechaza IP literales no públicas, pero trata el ValueError de un nombre de host como si fuera un dominio y lo acepta sin resolver DNS. El fragmento corregido reproduce _pinned_curl_args del commit 2bbfe12. El validador corregido también llama a _host_pins; el flujo de solicitudes pasa esos argumentos --resolve a curl y valida cada destino de redirección antes del siguiente salto. El parche solo permite destinos internos en dominios configurados explícitamente. Son fragmentos concretos, no un reemplazo completo del flujo de solicitudes URL.[2], [3]

Fragmento de origen: mcp_server/tools/webshell_check.py — validación 6bd1fa8; corrección 2bbfe12

IMPLEMENTACIÓN VULNERABLE
@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}")
    # Rechaza IP privadas/reservadas en el host de la 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
PARCHE SEGURO Y 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 Verificación de Seguridad para Ingeniería

Fuentes

← AI Security Directory →