FlawOpen

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

● CWE-918 · Tinggi
Riset Keamanan · FlawOpen

Celah SSRF pada pemeriksa webshell Wazuh MCP — GHSA-pw2j-pj4h-f5vg

Nama host melewati pemeriksaan IP literal Wazuh-MCP-Server sebelum diakses oleh alat forensik. GHSA memberi tingkat keparahan High dan CWE-918; CVE belum ditetapkan. Diperbaiki pada commit 2bbfe12.[1], [2], [3]

💡 Penjelasan Sederhana (ELI5)

Petugas resepsionis memeriksa apakah kartu pengunjung ada dalam daftar blokir, lalu membiarkan pengunjung memilih kantor. Memeriksa nomor yang tercetak saja tidak menunjukkan bahwa nama itu mengarah ke ruangan terlarang. Perbaikannya memeriksa hasil resolusi nama dan mengikat koneksi ke alamat yang sudah diperiksa.[1], [3]

Konsep Kunci & Istilah

Server-side request forgery (SSRF)
Server mengirim permintaan jaringan ke tujuan yang dipengaruhi pemanggilnya. Pemeriksaan tujuan yang tidak lengkap dapat membuat server menjangkau jaringan yang tidak dapat diakses pemanggil secara langsung.[4], [1]
Resolusi DNS dan rebinding
Nama host diterjemahkan menjadi satu atau beberapa alamat IP. Jika hanya teks nama yang diperiksa atau klien HTTP melakukan resolusi ulang, koneksi dapat menuju alamat berbeda dari yang telah diperiksa.[1], [3]
Penguncian alamat tujuan
IP yang sudah divalidasi diikat ke koneksi keluar, sementara nama host tetap digunakan untuk pemeriksaan HTTP dan TLS. Ini menutup jeda antara validasi dan koneksi.[3]

Pemetaan kerangka kerja

Alur Serangan Langkah demi Langkah

Langkah 1

URL dari data forensik masuk ke alat pemeriksa

Alat terdampak blueteam_check_webshell menerima URL agar analis dapat memeriksa alamat yang dicurigai menghosting webshell. Advisory membatasi masalah pada alat ini, bukan seluruh kemampuan Wazuh-MCP-Server.[1]

Langkah 2

Validator hanya memeriksa IP literal

WebshellCheckInput.validate_url yang rentan mengambil host dan memanggil ipaddress.ip_address. IP literal dapat diperiksa, tetapi nama DNS menghasilkan ValueError lalu diterima tanpa pencarian catatan A atau AAAA.[2], [1]

Langkah 3

Klien HTTP melakukan resolusi dan mengikuti pengalihan

URL yang lolos pemeriksaan kemudian diberikan kepada curl, yang melakukan resolusi DNS sendiri. Advisory juga menjelaskan pengalihan otomatis tanpa validasi ulang tujuan. SSRF dapat terjadi jika server menjangkau alamat internal dan masukan yang tidak tepercaya dapat memengaruhi argumen URL. Dampak nyata bergantung pada jangkauan jaringan dan akses alat.[1], [3]

Langkah 4

Patch memvalidasi dan mengunci alamat hasil resolusi

Commit 2bbfe12 menyelesaikan catatan IPv4 dan IPv6, menolak alamat nonpublik kecuali host secara eksplisit dikonfigurasi di ALLOWED_INTERNAL_DOMAINS, lalu mengunci alamat terpilih pada permintaan. Jalur pengambilan yang diperbaiki tidak mengikuti pengalihan otomatis. GHSA memberi tingkat High dan CWE-918; tidak ada CVE atau rentang versi paket.[1], [3]

Kode Sumber: Rentan vs Aman

Cuplikan rentan mereproduksi logika terkait WebshellCheckInput.validate_url dari commit 6bd1fa8; import dan field kelas dihilangkan. IP literal non-publik ditolak, tetapi ValueError dari nama host dianggap sebagai domain dan diterima tanpa resolusi DNS. Cuplikan yang diperbaiki mereproduksi _pinned_curl_args dari commit 2bbfe12. Validator baru juga memanggil _host_pins; loop permintaan meneruskan argumen --resolve ke curl dan memvalidasi setiap tujuan pengalihan sebelum hop berikutnya. Tujuan internal hanya diizinkan untuk domain yang dikonfigurasi secara eksplisit. Ini cuplikan kode yang terfokus, bukan pengganti seluruh alur pengambilan URL.[2], [3]

Cuplikan sumber: mcp_server/tools/webshell_check.py — validasi 6bd1fa8; perbaikan 2bbfe12

IMPLEMENTASI RENTAN
@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}")
    # Tolak IP privat/tercadang pada host 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
PERBAIKAN AMAN & KUAT
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

Daftar Periksa Penguatan Sistem Rekayasa

Sumber

← AI Security Directory →