作者:HuanGMz@知道创宇404实验室
日期:2021年3月11日
近期Exchange 爆出了已被在野利用的高危利用链,CVE-2021-26855 和 CVE-2021-27065 就是其中涉及到的两个漏洞。
CVE-2021-26855 是一个SSRF 漏洞,问题出现在将客户端请求代理到服务端时,该漏洞可以获取用户的sid,实现了无交互攻击链中最重要的第一步。
CVE-2021-27065 则是一个写文件漏洞,虽然不能完全控制要写入的内容,但是文件名与路径可以任意设置。当我们以 .aspx 为后缀创建文件,并在文件中插入一句话木马时,可以实现远程控制。
该漏洞虽然只是个绕过安全验证的ssrf,但堪称近Exchange近两年比较重要的漏洞之一。因为与以往那些隔靴搔痒的RCE相比(需要基础用户权限),该漏洞提供了批量攻击的机会,这也是本次漏洞影响这么大的原因。
上图为利用该漏洞时发出的post请求包,其中有两处关键:
url: /ecp/target.js
Cookie: X-BEResource=name]@WIN-PDEIT81MJNQ.server.cd:444/autodiscover/autodiscover.xml?#~1941962753
我们带着以下几个问题去分析:
要想回答上面的问题,我们必须找到 target.js 是如何处理请求的。经过搜索,我们可以断定 target.js 不是一个实体文件,而是一个虚拟路径,那么处理该请求的代码位于哪里?
经过调试,我们可以得出以下调用过程:
Microsoft.Exchange.FrontEndHttpProxy.dll!Microsoft.Exchange.HttpProxy.ProxyModule.OnPostAuthorizeRequest()
首先进入了ProxyModule的 OnPostAuthorizeRequest() 函数,从名字来看,该函数用于对post请求进行安全验证。该函数又调用了 ProxyModule.OnPostAuthorizeInternal() 函数。
在OnPostAuthorizeInternal()函数里,调用了ProxyModule.SelectHandlerForUnauthenticatedRequest() 方法,该方法用于对 未安全验证的请求 选择 Handler (即处理函数),后续会调用该Handler 类的BeginProcessRequest函数进一步处理请求。 我们跟进去看他是根据什么选择Hanlder 的。
可以看到,对于不同的 ProtocolType ,使用不同的 RequestPathParser 函数进行判断,并生成了不同的 httpHandler。这个ProtocolType 不清楚来自哪里,应该是与我们请求的应用程序集有关,以 /ecp/target.js 进行请求,则ProtocolType 就是 Ecp。
在ProtocolType 为Ecp 的情况下,又进行了多次判断,其中最关键的就是 BEResourceRequestHandler.CanHandle() 函数。如果该函数判断为真,则使用 BEResourceRequestHandler 作为请求的 Handler。我们跟入 CanHandler 看一下。
可以看到,判断要素有两个,Cookies 中要有 X-BEResource 字段,且请求路径需要以 .axc 或 .css 或 .js 或 其他一些后缀进行结尾。只有这两个条件满足,才会选择 BEResourceRequestHandler 作为请求 Handler。
该Handler 会被设置给 context.RemapHandlerInstance 属性,并最终被赋值给了 context.Handler。
而后便会调用 Handler.BeginProcessRequest() 函数来对请求进行进一步处理。由于BEResourceRequestHandler 继承与 ProxyRequestHandler,所以最终调用了 ProxyRequestHandler.BeginProcessRequest() 函数。
ProxyRequestHandler 类的作用应是将指向 FrontEnd 的Http 请求,转发给 BackEnd。
在ProxyRequestHandler.BeginProcessRequest() 函数里,创建新线程调用了 ProxyRequestHandler.BeginCalculateTargetBackEnd() 函数,其作用是根据 Cookie 中的 X-BEResource 字段来判断与生成指向 BackEnd 的目标url。
BeginCalculateTargetBackEnd() 调用了 ProxyRequestHandler.InternalBeginCalculateTargetBackEnd() 函数。
InternalBeginCalculateTargetBackEnd() 调用了 BEResourceRequestHandler.ResolveAnchorMailbox() 函数。
如下:
可以看到,该函数直接提取出Cookie 中的 X-BEResource 字段,并用其生成 BackEndServe实例。查看 BackEndServer.FromString() 函数,会发现它直接 依据 '~' 符号切割 beresourceCookie 字符串,前半段作为 fqdn,后半段作为 version。所谓 fqdn 既是 "全限定域名" ,而邮件服务器中的fqdn 类似于这样: [email protected]。这个version 指的是 BackEndServer Version。
之后:
InternalBeginCalculateTargetBackEnd() 创建新线程调用了 ProxyRequestHandler.OnCalculateTargetBackEndCompleted() 函数。
OnCalculateTargetBackEndCompleted() 函数又调用了 BEResourceRequestHandler.InternalOnCalculateTargetBackEndCompleted() 函数。
。。。(省略好几个调用)
进入了 ProxyRequestHandler.BeginProxyRequest() 函数,该函数开始处理代理请求。
BeginProxyRequest 调用 GetTargetBackEndServerUrl() 来获取 之前给你 BackEnd 的url。
这段代码实例化了一个 UrlBuilder类,涉及三个关键属性,Scheme、Host 和 Port。
Schema 被设置为https;
Host 取自于 BackEndServer.Fqdn,看一下 Host.Set() :
如果fqdn 中包含 ':' 且不是以 '[' 开头,则使用 '[]' 来包住 fqdn。':' 用于指示目标 BackEnd Server 端口。
Port 依据 BackEndServer.Version。如果该Version 小于 E15MinVersion(即1941962752),则指定this.ProxyToDownLevel 为true, 且将port 将指定为443。
还记得我们第一张图里的 X-BeResource 吗:
name]@WIN-PDEIT81MJNQ.server.cd:444/autodiscover/autodiscover.xml?#~1941962753
其对应的三个字段下图:
但在给Post字段赋值完后会自动进行重新解析,变成下面这样:
我没有找到这个自动重新解析的原理。
在将上面三个属性赋值后,该函数就返回了 clientUrlForProxy.Uri,查看Uri 的get方法:
调用了 UriBuilder.ToString() 方法来取得最终的 指向BackEnd 的目标url。
在 ToString() 中对各个参数进行拼接,形成url。
至此我们就获得了一个指向BackEnd 的url,类似下面这样:
https://[name]@win-pdeit81mjnq.server.cd:444/autodiscover/autodiscover.xml?#]:443/ecp/target.js
经过上面的分析,我们回答了一开始的问题:
/ecp/target.js 不是必须的,它可以是其他的路径 /ecp/xxxxxxxx.png
X-BEResource 用于代理请求,其原本格式应该是 [fqdn]~BackEndServerVersion
BackEndServerVersion 应该大于1941962752,‘#’ 用于在有url请求参数时分隔参数。
而且我们知道了X-BEResource 实际上完全不需要 ']' 去闭合中括号,我们完全可以直接用‘[]’来将name 括起来,比如下面这样:
[name]@WIN-PDEIT81MJNQ.server.cd:444/autodiscover/autodiscover.xml?#~1941962754
[name]@WIN-PDEIT81MJNQ.server.cd:444/autodiscover/autodiscover.xml?&~1941962755
甚至如果你不需要特别指定端口号,你还可以使用下面的值:
WIN-PDEIT81MJNQ.server.cd/autodiscover/autodiscover.xml?#~1941962753
这样会导致以443 端口访问https://win-pdeit81mjnq.server.cd/autodiscover/autodiscover.xml?#:444/ecp/target.js
不过大多数的 BackEnd 站点需要用444端口访问,不指定端口可能会导致访问失败。
之后ProxyRequestHandler 就会向目标 BackEnd Url 发起请求,将来自客户端的请求代理给服务端。
需要注意的是,中间代理在 BeginProxyRequest() 函数中 调用 CreateServerRequest() 来创建指向服务端的请求,而该函数会间接调用 GenerateKerberosAuthHeader() 函数来 创建Kerberos 认证头部。这也是中间代理能够访问BackEnd Server 的一个重要原因。
不过还有一点我们没有解决,就是关于 BackEndServer Version 的问题。前面我们将其设置为1941962753 时,说是要大于 E15MinVersion。而且我看其他的分析中也说到,如果不 设置大于,会导致后面进行安全验证,进而导致失败。但我在 Exchange 2016 上观察,AddDownLevelProxyHeaders()函数的作用大致是面对低版本的Server ,添加额外的请求头部,如果没有安全验证,则不添加。并没有出现报错的情况。
后来我在Exchange 2013 上测试,发现了端倪:
在Exchange2013 上代码稍有不同,这里没有直接返回,而是会进入 if 分支,调用 GetSecurityIdentifier()。而我们的请求是未安全验证的,identity 中User属性为Null。所以当GetSecurityIdentifier() 返回 identity.User,并调用其 ToString() 方法时,会直接报错:"未将对象引用设置到对象的实例"。
该漏洞是一个写文件漏洞,其原理很简单。
在ecp 管理界面找到 关于虚拟目录的配置窗口:
可以看到,在OAB VirtualDirectory 的配置界面有多个字段可以配置,我么将其中的ExternUrl 中设置为一句话木马。
而后选择重置 虚拟目录:
可以看到,页面中说明会将当前设置存储在文件中,路径由我们自己设置,虽然示例给的文件名后缀是 .txt ,但实际上我们可以将其设置为 .aspx 。我们选择将设置保存在 C:\inetpub\wwwroot\myshell.aspx,这样做会导致当我们通过网络请求访问该文件时,服务器会以aspx 的格式对其进行解析。
注意:如果是从网页进行重置,需要使用unc路径,需要提前将目标文件夹进行网络共享。但是 如果通过 /ecp/DDI/DDIService.svc/SetObject 接口进行重置,则可以直接使用物理路径,会自动将对应文件夹进行网络共享。
选择重置后,我们查看对应位置:
可以看到,OAB VirtualDirectory 的配置信息已经被写入到目标位置,这其中藏着一句话木马,而且以 .aspx 为文件后缀。
该漏洞的官方补丁也很简单,直接强制要求重置配置文件时,新建的文件必须以 .txt 为后缀。
https://www.praetorian.com/blog/reproducing-proxylogon-exploit/
https://www.microsoft.com/security/blog/2021/03/02/hafnium-targeting-exchange-servers/
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1501/