FlawOpen

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

● CWE-918 · 높음
보안 연구 · FlawOpen

Wazuh MCP webshell 검사 도구의 SSRF 취약점 — GHSA-pw2j-pj4h-f5vg

포렌식 도구가 요청하기 전에 호스트 이름이 Wazuh-MCP-Server의 리터럴 IP 검사를 우회했습니다. GHSA는 심각도를 High, CWE를 CWE-918로 지정했으며 CVE는 없습니다. commit 2bbfe12에서 수정되었습니다.[1], [2], [3]

💡 알기 쉬운 설명 (ELI5)

안내 데스크 직원이 방문자 배지를 차단 목록과 대조한 뒤 방문자가 사무실을 고르게 한다고 생각해 보세요. 인쇄된 배지만 확인하면 그 이름이 제한된 방을 가리키는지 알 수 없습니다. 수정 사항은 이름이 해석되는 주소를 확인하고 연결을 확인된 주소에 고정합니다.[1], [3]

핵심 개념 및 용어

서버 측 요청 위조(SSRF)
호출자가 영향을 준 대상에 서버가 네트워크 요청을 보내는 문제입니다. 대상 검사가 불완전하면 호출자가 직접 접근하지 못하는 네트워크에 서버가 도달할 수 있습니다.[4], [1]
DNS 해석 및 rebinding
호스트 이름은 하나 이상의 IP 주소로 해석됩니다. 이름 문자열만 검사하거나 HTTP 클라이언트가 나중에 다시 해석하면 실제 연결 주소가 검사한 주소와 달라질 수 있습니다.[1], [3]
대상 주소 고정
검증한 IP를 송신 연결에 연결하면서 HTTP와 TLS 검사에는 호스트 이름을 유지합니다. 검증과 연결 사이의 차이를 없앱니다.[3]

프레임워크 매핑

단계별 공격 실행 흐름

단계 1

포렌식 데이터의 URL이 검사 도구로 전달됨

영향을 받는 blueteam_check_webshell 도구는 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 조회가 수행됩니다. 권고문은 목적지를 다시 검사하지 않는 자동 리디렉션도 설명합니다. 서버가 내부 주소에 도달할 수 있고 신뢰할 수 없는 입력이 URL을 제어하면 SSRF 위험이 생깁니다. 실제 영향은 네트워크 도달성과 도구 권한에 달려 있습니다.[1], [3]

단계 4

패치는 해석된 주소를 검증하고 고정함

commit 2bbfe12는 IPv4와 IPv6 레코드를 해석하고, ALLOWED_INTERNAL_DOMAINS에 명시된 호스트가 아니면 비공개 주소를 거부하며 선택한 주소를 요청에 고정합니다. 수정된 가져오기 경로는 리디렉션을 자동으로 따르지 않습니다. GHSA는 High와 CWE-918을 지정했고 CVE나 패키지 버전 범위는 없습니다.[1], [3]

소스 코드 비교: 취약한 구현 vs 보안 패치

취약 코드 발췌는 commit 6bd1fa8의 WebshellCheckInput.validate_url 관련 로직을 재현하며 import와 클래스 필드는 생략했습니다. 공개되지 않은 리터럴 IP는 거부하지만 호스트 이름에서 발생한 ValueError를 도메인으로 취급해 DNS 조회 없이 허용합니다. 수정 코드 발췌는 fix commit 2bbfe12의 _pinned_curl_args를 재현합니다. 수정된 validator도 _host_pins를 호출하며, 요청 루프는 --resolve 인수를 curl에 전달하고 다음 이동 전에 각 리디렉션 대상을 검증합니다. 내부 대상은 명시적으로 설정된 도메인에만 허용됩니다. 이는 핵심을 보여 주는 소스 발췌이며 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}")
    # URL 호스트의 사설/예약 IP 주소 거부.
    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 →