文章目录
1. 前言
Exchange服务已经存在很多年,架构一直在变化而且愈来愈复杂,与此同时出现问题的可能性也更多了。
CAS模块,在Exchange 2016上正式成为Mailbox Role上的一个服务,ProxyLogon由此模块开始。
2. 理解CAS
CAS,全名是客户端访问服务。一句话概括,CAS是一个Exchange服务端代理,运行在443端口,真正的服务运行在exchange服务器的444端口。
它可以代理所有协议的客户端访问,并将流量输送到服务端也就是Backend services。在代理的时候除了客户端原本的请求信息外,CAS还会自己在请求包中加一些数据。
cas中有许多服务例如autodiscover,他们一起构成了frontend,但他们都不是服务本身。不同的服务都有自己专属的proxy module,这些proxy module的作用是将流量传送给后端(backend)真正的服务。
真正的访问流程应该如下:
client<>echange:443(CAS也可以叫做frontend)<>exchange:444(backend)
CAS web端是基于IIS的,里面含有很多服务,例如POP3等如下图:
我们可知道Default Web Site是绑定在80和443端口,并且绑定在0.0.0.0的也就意味着所有人都可访问,Default Web Site可以理解成CAS所有服务的集合体,也可以叫做Frontend
。
而Back End在81、444端口,绑定的ip也是0.0.0.0,也是所有人都可以访问的。
Frontend必须有proxy module,它用来接收客户端发送的http请求然后加上一些配置后再发送给Backend。
Backend一定会有Rehydration Module模块,它是用来负责解析Frontend中proxy module发送过来的请求的,并将请求传送给真正的服务进行处理然后将结果返回给Frontend中的proxy module。
综上,CAS里有许多proxy module,这些模块负责接收流量,并将其正确的传送给backend。而backend的Rehydration Module模块负责接收这些流量并进行处理。整体通行过程是客户与CAS中的proxy module交互,proxy module与rehydration module交互。
2.1 理解Frontend中的proxy module
不同的服务有不同的proxy module,例如我们访问ews服务,对应的proxy module就是EwsProxyRequestHandler,EwsProxyRequestHandler和其他对应服务的handler都是继承了ProxyRequestHandler,然后实现自己的核心逻辑,例如,如何处理来自用户的HTTP请求,什么url要代理到Backend,以及如何与backend同步信息。
总的来说最重要的也就是这个父类ProxyRequestHandler了,因此我们好好分析一下他。根据ProxyRequestHandler.cs的代码,我们可以将其分为三部分:Request section、proxy section、Response section。
2.1.1 分析ProxyRequestHandler-Request section
Request Section是用来解析来自client的http请求的,它会接受请求并http请求是否合法,如果合法则会保留请求并添加客户端的身份信息到X-CommonAccessToken头中。
Frontend根Backend都是依靠http header来同步信息和代理内部状态的。因此,Exchange定义了一个黑名单,以避免一些内部Headers被误用。
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());
}
也就意味着出现上述任何一个http header,这个http请求都不会被转发给backend。
在request的最后一步,会调用一个叫做AddProtocolSpecificHeadersToServerRequest
的函数来加上一些需要与Backend进行交互的信息,并且会将当前用户的身份信息序列化后放到一个名为X-CommonAccessToken的http头中
,例如,如果我使用名称 Orange 登录 Outlook Web Access (OWA),则X-CommonAccessToken
前端到后端的代理将是:
2.1.2 分析ProxyRequestHandler-Proxy Section
首先会使用GetTargetBackendServerURL
函数来确定Request section阶段得到的格式化的http请求应该被转发给backend的哪个url。然后使用CreateServerRequest
函数来初始化一个新的http请求,具体就是给请求中加上一些http头并填入相关信息,其中有一个比较重要的是Authorization
头,这个头的存在是为了阻止未认证用户直接访问Backend,这个头的内容是Kerberos ticket。通过这个头,当请求被传输给backend的时候,backend就可以判断请求是否有效。
综上,发送给backend的http请求中最重要的两个http头是authorization与x-CommonAccessToken,前者代表这个请求时合法的,后者代表访问者的身份信息。如果我们可以伪造这两个头则可以伪装人意用户的身份访问backend。
2.1.3 分析ProxyRequestHandler-Response Section
这一部分的作用是接收Backend发送过来的信息,并确定什么头或者cookie可以发送给frontend。
2.2 理解Backend中的Rehydration Module
这个模块被BackendRehydrationModule.cs中的代码实现。
当接收到ProxyRequestHandler发送过来的http请求的时候,Rehydration Module首先会使用IsAuthenticated
函数判断这个请求是否经过身份认证,然后判断是否有一个叫做ms-Exch-EPI-Token-Serialization
的扩张权限,当两者都通过验证的时候,才算验证成功。一般说来只有Exchange的服务账号(机器账号)才能通过验证。
通过验证后,Exhcange会将请求中的身份信息从Exchange机器账号的身份信息变为X-commonAccessToken中存储的身份信息。然后将其放入httpContext对象中,紧接着执行Backend中相关的其他业务逻辑。
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就是一个Http代理。
3. 攻击点
3.1 cve-2021-26855 SSRF漏洞
在HttpProxy\ProxyRequestHandler.cs中有一个函数GetTargetBackEndServerUrl,上面说过这个函数是用来生成要传递给backend的url,也就是最终被访问的url。
我们发现其中一个host是客户端可控的,具体是被客户端请求中cookie中的一个名为X-AnonResource-Backend-Cookie
的字段可控的。具体分隔方式是将X-AnonResource-Backend-Cookie的值用~进行分割,前面是fqdn,后面是version。
综上,只要我们控制X-AnonResource-Backend-Cookie字段,进而控制传递给backend的host,进而实现访问人意backend的api,也就形成了SSRF漏洞。
HttpProxy\ProxyRequestHandler.cs
protected virtual Uri GetTargetBackEndServerUrl() {
this.LogElapsedTime("E_TargetBEUrl");
Uri result;
try {
//这里就会调用下面HttpProxy\OwaResourceProxyRequestHandler.cs的代码生成urlAnchorMailbox类
UrlAnchorMailbox urlAnchorMailbox = this.AnchoredRoutingTarget.AnchorMailbox as UrlAnchorMailbox;
if (urlAnchorMailbox != null) {
result = urlAnchorMailbox.Url;
} else {
UriBuilder clientUrlForProxy = this.GetClientUrlForProxy();
clientUrlForProxy.Scheme = Uri.UriSchemeHttps;
// Host的值其实就是BackendServer也就是client发送的http请求中cookie中的X-AnonResource-Backend的值
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;
}
HttpProxy\OwaResourceProxyRequestHandler.cs
protected override AnchorMailbox ResolveAnchorMailbox() {
//将X-AnonResource-Backend赋值给httpCookie
HttpCookie httpCookie = base.ClientRequest.Cookies["X-AnonResource-Backend"];
if (httpCookie != null) {
//将httpcookie赋值给saveBackendServer
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);
}
//通过修改将savedbackendserver赋值给BackEndServer的值
return new ServerInfoAnchorMailbox(BackEndServer.FromString(this.savedBackendServer), this);
}
return new AnonymousAnchorMailbox(this);
}
3.2 CVE-2021-27065 认证后的任意文件写入
当我们访问的服务是/ECP且http请求的uri中资源的后缀合法(即以.skin等结尾,具体可看源码)的话,
就会使用BEResourceRequestHandler来进行url处理,在这种处理模式下,http头中的X-BEResource会对后端访问的真实url产生影响,及X-BEResource的值被~分隔,前面会变成host,后面是version。如下图,后端真正访问的其实是:444/ecp/proxyLogon.ecp#,而version是1:
接着利用autodiscover服务,获取administrator的LegacyDN、sid。
再使用proxyLogon获取ASP.NET_SessionId与msExchEcpCanary。
拥有以上参数后我们就可以伪造自己为administrator。
访问/ecpDDI/DDIService.svc服务来实现以administrator的身份来进行任意文件写入。
4. 参考文章
A New Attack Surface on MS Exchange Part 1 - ProxyLogon!