微软Exchange新攻击面揭秘:ProxyLogon漏洞链分析

本文深入分析微软Exchange服务器中发现的ProxyLogon漏洞链,包括CVE-2021-26855 SSRF漏洞和CVE-2021-27065任意文件写入漏洞,详细讲解攻击面原理和利用方式,涉及客户端访问服务架构和Kerberos认证机制。

Orange: MS Exchange新攻击面第一部分 - ProxyLogon!

这是Orange在发言 :)

2021年8月6日 星期五

MS Exchange新攻击面第一部分 - ProxyLogon!

作者: Orange Tsai(@orange_8361)
: 这是来自DEVCORE的交叉发布博客

Microsoft Exchange系列新攻击面:

  • MS Exchange新攻击面第一部分 - ProxyLogon!
  • MS Exchange新攻击面第二部分 - ProxyOracle!
  • MS Exchange新攻击面第三部分 - ProxyShell!
  • MS Exchange新攻击面第四部分 - ProxyRelay!

Microsoft Exchange作为全球最常见的电子邮件解决方案之一,已成为政府和企业日常运营和安全连接的重要组成部分。今年一月,我们向微软报告了Exchange Server的一系列漏洞,并将其命名为ProxyLogon。ProxyLogon可能是Exchange历史上最严重、影响最大的漏洞。如果您关注行业新闻,一定听说过它。

从架构层面研究ProxyLogon时,我们发现它不仅仅是一个漏洞,而是一个全新的、之前从未被提及过的攻击面。这个攻击面可能导致黑客或安全研究人员发现更多漏洞。因此,我们决定专注于这个攻击面,最终发现了至少8个漏洞。这些漏洞涵盖服务器端、客户端甚至加密错误。我们将这些漏洞串联成3种攻击方式:

  • ProxyLogon: 最知名且影响最大的Exchange利用链
  • ProxyOracle: 可以恢复Exchange用户明文密码的攻击
  • ProxyShell: 我们在Pwn2Own 2021演示的利用链,用于接管Exchange并获得20万美元奖金

我想强调的是,我们在这里披露的所有漏洞都是逻辑错误,这意味着它们比任何内存损坏错误都更容易复现和利用。我们已在Black Hat USA和DEFCON上展示了我们的研究,并获得了Pwnie Awards 2021最佳服务器端漏洞奖。您可以在此查看我们的演示材料:

ProxyLogon只是冰山一角:Microsoft Exchange Server的新攻击面!
[幻灯片] [视频]

通过了解这个新攻击面的基础知识,您就不会惊讶为什么我们能轻松地发现0day漏洞了!

介绍

我想声明,所有提到的漏洞都已通过负责任的漏洞披露流程报告给微软并已修复。您可以从下表中找到CVE的详细信息和报告时间线。

报告时间 名称 CVE 修复时间 CAS[1] 报告者
2021年1月5日 ProxyLogon CVE-2021-26855 2021年3月2日 Orange Tsai, Volexity和MSTIC
2021年1月5日 ProxyLogon CVE-2021-27065 2021年3月2日 - Orange Tsai, Volexity和MSTIC
2021年1月17日 ProxyOracle CVE-2021-31196 2021年7月13日 Orange Tsai
2021年1月17日 ProxyOracle CVE-2021-31195 2021年5月11日 - Orange Tsai
2021年4月2日 ProxyShell[2] CVE-2021-34473 2021年4月13日 Orange Tsai与ZDI合作
2021年4月2日 ProxyShell[2] CVE-2021-34523 2021年4月13日 Orange Tsai与ZDI合作
2021年4月2日 ProxyShell[2] CVE-2021-31207 2021年5月11日 - Orange Tsai与ZDI合作
2021年6月2日 - - - Orange Tsai
2021年6月2日 - CVE-2021-33768 2021年7月13日 - Orange Tsai和Dlive

[1] 直接与此新攻击面相关的错误
[2] Pwn2Own 2021错误

为什么Exchange Server成为热门话题?从我的角度来看,整个ProxyLogon攻击面实际上位于Exchange请求处理的早期阶段。例如,如果Exchange的入口是0,核心业务逻辑是100,那么ProxyLogon大约在10的位置。同样,由于漏洞位于开始位置,我相信任何仔细审查Exchange安全性的人都会发现这个攻击面。这也是为什么我在向微软报告后发推文表达了对错误碰撞的担忧。这个漏洞影响如此之大,却又如此简单,且位于如此早期的阶段。

接下来发生的事情大家都知道了,Volexity发现一个APT组织在2021年1月初利用相同的SSRF(CVE-2021-26855)访问用户电子邮件并向微软报告。微软也在3月发布了紧急补丁。从之后发布的公开信息中,我们发现尽管他们使用了相同的SSRF,但APT组织的利用方式与我们完全不同。我们通过CVE-2021-27065完成了ProxyLogon攻击链,而APT组织在其攻击中使用了EWS和两个未知漏洞。这使我们确信SSRF漏洞存在错误碰撞。

来自微软博客的图片

关于我们报告给MSRC的ProxyLogon PoC在2月底出现在野外,我们在通过彻底调查排除了从我们这边泄露的可能性后,与大家一样好奇。随着更清晰的时间线出现和更多讨论发生,这似乎不是微软第一次发生这样的事情。也许您会有兴趣从这里了解一些有趣的故事。

为什么针对Exchange Server?

邮件服务器是持有最机密秘密和企业数据的高价值资产。换句话说,控制邮件服务器意味着控制公司的生命线。作为最常用的电子邮件解决方案,Exchange Server长期以来一直是黑客的首要目标。根据我们的研究,互联网上暴露了超过40万台Exchange Server。每台服务器代表一家公司,您可以想象当Exchange Server出现严重漏洞时会有多可怕。

通常,在开始研究之前,我会审查现有的论文和错误。在整个Exchange历史中,有什么有趣的案例吗?当然有。尽管大多数漏洞基于已知的攻击向量,如反序列化或不良输入验证,但仍有一些错误值得一提。

最特别的
最特别的是2017年Equation Group的武器库。这是Exchange历史上唯一实用且公开的预认证RCE。不幸的是,该武器库仅适用于古老的Exchange Server 2003。如果武器库泄露更早发生,可能会引发另一场核级危机。

最有趣的
最有趣的是与ZDI合作的某人披露的CVE-2018-8581。虽然它只是一个SSRF,但结合该功能,它可以与NTLM Relay结合,攻击者可以将一个无聊的SSRF变成真正花哨的东西。例如,它可以通过低权限账户直接控制整个域控制器。

最令人惊讶的
最令人惊讶的是与ZDI合作的某人披露的CVE-2020-0688。该错误的根本原因是Microsoft Exchange中的硬编码加密密钥。通过此硬编码密钥,低权限攻击者可以接管整个Exchange Server。如您所见,即使在2020年,像Exchange这样的重要软件中仍然可以找到愚蠢的硬编码加密密钥。这表明Exchange缺乏安全审查,这也激励我深入研究Exchange安全性。

新攻击面在哪里?

Exchange是一个非常复杂的应用程序。自2000年以来,Exchange每3年发布一个新版本。每当Exchange发布新版本时,架构都会发生很大变化并变得不同。架构的变化和迭代使得升级Exchange Server变得困难。为了确保新架构与旧架构之间的兼容性,Exchange Server产生了一些设计债务,并导致了我们发现的新攻击面。

我们在Microsoft Exchange关注哪里?我们关注客户端访问服务(CAS)。CAS是Exchange的基本组件。回到2000/2003版本,CAS是一个独立的前端服务器,负责所有前端Web渲染逻辑。经过多次重命名、集成和版本差异,CAS已降级为邮箱角色下的服务。微软的官方文档指出:

邮箱服务器包含接受所有协议客户端连接的客户端访问服务。这些前端服务负责将连接路由或代理到邮箱服务器上相应的后端服务。

从叙述中您可以意识到CAS的重要性,并且您可以想象当在此类基础设施中发现错误时有多关键。CAS是我们关注的地方,也是攻击面出现的地方。

CAS架构

CAS是负责接受来自客户端的所有连接的基本组件,无论是HTTP、POP3、IMAP还是SMTP,并将连接代理到相应的后端服务。作为一名Web安全研究人员,我专注于CAS的Web实现。

CAS Web构建在Microsoft IIS上。如您所见,IIS内部有两个网站。“默认网站”是我们之前提到的前端,“Exchange后端”是业务逻辑所在的位置。仔细查看配置后,我们注意到前端绑定端口80和443,后端监听端口81和444。所有端口都绑定到0.0.0.0,这意味着任何人都可以直接访问Exchange的前端和后端。这不会很危险吗?请记住这个问题,我们稍后会回答。

Exchange通过IIS模块实现前端和后端的逻辑。前端和后端有几个模块来完成不同的任务,如过滤、验证和日志记录。前端必须包含一个代理模块。代理模块从客户端获取HTTP请求并添加一些内部设置,然后将请求转发到后端。至于后端,所有应用程序都包括再水合模块(Rehydration Module),负责解析前端请求,重新填充客户端信息,并继续处理业务逻辑。稍后我们将详细说明代理模块和再水合模块的工作原理。

前端代理模块

代理模块根据当前的ApplicationPath选择处理程序来处理来自客户端的HTTP请求。例如,访问/EWS将使用EwsProxyRequestHandler,而访问/OWA将触发OwaProxyRequestHandler。Exchange中的所有处理程序都继承自ProxyRequestHandler类并实现其核心逻辑,例如如何处理来自用户的HTTP请求,代理到后端的哪个URL,以及如何与后端同步信息。该类也是整个代理模块最核心的部分,我们将ProxyRequestHandler分为3个部分:

前端请求部分
请求部分将解析来自客户端的HTTP请求,并确定哪些cookie和标头可以代理到后端。前端和后端依赖HTTP标头来同步信息和代理内部状态。因此,Exchange定义了一个黑名单来避免某些内部标头被滥用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
HttpProxy\ProxyRequestHandler.cs

protected virtual bool ShouldCopyHeaderToServerRequest(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客户端请求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
HttpProxy\ProxyRequestHandler.cs

protected HttpWebRequest CreateServerRequest(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票证,后端可以验证来自前端的访问。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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(string host, int traceContext, ref AuthenticationContext authenticationContext, 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票证,代表来自前端的合法访问。

前端响应部分
最后是响应部分。它接收来自后端的响应,并决定允许哪些标头或cookie发送回前端。

后端再水合模块

现在让我们继续检查后端如何处理来自前端的请求。后端首先使用IsAuthenticated方法检查传入请求是否经过身份验证。然后后端将验证请求是否配备了称为ms-Exch-EPI-Token-Serialization的扩展权限。在默认设置下,只有Exchange机器账户才有此授权。这也是为什么前端生成的Kerberos票证可以通过检查点,但您无法使用低权限账户直接访问后端。

通过检查后,Exchange将通过将标头X-CommonAccessToken反序列化回原始访问令牌来恢复前端使用的登录身份,然后将其放入httpContext对象中以继续处理后端的业务逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Authentication\BackendRehydrationModule.cs

private void OnAuthenticateRequest(object source, EventArgs args) {
    if (httpContext.Request.IsAuthenticated) {
        this.ProcessRequest(httpContext);
    }
}

private void ProcessRequest(HttpContext httpContext) {
    CommonAccessToken token;
    if (this.TryGetCommonAccessToken(httpContext, out token)) {
        // ...
    }
}

private bool TryGetCommonAccessToken(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 as WindowsIdentity);
    } finally {
        httpContext.Items["BEValidateCATRightsLatency"] = stopwatch.ElapsedMilliseconds - elapsedMilliseconds;
    }

    token = CommonAccessToken.Deserialize(text);
    httpContext.Items["Item-CommonAccessToken"] = token;
    
    //...
}

private bool IsTokenSerializationAllowed(WindowsIdentity windowsIdentity) {
   flag2 = LocalServer.AllowsTokenSerializationBy(clientSecurityContext);
   return flag2;
}

private static bool AllowsTokenSerializationBy(ClientSecurityContext clientContext) {
    return LocalServer.HasExtendedRightOnServer(clientContext, 
        WellKnownGuid.TokenSerializationRightGuid);  // ms-Exch-EPI-Token-Serialization

}

攻击面

在简要介绍了CAS的架构之后,我们现在意识到CAS只是一个编写良好的HTTP代理(或客户端),并且我们知道实现代理并不容易。所以我在想:

我能否使用单个HTTP请求分别访问前端和后端的不同上下文,从而造成一些混淆?

如果我们能做到这一点,也许我可以绕过一些前端限制来访问任意后端并滥用一些内部API。或者,我们可以混淆上下文,利用前端和后端之间危险HTTP标头定义的不一致性来进行进一步的有趣攻击。

带着这些想法,让我们开始狩猎吧!

ProxyLogon

第一个利用是ProxyLogon。如前所述,这可能是Exchange历史上最严重的漏洞。ProxyLogon由2个错误串联而成:

  • CVE-2021-26855 - 预认证SSRF导致身份验证绕过
  • CVE-2021-27065 - 后认证任意文件写入导致RCE

CVE-2021-26855 - 预认证SSRF

前端中有20多个处理程序对应不同的应用程序路径。在审查实现时,我们发现静态资源处理程序中的GetTargetBackEndServerUrl方法直接通过cookie分配后端目标URL。

在了解了架构之后,您现在知道这个漏洞有多简单了!

1
2
3
4
HttpProxy\ProxyRequestHandler.cs

protected virtual Uri GetTargetBackEndServerUrl() {
    this.LogElapsedTime("E_T
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计