In 2023 NetSPI discovered that Microsoft Outlook was vulnerable to authenticated remote code execution (RCE) via synced form objects. This blog will cover how we discovered CVE-2024-21378 and weaponized it by modifying Ruler, an Outlook penetration testing tool published by SensePost. Note, a pull request containing the proof-of-concept code is forthcoming to provide organizations with sufficient time to patch.

Edit: The pull request containing the PoC can be found at

An Overview of the Vulnerability 

The original variant of this attack was documented by Etienne Stalmans at SensePost (Orange CyberDefense) in 2017 and leveraged VBScript code inside Outlook form objects to obtain code execution with access to a mailbox. In response, a patch was issued to enforce allowlisting for script code in custom forms. However, the syncing capability of these form objects was never altered. 

Underneath, forms are MAPI synced using IPM.Microsoft.FolderDesign.FormsDescription objects. These objects carry special properties and attachments which are used to “install” the form when it’s first used on a client. Below is an overview of this procedure: 

  1. Outlook requests the instantiation of a particular message class (IPM.Note.Evil). 
  2. The MAPI associated contents table of the relevant folder is consulted for IPM.Microsoft.FolderDesign.FormsDescription objects. 
  3. If the classname stored in the PidTagOfflineAddressBookName property matches, the form installation process starts. 
  4. The PidTagOfflineAddressBookDistinguishedName is used as the CLSID for the new form install (all forms are COM objects). 
  5. The first attachment of the form description and a special property, 0x6902001F, determines what registry keys need to be added under the CLSID to install the form. 
  • In older style forms (“Forms that bypass outlook”), these registry values typically include InProcServer keys or equivalents that bond to DLLs extracted to disk. 
  • In newer forms, Outlook-specific MsgClass keys are used to bond the form to an OLE object extracted to disk. 
  • Inside any of these keys, %d can be used to refer to the directory where the remaining form attachments are extracted (%localappdata%MicrosoftFORMS). 
  1. After the registry changes are confirmed, Outlook proceeds to load the form as a COM object. 

There were some serious issues with this process: 

  • When extracting the attachments to %localappdata%MicrosoftFORMS, you can perform path traversal via the PidTagAttachFilename property. You can also have multiple files written to disk. This essentially is an arbitrary disk write primitive anytime the form is installed. 
  • When creating registry keys for the form, the 0x6902001F property data is broken by newline expecting key=value lines. Each line is processed where key is a subkey of the CLSID root, and value is the default value for that key. To prevent “Forms that bypass outlook”, a denylist of typical COM server keys (InProcServer, LocalServer, etc.) is compared against the start of each line (OLMAPI32.DLL). However, when installing the value, you can use a leading character to imply a full subkey path under HKCR. For instance, CLSID<CLSID>InprocServer32=%devil.dll will bypass the denylist check and result in a full COM object registration for the form. 

We found that we had the ability to create arbitrary files on disk, as well as install arbitrary registry keys (with default values) under HKEY_CLASSES_ROOT (HKCR). These primitives are enough to gain trivial RCE.

In-Depth Review

Setting the Stage

Colloquially, we consider this to be the fourth iteration of a series of attacks based on the premise of using compromised credentials to sync objects through Exchange. In late 2015, Nick Landers, Co-Founder of Dreadnode, published a blog on the abuse of Outlook Rules for RCE. Over the next couple of years Etienne (SensePost) and Nick dual discovered two additional sets of vectors which were eventually patched by Microsoft, including the abuse of Outlook Forms. SensePost released an excellent set of blogs (see references) digging into the vulnerabilities and underlying technologies as well as the exploitation tool, Ruler

We intended to come back to this research as we felt that Outlook features a vast, underexplored attack surface and our repeated success on engagements over the last couple of years with Device Code phishing/vishing was the final push we needed.  

We began our research by manually exploring Outlook forms from the Outlook Client, as well as, with MFCMAPI and ProcMon. We will keep an overview of the underlying technology high-level, but essentially the various items available through Outlook (messages, calendar invites, tasks, etc.) are displayed through a form structure in an “inspector window”. Outlook both contains standard forms and allows custom forms that can be published and synced through Exchange (including Exchange Online). 

During our research we came upon the format of form configuration files which can be used to install a custom form. Of particular interest were the file and registry entries.

The File entry lists the form server application executable file that the form library maintains and loads into a new subdirectory in the disk cache when the form is launched. . .

The Registry entry is used whenever the File entry is used, it identifies the registry key for the form library where the executable file for the form server application is stored. . .

We first attempted to prove out local code execution. Below is an example form config that we can import directly into Outlook to install a form. We set the File entry to the location of a DLL we would like to install with our form and saved this file to c:pochello.cfg. 



File = C:pochello.dll 
Registry = InprocServer32 = %dhello.dll 

For testing purposes, we compiled a DLL with an execution primitive inside DllMain and again, placed it within the same folder as our hello.cfg file above.

#include <Windows.h> 

BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved) { 

     if (reason == DLL_PROCESS_ATTACH) { 

     MessageBoxA(0, "Hello", "Ruh Roh", 0); 


     return TRUE;


The configuration file can then be used to install a form within Outlook by navigating to File -> Options -> Advanced -> Custom Forms and selecting the hello.cfg file.

Note: we set the location to Inbox when installing the custom form via config file. 

Navigating to the Forms Manager
Installing the custom form config file in the Inbox folder
Verifying the form has been installed

We then proceeded to select the Developer tab -> Choose Form and open our newly created form.

Attempting to open our newly installed form

Well, not ideal but we remembered seeing a potentially related configuration when installing the form config file. We make the change and retry.

Custom form option related to our previous selection

Better! We confirmed execution of an arbitrary DLL, but we still have a bit to go towards anything useful. On the backend, upon installing the form the configuration file was converted to an IPM.Microsoft.FolderDesign.FormsDescription Message Class object and stored within the Inbox directory in Outlook. A review of the associated registry key after choosing the custom form from the developer tab revealed that our DLL was stored in %localappdata%MicrosoftFORMSIPM.Note.Hello, confirming not only execution but also seemingly arbitrary registry writes, and file write to disk.

Homing In

To gain a better understanding of what was happening underneath the hood we started exploring the contents of Outlook with MFCMAPI. Outlook contains your email but it’s also a database, and we can think of MFCMAPI as essentially a database exploration tool that provides you with access to properties and objects that are not otherwise visible in the Outlook client. From within MFCMAPI we right clicked on our Inbox (since that is where we saved our custom form) and navigated to the “Open associated contents table” button where we found the new form and its various properties.

Reviewing the hidden contents table for our Inbox Folder

As we explored how to recreate the IPM.Microsoft.FolderDesign.FormsDescription
objects in code, we began asking ourselves, “what is the difference between a form that can be installed and a form that ‘bypasses Outlook’? What makes that determination?” 

The property that seemed the most immediately relevant was the “PidTagOfflineAddressBookDistinguishedName” or “PR_OAB_DN”. This property tag contained the COM GUID that we have assigned in the configuration file, which ultimately defines what COM CLSID the form was eventually registered as. This was interesting because it seemed that we could create any registry key under HKCR and set it’s (default) value. Additionally, if the CLSID is arbitrary then what is stopping us from putting in the CLSID of an existing object and performing a classic COM hijack? Again, SensePost provides an overview of these property tags and their importance so we will refrain from re-stating the same here.

Property tags and their value for the IPM.Note.Hello form

We then right clicked on the form, selected “Attachments -> Display attachments table” and found the form contained several attachments, including the DLL to be registered through the form message object. Within the first attachment we also found our registry key information within a couple of property tags. We noticed the PR_ATTACH_DATA_BIN property did not seem important as we could hollow out the contents and sync the form back to Outlook with no effect. We discovered that the value for the 0x6902001F property determined what registry keys need to be added under the CLSID to install the form – another seemingly critical component. Within the first attachment we also found our registry key information within a couple of property tags.

The form attachments table in MFCMAPI

We then used Ruler to send ourselves a form intended to execute a VBScript and began comparing it to our COM DLL execution form. Reviewing the results, we confirm that the values passed in the 0x6902001F property tag were used to set registry keys.

Testing various inputs in the 0x6092001F property tag
Reviewing the results in the registry 

We also found that adding InProcServer32, to the VBScript form keys and syncing the form back to Outlook via MFCMAPI would cause a failure as we would receive the “Forms that bypass Outlook cannot be installed” error. We could clearly see this was a denylist as any other key we set would be created.

This brought us to a new line of questioning, the first being where is this denylist implemented? Could we circumvent this denylist through alternative registry keys that would not be on the denylist but would still allow us to gain code execution or execute a COM hijack? Do they limit the GUID? If we supply a GUID of a CLSID that already exists, would that work, append to the registry, or fail out? But then again, it seemed arbitrary, whatever keys we put in the form would simply be created, which seemed to us like just a bad policy.

We decided to start hunting for where the denylist was occurring. This is a bit tricky because Outlook is a beast to decompile, but we might be able to get there with help from ProcMon. We open ProcMon and execute the form one more time specifically filtering for our COM GUID.

Reviewing the stack in Process Monitor

As you can see above the last call in the stack before RegOpenKeyExA follows from a function call in OLMAPI32.DLL. We begin decompiling OLMAPI32.DLL and meander through functions and their references, ScOpenRegKey -> RegisterFormClass -> HrDownloadFormFiles, until we eventually come upon a familiar looking property tag, 0x6902001F! Well, we find 0x6902001E which is the ASCII version of our original property tag.

Jumping to our address identified in ProcMon
Reviewing references to ScOpenRegKey and discovering RegisterFormClass
Selecting a reference to RegisterFormClass
Discovering our property tag of interest within the RegisterFormClass function

Reviewing RegisterFormClass once again we find our check function (sub_1803DF094) and the denylist variable (v8 = (LPCSTR *)off_1806DAB0). 

Note: some of the variable and function names below have been modified for clarity.

The check function and denylist variable (names modified)
Our sought after denylist variable

Reviewing the delta between the denylist check and the installation we find a simple bypass using a relative path by prefacing each new line with “”. For example, “InprocServer32=%devil.dll” was blocked whereas “CLSID{00000000-1234-1234-1234-FEED00000000}InprocServer32=%devil.dll” was not blocked.

__int64 ApplyRegistryKeysFromString(HKEY regKey, LPCSTR path, LPCSTR lpSubKey, char *value) {
    HKEY hKey = 0;
    int pathLen = strlen(path);
    char *block = strdup(value);
    if (!block) return 2147942414;

    char *lineStart = block;
    while (lineStart && *lineStart) {
        const char *lineEnd = strchr(lineStart, 10);
        if (lineEnd) {
            *lineEnd = 0;

        char *equalSign = strchr(lineStart, 61);
        const char *keyValue = equalSign ? equalSign + 1 : “SzNull”;

        if (equalSign) *equalSign = 0;

        if (*lineStart == ‘’) {
            if (ScOpenRegKey(&hKey, HKEY_CLASSES_ROOT, lineStart + 1, 2, 1) == 0) {
                if (RegSetValueA(hKey, 0, 1, keyValue, strlen(keyValue)) != 0) {
                    return -2147221167;
        } else {
            if (ScSetRegValue(&hKey, regKey, lineStart, keyValue, strlen(keyValue)) != 0) {
                return -2147221167;

        lineStart = (char *)lineEnd;

    return 0;

Our research demonstrated that as an authenticated user we could: 

  • Create any registry key under HKCR and set the key’s (default) value
  • Place any number of files at an arbitrary location on disk
  • Create a form that when executed would lead to Outlook loading the registered COM object by CLSID.


As mentioned at the beginning of this article, a main driver for our research was our success with Device Code authentication token abuse during Red Team engagements. Having identified a vulnerability we could exploit for remote code execution, we proceeded to fork and modify Ruler to support authentication to Exchange Online via compromised access tokens. We considered various execution techniques but for the purpose of this proof-of-concept we kept it simple by adding code to sync a form containing the required properties to execute an arbitrary COM compliant native DLL. The public fork containing the PoC code will be found here along with a pull request to the original repo at a later date.

Some initial OpSec concerns we identified off the bat:

  • Triggering the form requires user interaction for execution (although some further digging might reveal automated execution).
  • The DLL will be extracted to the well-known FORMS directory, which could be monitored from historical attacks.
  • CLSID changes in the registry are executed by the Outlook process.
  • The DLL will be loaded into the Outlook process.

Below we provide a high-level overview for a general exploitation flow:

  1. Obtain credential material for a targeted user.
  2. Create a COM compliant DLL that we want to execute.
  3. Specify our credential material, DLL and other required/optional parameters to Ruler.
  4. Ruler will authenticate to the Exchange Server/Exchange Online and send the form as an email.
  5. The user will then need to trigger the form by either clicking, forwarding, or printing the email from their Microsoft Outlook thick client on a Windows device.
  6. Execution of the form will create the registry keys, drop the DLL to disk and load the DLL into the Outlook process.

Walkthrough from Device Code to Execution

In this section, we will walk through practical exploitation of the issue. First, we obtain refresh tokens via device code phishing/vishing using our weapon of choice, in this case TokenTactics.

PS C:TokenTactics> Import-Module .TokenTactics.psd1
PS C:TokenTactics> Get-AzureToken -Client Outlook
user_code        : L78NMWTT3
device_code      : [REDACTED]              
verification_url :
expires_in       : 900
interval         : 5
message          : To sign in, use a web browser to open the page and enter the code L78NMWTT3 to authenticate.

token_type     : Bearer
scope          : AuditLog.Read.All 
expires_in     : 8492
ext_expires_in : 8492
expires_on     : 1697842572
not_before     : 1697833779
resource       :
access_token   : eyJ0[REDACTED]

PS C:TokenTactics> $response.access_token | clip

After compiling a COM DLL, we send the form using our fork of Ruler. Below is an example Ruler command, note that the order of the provided arguments matters. Ruler will authenticate to Exchange Online with our Outlook access token and send a form as an email message that includes the DLL file. 

$ go run .ruler.go --token "[REDACTED]" --email --o365 --debug form add-com --dll evil.dll --suffix Evil -s
[+] Found cached Autodiscover record. Using this (use --nocache to force new lookup)
[+] Create Form Pointer Attachment with data:  CLSID{00000000-1234-1234-1234-FEED00000000}InprocServer32=%dMicrosoft.Teams.Shim.dll
Starting Upload
Writing final piece 0 of 0
[+] Create Form Template Attachment
Starting Upload
Writing 0 of 43
Writing final piece 43 of 43
[+] Form created successfully:  IPM.Note.Evil
[+] Sending email.
[+] Email sent!

Again, in the example above we have created a new form and sent a trigger email to the compromised email account (from itself). Although the form is installed, execution will not occur unless the trigger email is clicked (viewed in the preview pane), forwarded, or printed from within the Outlook thick client. 

Let’s take a look at some of the possible arguments for our new functionality in form add-com.

$ go run ruler.go form add-com -h                                                                                                                                                         
   ruler form add-com - creates a new COM based form.

   ruler form add-com [command options] [arguments...]

   --suffix value           A 3 character suffix for the form. Defaults to pew (default: "pew")
   --dll value, -d value    A path to a the COM DLL file to execute
   --clsid value, -c value  CLSID to use for the remote registration (default: "random")
   --name value, -n value   The DLL name on the remote system (default: "Microsoft.Teams.Shim.dll")
   --hidden                 Attempt to hide the form.
   --send, -s               Trigger the form once it's been created
   --body value, -b value   The email body you may wish to use (default: "This message cannot be displayed in the previewer.nnnnn")
   --subject value          The subject you wish to use, this should contain your trigger word (default: "Exchange Quarantine Report")

There are obviously some OpSec considerations left up to the reader.


Microsoft has released patches for CVE-2024-21378 (see the MSRC update guidance for your version of Outlook and/or Office) and we hope delaying this post a bit has given organizations a head start. We also hope that this brings back attention to what we think is a yet to be fully explored attack surface (and how shallow some protections can be). Discovering the vulnerability and bypassing it wasn’t overly complicated – we took the normal hacker-ish approach of trying to push our understanding of the underlying technology and protocols a bit further, not to mention, building on top of previous research certainly helps.

For defensive teams, Microsoft has previously published guidance regarding detecting and remediating Outlook rule and forms abuse,


  • Sept 29, 2023 – Vulnerability submitted to Microsoft.
  • Oct 2, 2023 – Microsoft opens case.
  • Oct 25, 2023 – Microsoft confirmed the behavior reported and states that they will continue their investigation and determine how to address this issue.
  • Feb 04, 2024 – Confirmation from Microsoft that a fix will be released in the following patching cycle.
  • Feb 13, 2024 – Fix for CVE-2024-21378 is released and case is closed.
  • Feb 28, 2024 – Errors in CVE FAQ and CVSS reported to Microsoft.
  • Mar 04, 2024 – Microsoft acknowledges they received the details and begins coordination with internal stakeholders.
  • Mar 05, 2024 – Microsoft updates CVE to address FAQ and CVSS errors.
  • Mar 11, 2024 –NetSPI published vulnerability and exploit details without the POC
  • Mar 18, 2024 – NetSPI published PoC