Exchange服务器新攻击面分析:ProxyLogon漏洞链详解
作者:Orange Tsai(@orange_8361)
Microsoft Exchange攻击面系列
- A New Attack Surface on MS Exchange Part 1 - ProxyLogon!
- A New Attack Surface on MS Exchange Part 2 - ProxyOracle!
- A New Attack Surface on MS Exchange Part 3 - ProxyShell!
- A New Attack Surface on MS Exchange Part 4 - ProxyRelay!
概述
Microsoft Exchange作为全球最常见的电子邮件解决方案之一,已成为政府和企业日常运营和安全连接的重要组成部分。今年1月,我们向微软报告了Exchange Server的一系列漏洞,并将其命名为ProxyLogon。这可能是Exchange历史上最严重和影响最大的漏洞。
从架构层面研究ProxyLogon时,我们发现它不仅仅是一个漏洞,而是一个全新的、从未被提及过的攻击面。这个攻击面可能导致黑客或安全研究人员发现更多漏洞。因此,我们决定专注于这个攻击面,最终发现了至少8个漏洞。这些漏洞涵盖服务器端、客户端,甚至是加密错误。我们将这些漏洞串联成3种攻击:
- ProxyLogon:最知名和影响最大的Exchange利用链
- ProxyOracle:可恢复Exchange用户明文密码的攻击
- ProxyShell:在Pwn2Own 2021演示的利用链,接管Exchange并获得200,000美元奖金
需要强调的是,我们披露的所有漏洞都是逻辑错误,这意味着它们比任何内存损坏错误更容易复现和利用。我们已在Black Hat USA和DEFCON上展示了研究成果,并获得了Pwnie Awards 2021最佳服务器端错误奖。
漏洞时间线
报告时间 |
名称 |
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?
邮件服务器是高度有价值的资产,包含最机密的秘密和企业数据。换句话说,控制邮件服务器意味着控制公司的生命线。作为最常用的电子邮件解决方案,Exchange Server长期以来一直是黑客的首要目标。根据我们的研究,互联网上暴露了超过40万台Exchange服务器。每台服务器代表一家公司,你可以想象当Exchange Server出现严重漏洞时会有多可怕。
新的攻击面在哪里?
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和header可以代理到后端。前端和后端依赖HTTP头来同步信息和代理内部状态。因此,Exchange定义了一个黑名单来避免某些内部头被滥用。
1
2
3
4
5
6
7
8
9
10
11
|
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
|
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
|
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);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
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
|
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分配后端目标。
在学习架构之后,你现在知道这个漏洞有多简单了!
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
|
protected virtual Uri GetTargetBackEndServerUrl() {
this.LogElapsedTime("E_TargetBEUrl");
Uri result;
try {
UrlAnchorMailbox urlAnchorMailbox = this.AnchoredRoutingTarget.AnchorMailbox as UrlAnchorMailbox;
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分配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
protected override AnchorMailbox ResolveAnchorMailbox() {
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来访问任意服务器和端口。
1
|
https://[foo]@example.com:443/path#]:444/owa/auth/x.js
|
到目前为止,我们有一个超级SSRF,可以控制几乎所有的HTTP请求并获取所有回复。最令人印象深刻的是,Exchange的前端会为我们生成Kerberos票据,这意味着即使我们在攻击受保护且加入域的HTTP服务时,仍然可以使用Exchange机器帐户的身份验证进行黑客攻击。
那么,这种任意后端分配的根源是什么?如前所述,Exchange Server在发布新版本时会更改其架构。即使同名组件在不同版本中也可能具有不同的功能。微软为确保新旧版本之间的架构兼容性付出了巨大努力。这个cookie是一个快速解决方案,也是Exchange的设计债务,使得新架构中的前端能够识别旧后端的位置。
CVE-2021-27065 - 认证后任意文件写入
感谢超级SSRF允许我们无限制地访问后端。接下来是找到一个RCE错误来串联在一起。这里我们利用后端内部API /proxyLogon.ecp来成为管理员。这个API也是我们称之为ProxyLogon的原因。
由于我们利用静态资源的前端处理程序访问ECP(Exchange控制面板)后端,ECP后端中的特殊HTTP头msExchLogonMailbox不会被前端阻止。通过利用这种微小的不一致性,我们可以将自己指定为SYSTEM用户,并通过内部API生成有效的ECP会话。
利用前端和后端之间的不一致性,我们可以通过头伪造和内部后端API滥用访问ECP上的所有功能。接下来,我们必须在ECP接口上找到一个RCE错误来将它们串联在一起。ECP通过/ecp/DDI/DDIService.svc将Exchange PowerShell命令包装为抽象接口。DDIService通过XAML定义了几个PowerShell执行管道,以便可以通过Web访问。在验证DDI实现时,我们发现WriteFileActivity的标签没有正确检查文件路径,导致任意文件写入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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完美展示了这个攻击面可能有多严重。我们还有更多示例即将到来。敬请关注!