组件架构

Exchange不同版本的组件架构并不相同,但总体上可以将其分为核心的邮箱服务器角色(Mailbox Role)和可选的边缘传输角色(Edge Transport Role)。

  • Exchange作为边缘传输角色时部署在内外网交界处,充当邮件安全网关

Exchange作为邮箱服务器角色时分为客户端访问服务(Client Access Services)和后端服务(Backend Services)部分,CAS负责校验用户身份并将请求反代至具体的后端服务。

CAS对应IIS中的Default Web Site监听在80和443端口,BS对应IIS中的Exchange Back End监听在81和444端口。

出于解耦和兼容考虑,各个功能被封装为多个模块,有如下常用功能(缩写名对应URL访问路径):

  • OWA(Outlook Web App)
  • ECP(Exchange Control Panel)
  • EWS(Exchange Web Service)
  • Autodiscover
  • MAPI(Messaging Application Programming Interface)
  • EAS(Exchange ActiveSync)
  • OAB(Offline Address Books)
  • PowerShell

影响版本

  • Exchange Server 2013 < Mar21SU
  • Exchange Server 2016 < Mar21SU < CU20
  • Exchange Server 2019 < Mar21SU < CU9

补丁分析

根据微软官方通告可以知道ProxyLogon漏洞的补丁编号为KB5000871,也可以看到此补丁的前置补丁编号为KB4602269,将两个msp补丁文件下载下来并通过7z解压得到多个dll。

下载dnSpy用于反编译和调试C#的dll文件。由于我们并不是要调试二进制洞,为了避免干扰需要取消勾选View->Options->Decompiler->ILSpy->Show tokens, RVAs and file offsets。将解压出的dll拖入dnSpy并选中高亮,通过File->Export to Project就可以得到反编译后的工程文件。

拿到补丁前后两份反编译的源码后,在低版本源码文件夹建立git目录,再将高版本源码文件覆盖过来:

1
2
3
4
5
6
7
cd kb4602269
git init
git add .
git commit -m 'init'

# Bypass Alias cp='cp -i'
/usr/bin/cp -r ../kb5000871/* ./

这样就能在任意支持git管理的IDE中方便地进行补丁对比了(比如VSCode),小缺点就是有的整个文件就一点无关紧要的字符变化而已(之前对比vCenter时也是),而我们显然只是想关注一些函数和流程的变动,所以之后也许可以结合页面相似度之类的算法再筛一遍,现阶段可以用批量替换的办法凑合。

CVE-2021-26855

Microsoft.Exchange.FrontEndHttpProxy未有效校验Cookie中用户可控的X-BEResource值,后续处理中结合.NET的UriBuilder类特性造成SSRF。

漏洞分析

CVE-2021-26855 is a server-side request forgery (SSRF) vulnerability in Exchange which allowed the attacker to send arbitrary HTTP requests and authenticate as the Exchange server.

微软通告说这是一个以Exchange服务器作为身份认证的SSRF漏洞,说明肯定涉及到了NTLM/Kerberos认证,再结合Volexity捕获到的相关访问路径来看,定位到Microsoft.Exchange.FrontEndHttpProxy相关的代码变动:

ProxyRequestHandler类是CAS反代过程中,负责处理用户请求与后端响应的一个承前启后的组件。因为函数调用关系比较复杂,为了避免看上去一团乱麻,所以在具体分析某个方法作用前,先从广度上列出从收到用户请求开始几个主线的方法调用栈。

1
2
3
4
5
6
7
8
9
10
// Microsoft.Exchange.FrontEndHttpProxy
public class ProxyModule : IHttpModule
public void Init(HttpApplication application)
private void OnPostAuthorizeRequest(object sender, EventArgs e)
protected virtual void OnPostAuthorizeInternal(HttpApplication httpApplication)

private IHttpHandler SelectHandlerForUnauthenticatedRequest(HttpContext httpContext)

HttpContext context = httpApplication.Context;
context.RemapHandler(httpHandler);

当请求路径为/ecp/时,会通过IsResourceRequest方法判断文件后缀名:

1
2
3
4
5
// Microsoft.Exchange.FrontEndHttpProxy
internal class BEResourceRequestHandler : ProxyRequestHandler
internal static bool CanHandle(HttpRequest httpRequest)
private static string GetBEResouceCookie(HttpRequest httpRequest)
internal static bool IsResourceRequest(string localPath)

通过判断后由BeginProcessRequest方法继续处理后续流程:

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
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
private void InternalBeginCalculateTargetBackEnd(out AnchorMailbox anchorMailbox)

protected override AnchorMailbox ResolveAnchorMailbox()
public ServerInfoAnchorMailbox(BackEndServer backendServer, IRequestContext requestContext)
public static BackEndServer FromString(string input)

private void OnCalculateTargetBackEndCompleted(object extraData)
private void InternalOnCalculateTargetBackEndCompleted(TargetCalculationCallbackBeacon beacon)
private void BeginValidateBackendServerCacheOrProxyOrRecalculate()
protected void BeginProxyRequestOrRecalculate()
protected void BeginProxyRequest(object extraData)

protected virtual Uri GetTargetBackEndServerUrl()

protected HttpWebRequest CreateServerRequest(Uri targetUrl)
protected void PrepareServerRequest(HttpWebRequest serverRequest)

internal static string KerberosUtilities.GenerateKerberosAuthHeader(...)

private void CopyHeadersToServerRequest(HttpWebRequest destination)
protected virtual bool ShouldCopyHeaderToServerRequest(string headerName)

private void CopyCookiesToServerRequest(HttpWebRequest serverRequest)

protected virtual void SetProtocolSpecificServerRequestParameters(HttpWebRequest serverRequest)
protected virtual void AddProtocolSpecificHeadersToServerRequest(WebHeaderCollection headers)

private void BeginGetServerResponse()
private static void ResponseReadyCallback(IAsyncResult result)
private void OnResponseReady(object extraData)
private void ProcessResponse(WebException exception)
private void CopyHeadersToClientResponse()
private void CopyCookiesToClientResponse()

BackEndServer.FromString方法获取Cookie的X-BEResource值中,以波浪线分隔开的FQDN和version,而且涉及一处补丁变更:

这里的值可以由Cookie控制,调用FromStringResolveAnchorMailbox方法也有补丁变更,基本可以说明漏洞点就在这附近了。果然随后的GetTargetBackEndServerUrl方法就把Fqdn赋值给了UriBuilder对象Host属性:

1
2
3
4
protected virtual UriBuilder GetClientUrlForProxy()
{
return new UriBuilder(this.ClientRequest.Url);
}

UriBuilder是一个.NET类,在微软的Reference Source找到源码。如果传入的Host中存在:冒号,并且不是[开头,就用一对中括号将值包裹起来。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public string Host {
get {
return m_host;
}
set {
if (value == null) {
value = String.Empty;
}
m_host = value;
//probable ipv6 address -
if (m_host.IndexOf(':') >= 0) {
//set brackets
if (m_host[0] != '[')
m_host = "[" + m_host + "]";
}
m_changed = true;
}
}

根据传入的version是否大于Server.E15MinVersion(1941962752),将Port赋值为444或443。最后由Uri属性的get访问器(accessor)调用ToString将各部分拼接还原:

1
2
3
4
5
6
7
8
9
10
public Uri Uri {
get {
if (m_changed) {
m_uri = new Uri(ToString());
SetFieldsFromUri(m_uri);
m_changed = false;
}
return m_uri;
}
}

得到后端URL之后继续处理请求头,将GenerateKerberosAuthHeader方法生成的Kerberos票据放入Authorization请求头。CopyHeadersToServerRequest方法会筛选出后端需要的请求头,其中ShouldCopyHeaderToServerRequest方法用来过滤一些自定义请求头:

最后AddProtocolSpecificHeadersToServerRequest方法会将序列化得到的用于标识用户身份的Token,放入X-CommonAccessToken请求头中:

相应的,后端模块会由AllowsTokenSerializationBy方法校验通常机器用户才有的ms-Exch-EPI-Token-Serialization扩展权限(验证请求由CAS发出),随后反序列化还原X-CommonAccessToken请求头的身份标识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Microsoft.Exchange.Security.Authentication
public class BackendRehydrationModule : IHttpModule
public void Init(HttpApplication application)
private void OnAuthenticateRequest(object source, EventArgs args)
private void ProcessRequest(HttpContext httpContext)
private bool TryGetCommonAccessToken(HttpContext httpContext, Stopwatch stopwatch, out CommonAccessToken token)

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

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

小结一下,Cookie的X-BEResource值可以控制CAS请求的Host,结合UriBuilder类特性可以构造出可控的完整URL,因为采用Kerberos认证所以不能向任意站点发起请求:

X-FEServer响应头的值就是计算机名,可以用它构造URL请求后端服务:

  • Exchange2013需要将Version设置为大于1941962752的值

CVE-2021-27065

Microsoft.Exchange.Management.DDIService.WriteFileActivity未校验写文件后缀,可由文件内容部分可控的相关功能写入WebShell。

利用流程

Microsoft.Exchange.Management.DDIService.WriteFileActivity中有一处明显的补丁变动,使得文件后缀名只能为txt。

1
private static readonly string textExtension = ".txt";

ResetOABVirtualDirectory触发点为例,利用流程如下(均通过SSRF发起):

  1. 请求EWS,从X-CalculatedBETarget响应头获取后端域名

  1. 爆破邮箱用户名,请求Autodiscover获取配置中的LegacyDN

  1. MAPI over HTTP请求引发Microsoft.Exchange.RpcClientAccess.Server.LoginPermException,获取SID

  1. 替换尾部RID为500,伪造管理员SID,由ProxyLogonHandler获取管理员身份ASP.NET_SessionIdmsExchEcpCanary

  1. 通过DDI组件Getlist接口获取RawIdentity(GetObject接口有时候返回NULL)

  1. 利用外部URL虚拟路径属性引入WebShell

  1. 最后触发重置时的备份功能,将文件写入指定的UNC目录

  • WebShell的内容需要规避会被URL编码的特殊字符,且字符长度不能超过255

下一篇我们将一起讨论ProxyOracle~

参考链接

Exchange architecture

Web services reference for Exchange

HAFNIUM targeting Exchange Servers with 0-day exploits

Description of the security update for Microsoft Exchange Server 2019, 2016, and 2013: March 2, 2021 (KB5000871)

Operation Exchange Marauder: Active Exploitation of Multiple Zero-Day Microsoft Exchange Vulnerabilities

Reproducing the Microsoft Exchange Proxylogon Exploit Chain

Phân tích lỗ hổng ProxyLogon — Mail Exchange RCE (Sự kết hợp hoàn hảo CVE-2021–26855 + CVE-2021–27065)

A New Attack Surface on MS Exchange Part 1 - ProxyLogon!

Attacking MS Exchange Web Interfaces