MS Exchange的新攻击面,第1部分:ProxyLogon!
作为世界上最常见的电子邮件解决方案之一,Microsoft Exchange已经成为政府和企业日常运作和安全防护的重要组成部分。今年一月,我们向微软报告了Exchange Server的一系列漏洞,并将其命名为ProxyLogon。ProxyLogon可能是Exchange历史上最严重、影响最大的漏洞。如果您关注行业新闻,那一定听说过这则消息。
在从架构层面研究ProxyLogon时,我们发现这绝不仅仅是一个漏洞,而是一个全新的攻击面,因为以前从来没有人提到过。这个攻击面可以引导黑客或安全研究人员发现更多的漏洞。因此,我们决定深入研究这个攻击面,最终发现了至少8个漏洞。这些漏洞涵盖了服务器端、客户端,甚至是密码漏洞。我们将这些漏洞组合成3种攻击,具体如下所示:
- ProxyLogon:最著名和最有影响的Exchange漏洞链。
- ProxyOracle:可以恢复Exchange用户的任何明文格式的密码的攻击。
- PrxoyShell:我们在Pwn2Own 2021上展示的攻击链,用于接管Exchange并获得了
- 200,000美元的赏金。
我想强调的是,我们在这里揭示所有漏洞都属于逻辑漏洞,这意味着它们比任何内存损坏漏洞更容易被复现和利用。我们已经在美国黑帽大会和DEFCON上展示了我们的研究成果,并赢得了2021年Pwnie Awards的最佳服务器端漏洞。感兴趣的读者可以从这里找到我们的演讲材料:
- ProxyLogon is Just the Tip of the Iceberg:A New Attack Surface on Microsoft ExchangeServer! [Slides] [Video]
通过了解这个新的攻击面的基础知识,您就不会对我们如此轻松地爆出这么多0day漏洞感到惊讶了!
简介
需要说明的是,这里提到的所有漏洞都是通过负责任的漏洞披露流程报告的,并且Microsoft公司目前已经对这些漏洞进行了修复。您可以从下表中找到有关CVE和报告时间表的更多详细信息。
[1] 与这个新的攻击面直接相关的漏洞。
[2] Pwn2Own 2021大赛中使用的漏洞。
为什么Exchange Server会成为热门话题?在我看来,整个ProxyLogon攻击面实际上位于Exchange请求处理的早期阶段。例如,如果Exchange的入口所处的阶段为0,而核心业务逻辑所处的阶段为100,那么ProxyLogon则大约是10。同样,由于该漏洞位于开始位置,我相信只要是仔细审查过Exchange安全性的人都能发现该攻击面。因此,我在向微软报告后,曾经在推特上表示过对于“撞洞”的担忧。虽然该漏洞的影响非常巨大,但它的确非常简单,而且位于如此早期的阶段。
你们都知道接下来发生了什么,Volexity在2021年1月初发现一个APT小组正在利用相同的SSRF(CVE-2021-26855)访问用户的电子邮件,并向微软提交了漏洞报告。微软也在3月份发布了紧急补丁。从后来公布的公开信息中,我们发现,即使他们使用的是相同的SSRF,但APT组织利用该漏洞的方式与我们的方式存在明显差异。我们通过CVE-2021-27065完成了ProxyLogon攻击链,而APT组织则在他们的攻击中利用了EWS和两个未知漏洞。这使我们确信在SSRF漏洞方面的确存在“撞洞”现象。
对于我们报告给MSRC的ProxyLogon PoC在2月下旬出现在野外一事,我们和大家一样感到奇怪,因为我们通过彻底的调查排除了我们这边泄漏的可能性。随着更清晰的时间表和更多的讨论的出现,我们发现这样的事情似乎不是第一次发生在微软身上了。如果您有兴趣的话,也许可以从这里了解到一些有趣的事情。
为何将矛头指向Exchange Server?
邮件服务器是一种非常有价值的资产,它保存着最机密的秘密和公司数据。换句话说,控制邮件服务器就意味着控制了公司的生命线。作为最常用的电子邮件解决方案,Exchange Server长期以来一直是黑客的首要目标。根据我们的研究,有超过四十万个Exchange Server暴露在互联网上;而每台服务器则代表着一家公司,因此,您可以想象Exchange server中出现严重漏洞时,到底有多可怕。
通常情况下,在开始研究之前,我会考察现有的论文和安全漏洞。纵观Exchange的历史,是否存在有趣的案例呢?当然有。虽然大多数漏洞都是基于已知的攻击手段,比如反序列化或糟糕的输入验证,但仍有几个漏洞让人眼前一亮。
最特殊的漏洞
这个最特别的漏洞,就是2017年被Equation Group曝光的arsenal漏洞。这是Exchange历史上唯一实用和公开的无需身份验证的RCE漏洞。不幸的是,arsenal漏洞仅适用于古老的Exchange Server 2003。如果泄漏时间再提前一些,arsenal很可能成为另一个核弹级漏洞。
最有趣的漏洞
最有趣的漏洞,是由与ZDI合作的研究人员披露的CVE-2018-8581。虽然它只是一个简单的SSRF漏洞,但有了这个特性,就可以与NTLM Relay结合起来,这样的话,攻击者便可以把一个普通的SSRF玩出花来。例如,利用这种方式,攻击者可以通过一个低权限账户直接接管整个域控制器。
最令人惊讶的漏洞
最令人惊讶的漏洞是CVE-2020-0688,也是由ZDI的工作人员披露的。这个漏洞的根本原因是由于Microsoft Exchange中的硬编码加密密钥造成的。使用这个硬编码的密钥,具有低权限的攻击者可以接管整个Exchange Server。正如你所看到的,即使在2020年,一个愚蠢的硬编码密码密钥仍然可以在像Exchange这样的重要软件中找到。这表明Exchange缺乏安全审查,这也让我对Exchange的安全性进行深入研究增加了更大的动力。
新的攻击面在哪里
Exchange是一个非常复杂的应用程序。自2000年以来,Exchange每3年就会发布一个新的版本。每当Exchange发布一个新的版本,架构就会发生很大的变化。架构的变化和迭代使得Exchange服务器的升级变得困难。为了确保新架构和旧架构之间的兼容性,Exchange服务器出现了一些设计上的缺陷,从而导致了我们新发现的攻击面。
对于Exchange来说,我们的重点关注对象是什么?我们重点关注的是客户访问服务,即CAS。CAS是Exchange的一个基本组成部分。早在2000/2003年的版本中,CAS还是一个独立的前端服务器,负责所有的前端网络渲染逻辑。经过几次重命名、整合和版本更新,CAS已经被降级为Mailbox Role下的一个服务。根据微软的官方文档:
- 邮箱服务器包含接受所有协议的客户端连接的客户端访问服务。这些前端服务负责路由或代理连接到邮箱服务器上的相应后端服务。
从上面的叙述中你可以意识到CAS的重要性,也不难想象当在这种基础设施中发现漏洞时,会造成多大的危害。因此,CAS正是我们需要关注的地方,也是攻击面出现的地方。
CAS的架构
CAS是负责接受所有来自客户端的连接的基本组件,无论它是HTTP、POP3、IMAP还是SMTP,并代理连接到相应的后端服务。作为一名网络安全研究人员,我自然会专注于CAS的网络实现。
CAS的网络是建立在Microsoft IIS之上的。正如你所看到的,IIS里面有两个网站。“Default Website”是我们之前提到的前端,而“Exchange Backend”则是业务逻辑之所在。在仔细查看配置后,我们注意到前端与80和443端口相绑定,而后端则监听81和444端口。并且,所有的端口都与0.0.0.0绑定在一起,这意味着任何人都可以直接访问Exchange的前端和后端。这不是很危险吗?请记住这个问题,我们将在后面给出答案。
Exchange通过IIS模块实现前端和后端的逻辑。前端和后端有几个模块来完成不同的任务,如过滤、验证和记录。前端必须包含一个代理模块。代理模块从客户端接收HTTP请求,并添加一些内部设置,然后将请求转发给后端。至于后端,所有的应用程序都包括Rehydration模块,它负责解析前端的请求,填充客户端的信息,并继续处理业务逻辑。稍后我们将详细介绍代理模块和Rehydration模块的工作原理。
前端代理模块
代理模块用于根据当前的ApplicationPath选择一个处理程序来处理客户端的HTTP请求。例如,访问/EWS时,将使用EwsProxyRequestHandler;对于/OWA则触发OwaProxyRequestHandler。Exchange中的所有处理程序都继承了ProxyRequestHandler的类,并实现其核心逻辑,例如如何处理来自用户的HTTP请求,从后端代理到哪个URL,以及如何与后端同步信息。该类也是整个代理模块中最核心的部分,我们可以把ProxyRequestHandler分成3个部分:
前端请求部分
请求部分将解析来自客户端的HTTP请求,并确定哪些cookie和头部可以被代理到后端。前端和后端都是根据HTTP头部来同步信息和代理内部状态。因此,Exchange定义了一个黑名单,以避免一些内部头信息被误用。
HttpProxy\ProxyRequestHandler.cs
protected virtual boolShouldCopyHeaderToServerRequest(string
headerName) {
return !string.Equals(headerName, "X-CommonAccessToken",
OrdinalIgnoreCase)
&& !string.Equals(headerName, "X-IsFromCafe",OrdinalIgnoreCase)
&& !string.Equals(headerName, "X-SourceCafeServer",
OrdinalIgnoreCase)
&& !string.Equals(headerName, "msExchProxyUri",OrdinalIgnoreCase)
&& !string.Equals(headerName,"X-MSExchangeActivityCtx",
OrdinalIgnoreCase)
&& !string.Equals(headerName,"return-client-request-id",
OrdinalIgnoreCase)
&& !string.Equals(headerName, "X-Forwarded-For",
OrdinalIgnoreCase)
&& (!headerName.StartsWith("X-Backend-Diag-",OrdinalIgnoreCase)
|| this.ClientRequest.GetHttpRequestbase().IsProbeRequest());
}
在处理请求的最后阶段,代理模块将调用处理程序实现的AddProtocolSpecificHeadersToServerRequest方法,在HTTP头部中添加要与后端通信的信息。这一部分还将序列化当前登录用户的信息,并将其放入一个新的HTTP头部X-CommonAccessToken中,并将其转发给后端。
例如,如果我用Orange这个名字登录Outlook Web Access(OWA),那么,前端代理发给后端的X-CommonAccessToken头部将是下面的样子:
前端代理部分
代理部分首先使用GetTargetBackendServerURL方法来计算HTTP请求应该被转发到哪个后端URL。然后,它会用CreateServerRequest方法初始化一个新的HTTP客户端请求。
HttpProxy\ProxyRequestHandler.cs
protected HttpWebRequestCreateServerRequest(Uri targetUrl) {
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create
(targetUrl);
if (!HttpProxySettings.UseDefaultWebProxy.Value) {
httpWebRequest.Proxy = NullWebProxy.Instance;
}
httpWebRequest.ServicePoint.ConnectionLimit =
HttpProxySettings.ServicePointConnectionLimit.Value;
httpWebRequest.Method = this.ClientRequest.HttpMethod;
httpWebRequest.Headers["X-FE-ClientIP"] =
ClientEndpointResolver.GetClientIP(SharedHttpContextWrapper.GetWrapper
(this.HttpContext));
httpWebRequest.Headers["X-Forwarded-For"] =
ClientEndpointResolver.GetClientProxyChainIPs
(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
httpWebRequest.Headers["X-Forwarded-Port"] =
ClientEndpointResolver.GetClientPort
(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
httpWebRequest.Headers["X-MS-EdgeIP"] =
Utilities.GetEdgeServerIpAsProxyHeader
(SharedHttpContextWrapper.GetWrapper(this.HttpContext).Request);
// ...
return httpWebRequest;
}
此外,Exchange还会通过后端的HTTP服务类生成一个Kerberos票据,并将其放在Authorization头部中。这个头部的作用,就是为了防止匿名用户直接访问后端。有了Kerberos票据,后端就可以验证来自前端的访问了。
HttpProxy\ProxyRequestHandler.cs
if (this.ProxyKerberosAuthentication) {
serverRequest.ConnectionGroupName =
this.ClientRequest.UserHostAddress +":" + GccUtils.GetClientPort
(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
} else if (this.AuthBehavior.AuthState ==AuthState.BackEndFullAuth ||
this.
ShouldBackendRequestBeAnonymous() ||
(HttpProxySettings.TestBackEndSupportEnabled.Value
&& !string.IsNullOrEmpty(this.ClientRequest.Headers
["TestBackEndUrl"]))) {
serverRequest.ConnectionGroupName = "Unauthenticated";
} else {
serverRequest.Headers["Authorization"] =
KerberosUtilities.GenerateKerberosAuthHeader(
serverRequest.Address.Host, this.TraceContext,
ref this.authenticationContext, ref this.kerberosChallenge);
}
HttpProxy\KerberosUtilities.cs
internal static string GenerateKerberosAuthHeader(stringhost, int
traceContext, ref AuthenticationContextauthenticationContext, ref
string kerberosChallenge) {
byte[] array = null;
byte[] bytes = null;
// ...
authenticationContext = new AuthenticationContext();
string text = "HTTP/" + host;
authenticationContext.InitializeForOutboundNegotiate
(AuthenticationMechanism.Kerberos, text,null, null);
SecurityStatus securityStatus =
authenticationContext.NegotiateSecurityContext(inputBuffer,out bytes);
// ...
string @string = Encoding.ASCII.GetString(bytes);
return "Negotiate " + @string;
}
因此,代理到后端的客户端请求将被添加几个HTTP头部供内部使用。其中,两个最基本的头部是X-CommonAccessToken,它表示邮件用户的登录身份;另一个头部是Kerberos Ticket,它代表来自前端的合法访问权限。
前端响应部分
最后是响应部分。它接收来自后端的响应,并决定哪些头部或cookies允许被送回前端。
后端Rehydration模块
现在,让我们继续检查后端如何处理来自前端的请求。后端首先使用IsAuthenticated方法来检查传入的请求是否通过了身份验证。然后,后端将验证该请求是否配备了名为ms-Exch-EPI-Token-Serialization的扩展权限。在默认设置下,只有Exchange机器账户会有这样的授权。这也是为什么前端生成的Kerberos票据可以通过检查点,但你却无法用低权限账户直接访问后端的原因。
通过检查后,Exchange将恢复前端使用的登录身份,通过反序列化头部X-CommonAccessToken,恢复原始访问令牌,然后,将其放入httpContext对象中,以进入后端的业务逻辑。
Authentication\BackendRehydrationModule.cs
private void OnAuthenticateRequest(objectsource, EventArgs args) {
if (httpContext.Request.IsAuthenticated) {
this.ProcessRequest(httpContext);
}
}
private void ProcessRequest(HttpContexthttpContext) {
CommonAccessToken token;
if (this.TryGetCommonAccessToken(httpContext, out token)) {
// ...
}
}
private boolTryGetCommonAccessToken(HttpContext httpContext, out
CommonAccessToken token) {
string text =httpContext.Request.Headers["X-CommonAccessToken"];
if (string.IsNullOrEmpty(text)) {
return false;
}
bool flag;
try {
flag = this.IsTokenSerializationAllowed
(httpContext.User.Identity asWindowsIdentity);
}finally {
httpContext.Items["BEValidateCATRightsLatency"] =
stopwatch.ElapsedMilliseconds -elapsedMilliseconds;
}
token = CommonAccessToken.Deserialize(text);
httpContext.Items["Item-CommonAccessToken"] = token;
//...
}
private boolIsTokenSerializationAllowed(WindowsIdentity
windowsIdentity) {
flag2 = LocalServer.AllowsTokenSerializationBy
(clientSecurityContext);
return flag2;
}
private static boolAllowsTokenSerializationBy(ClientSecurityContext
clientContext) {
return LocalServer.HasExtendedRightOnServer(clientContext,
WellKnownGuid.TokenSerializationRightGuid); // ms-Exch-EPI-
Token-Serialization
}
攻击面在简单介绍了CAS的架构后,我们现在已经意识到:CAS只不过是一个写得很好的HTTP代理(或者说客户端)而已;同时,我们都知道实现代理并不是一件容易的事情。所以,我在想:
- 我能不能用一个HTTP请求来分别访问前端和后端不同的上下文,以制造一些混乱?
如果我们能做到这一点,也许我可以绕过一些前端的限制,来访问任意的后端并滥用一些内部API。或者,我们可以通过混淆上下文,利用前端和后端之间危险的HTTP头部定义的不一致性,来发动进一步的攻击。
带着这些想法,让我们开始猎洞吧!
ProxyLogon
第一个漏洞是ProxyLogon。正如之前介绍的,这可能是Exchange历史上最严重的漏洞。ProxyLogon是通过两个漏洞组合而成:
- CVE-2021-26855:无需身份验证的SSRF漏洞导致身份验证绕过;
- CVE-2021-27065:需要经过身份验证的任意文件写入漏洞导致RCE;
- CVE-2021-26855:无需身份验证的SSRF漏洞。
对于前端的不同应用路径,存在20多个相应的处理程序。在查看实现时,我们发现GetTargetBackEndServerUrl方法负责计算静态资源处理程序中的后端URL,它可以直接通过cookie分配后端目标。在学习了架构之后,您就明白前面为什么说这个漏洞其实非常简单了吧!
HttpProxy\ProxyRequestHandler.cs
protected virtual UriGetTargetBackEndServerUrl() {
this.LogElapsedTime("E_TargetBEUrl");
Uri result;
try {
UrlAnchorMailbox urlAnchorMailbox =
this.AnchoredRoutingTarget.AnchorMailbox asUrlAnchorMailbox;
if (urlAnchorMailbox != null) {
result = urlAnchorMailbox.Url;
} else {
UriBuilder clientUrlForProxy = this.GetClientUrlForProxy();
clientUrlForProxy.Scheme = Uri.UriSchemeHttps;
clientUrlForProxy.Host =
this.AnchoredRoutingTarget.BackEndServer.Fqdn;
clientUrlForProxy.Port = 444;
if (this.AnchoredRoutingTarget.BackEndServer.Version <
Server.E15MinVersion) {
this.ProxyToDownLevel = true;
RequestDetailsLoggerbase<RequestDetailsLogger>.SafeAppendGenericInfo
(this.Logger, "ProxyToDownLevel",true);
clientUrlForProxy.Port = 443;
}
result = clientUrlForProxy.Uri;
}
}
finally {
this.LogElapsedTime("L_TargetBEUrl");
}
return result;
}
从上面的代码片段中可以看到,AnchoredRoutingTarget的BackEndServer.Fqdn属性是由cookie直接分配的。
HttpProxy\OwaResourceProxyRequestHandler.cs
protected override AnchorMailboxResolveAnchorMailbox() {
HttpCookie httpCookie = base.ClientRequest.Cookies["X-AnonResource-
Backend"];
if (httpCookie != null) {
this.savedBackendServer = httpCookie.Value;
}
if (!string.IsNullOrEmpty(this.savedBackendServer)) {
base.Logger.Set(3, "X-AnonResource-Backend-Cookie");
if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
ExTraceGlobals.VerboseTracer.TraceDebug<HttpCookie, int>
((long)this.GetHashCode(),
"[OwaResourceProxyRequestHandler::ResolveAnchorMailbox]:
AnonResourceBackend cookie used: {0};context {1}.", httpCookie,
base.TraceContext);
}
return new ServerInfoAnchorMailbox(BackEndServer.FromString
(this.savedBackendServer), this);
}
return new AnonymousAnchorMailbox(this);
}
虽然我们现在只能控制URL的Host部分,但是等一下,操纵URL解析器不正是我所擅长的事情吗?不过,Exchange是通过内置的UriBuilder来建立后端URL的。然而,由于C#没有验证Host,所以,我们可以用一些特殊字符来围住整个URL,以访问任意的服务器和端口。
- https://[foo]@example.com:443/path#]:444/owa/auth/x.js
到目前为止,我们已经找到了一个超级SSRF漏洞,可以控制几乎所有的HTTP请求,并获得所有的响应。最令人印象深刻的是,Exchange的前端会为我们生成一个Kerberos票据,这意味着即使我们攻击的是一个受保护的、与域相连的HTTP服务,我们仍然可以利用Exchange机器帐户的身份验证来发动攻击。
那么,这种任意的后端权限分配的根本原因是什么?如前所述,Exchange服务器在发布新版本时改变了其架构。它在不同的版本中可能有不同的功能,即使是同一名称下的同一组件也是如此。虽然微软为确保新旧版本之间的架构架构兼容方面付出了巨大努力,但是,这个cookie却只是一个快餐式的解决方案,Exchange的设计缺陷使得新架构中的前端可以识别旧的后端在哪里。
CVE-2021-27065:需要身份验证的任意文件写入漏洞
利用这个超级SSRF漏洞,我们已经可以不受限制地访问后端了。接下来要做的事情,就是找到一个RCE漏洞,并将其串联起来。在这里,我们利用后端内部的API/proxyLogon.ecp获得管理员权限。这个漏洞之所以命名为ProxyLogon,也是由于需要利用这个API的缘故。
因为我们利用静态资源的前端处理程序来访问ECExchange控制面板(ECP)后端,这样,前端就不会阻止头部msExchLogonMailbox了,因为它是ECP后端的一个特殊HTTP头部。通过利用这种轻微的不一致性,我们可以将自己指定为SYSTEM用户,并使用内部API生成有效的ECP会话。
利用前端和后端之间的不一致性,我们就可以通过伪造头部并滥用内部后端API来访问ECP上的所有功能。接下来,我们必须在ECP接口上找到一个RCE漏洞,并把它们串联一起。ECP通过/ecp/DDI/DDIService.svc把Exchange的PowerShell命令封装成一个抽象接口。DDIService通过XAML定义了几个PowerShell执行管道,使其可以通过Web访问。在检查DDI的实现代码时,我们发现WriteFileActivity的标签并没有正确地检查文件路径,从而导致了任意文件写入漏洞。
DDIService\WriteFileActivity.cs
public override RunResult Run(DataRow input,DataTable dataTable,
DataobjectStore store, Type codeBehind,Workflow.UpdateTableDelegate
updateTableDelegate) {
DataRow dataRow = dataTable.Rows[0];
string value = (string)input[this.InputVariable];
string path = (string)input[this.OutputFileNameVariable];
RunResult runResult = new RunResult();
try {
runResult.ErrorOccur = true;
using (StreamWriter streamWriter = new StreamWriter(File.Open
(path, FileMode.CreateNew)))
{
streamWriter.WriteLine(value);
}
runResult.ErrorOccur = false;
}
// ...
}
实际上,存在多种途径,它们都可以触发任意文件写入的漏洞。这里,我们用ResetOABVirtualDirectory.xaml作为例子,把Set-OABVirtualDirectory的结果写到webroot上,使其成为我们的Webshell。
现在,我们找到了一个有效的、无需身份验证的RCE漏洞链。也就是说,现在一个未经身份验证的攻击者已经可以通过暴露的443端口在Microsoft Exchange Server上执行任意命令了。这里是我们的演示视频。
后记
作为这个系列的第一篇文章,ProxyLogon完美地展示了这个攻击面的危险程度。接下来,我们将演示更多的相关示例,敬请期待!
下一篇:MS Exchange的新攻击面,第2部分:ProxyOracle!
原文地址:https://devco.re/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-1-ProxyLogon/
最新评论