FlawOpen

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

● CWE-918 · Haute
Recherche · FlawOpen

Défaut SSRF de l’outil de vérification webshell Wazuh MCP — GHSA-pw2j-pj4h-f5vg

Un nom d’hôte contournait le contrôle des adresses IP littérales de Wazuh-MCP-Server avant qu’un outil forensique ne le consulte. La GHSA attribue une gravité élevée et CWE-918 ; aucun CVE n’est attribué. Correctif dans le commit 2bbfe12.[1], [2], [3]

💡 Explication en Langage Simple (ELI5)

Une réceptionniste vérifie qu’un badge ne figure pas sur une liste de blocage, puis laisse son porteur choisir un bureau. Vérifier seulement le badge imprimé ne montre pas que le nom mène à une salle interdite. Le correctif vérifie où le nom est résolu et oblige la connexion à utiliser cette destination contrôlée.[1], [3]

Concepts Clés et Termes

Server-side request forgery (SSRF)
Le serveur envoie une requête réseau vers une destination influencée par son appelant. Un contrôle incomplet peut lui permettre d’atteindre des réseaux inaccessibles directement à cet appelant.[4], [1]
Résolution DNS et rebinding
Un nom d’hôte est converti en une ou plusieurs adresses IP. Si seul le texte est contrôlé ou si le client HTTP résout à nouveau le nom, la connexion peut viser une adresse différente de celle vérifiée.[1], [3]
Épinglage de destination
L’adresse IP vérifiée est liée à la connexion sortante, tandis que le nom d’hôte est conservé pour HTTP et TLS. Cela ferme l’écart entre validation et connexion.[3]

Correspondances de référentiels

  • Référence CWE pour la faiblesse décrite dans ce guide : CWE-918
  • D3-OTF Filtrage du trafic sortant — La restriction du trafic sortant décrite dans ce guide limite les destinations accessibles au service ou à l’hôte.

Déroulement de l'Attaque Étape par Étape

Étape 1

Une URL issue de données forensiques atteint l’outil

L’outil concerné blueteam_check_webshell reçoit une URL afin qu’un analyste examine une adresse soupçonnée d’héberger un webshell. L’avis limite le défaut à cet outil, pas à toutes les fonctions de Wazuh-MCP-Server.[1]

Étape 2

Le validateur ne vérifie que les IP littérales

Le WebshellCheckInput.validate_url vulnérable extrait l’hôte puis appelle ipaddress.ip_address. Une IP littérale peut être contrôlée, mais un nom DNS déclenche ValueError et est accepté sans résolution A ou AAAA.[2], [1]

Étape 3

Le client HTTP résout l’hôte et suit les redirections

L’URL validée était ensuite transmise à curl, qui effectuait sa propre résolution DNS. L’avis documente aussi le suivi automatique des redirections sans nouvelle validation. Il y a risque SSRF si le serveur peut atteindre une adresse interne et si l’outil reçoit une URL influencée par une source non fiable. L’impact dépend du réseau et des droits d’accès à l’outil.[1], [3]

Étape 4

Le correctif valide et épingle les adresses résolues

Le commit 2bbfe12 résout les enregistrements IPv4 et IPv6, rejette les adresses non publiques sauf pour les domaines explicitement configurés dans ALLOWED_INTERNAL_DOMAINS, puis épingle l’adresse de la requête. Le chemin corrigé ne suit pas automatiquement les redirections. La GHSA attribue une gravité élevée et CWE-918 ; aucun CVE ni intervalle de versions n’est indiqué.[1], [3]

Code Source : Vulnérable vs Sécurisé

L’extrait vulnérable reproduit la logique pertinente de WebshellCheckInput.validate_url du commit 6bd1fa8 ; les imports et les champs de classe sont omis. Il rejette les IP littérales non publiques, mais traite l’erreur ValueError d’un nom d’hôte comme un domaine et l’accepte sans résolution DNS. L’extrait corrigé reproduit _pinned_curl_args du commit 2bbfe12. Le validateur corrigé appelle aussi _host_pins ; la boucle de requêtes transmet ces arguments --resolve à curl et valide chaque destination de redirection avant le saut suivant. Le correctif n’autorise des destinations internes que pour les domaines configurés explicitement. Ce sont des extraits ciblés, pas un remplacement complet du flux de récupération d’URL.[2], [3]

Extrait source: mcp_server/tools/webshell_check.py — validation 6bd1fa8 ; correctif 2bbfe12

IMPLÉMENTATION VULNÉRABLE
@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}")
    # Rejette les adresses IP privées/réservées dans l’hôte de l’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 SÉCURISÉ ET ROBUSTE
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

Liste de Contrôle de Sécurité pour l'Ingénierie

Sources

← AI Security Directory →