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 解決とリバインディング
ホスト名は1つ以上の 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 →