ajpy-0.0.4/0000755000175000017500000000000013251440232013136 5ustar hypn0shypn0s00000000000000ajpy-0.0.4/ajpy/0000755000175000017500000000000013251440232014101 5ustar hypn0shypn0s00000000000000ajpy-0.0.4/ajpy/__init__.py0000644000175000017500000000000012731012036016177 0ustar hypn0shypn0s00000000000000ajpy-0.0.4/ajpy/ajp.py0000644000175000017500000002722113247270054015241 0ustar hypn0shypn0s00000000000000# Copyright (c) 2018, Julien Legras # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. All advertising materials mentioning features or use of this software # must display the following acknowledgement: # This product includes software developed by the . # 4. Neither the name of the nor the # names of its contributors may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY SYNACKTIV ''AS IS'' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL SYNACKTIV BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import struct # Some references: # https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html #global helpers def pack_string(s): if s is None: return struct.pack(">h", -1) l = len(s) return struct.pack(">H%dsb" % l, l, s, 0) def unpack(stream, fmt): size = struct.calcsize(fmt) buf = stream.read(size) return struct.unpack(fmt, buf) def unpack_string(stream): size, = unpack(stream, ">h") if size == -1: # null string return None res, = unpack(stream, "%ds" % size) stream.read(1) # \0 return res class NotFoundException(Exception): pass class AjpBodyRequest(object): # server == web server, container == servlet SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2) MAX_REQUEST_LENGTH = 8186 def __init__(self, data_stream, data_len, data_direction=None): self.data_stream = data_stream self.data_len = data_len self.data_direction = data_direction def serialize(self): data = self.data_stream.read(AjpBodyRequest.MAX_REQUEST_LENGTH) if len(data) == 0: return struct.pack(">bbH", 0x12, 0x34, 0x00) else: res = "" res += struct.pack(">H", len(data)) res += data if self.data_direction == AjpBodyRequest.SERVER_TO_CONTAINER: header = struct.pack(">bbH", 0x12, 0x34, len(res)) else: header = struct.pack(">bbH", 0x41, 0x42, len(res)) return header + res def send_and_receive(self, socket, stream): while True: data = self.serialize() socket.send(data) r = AjpResponse.receive(stream) while r.prefix_code != AjpResponse.GET_BODY_CHUNK and r.prefix_code != AjpResponse.SEND_HEADERS: r = AjpResponse.receive(stream) if r.prefix_code == AjpResponse.SEND_HEADERS or len(data) == 4: break class AjpForwardRequest(object): """ 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 """ _, 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) REQUEST_METHODS = {'GET': GET, 'POST': POST, 'HEAD': HEAD, 'OPTIONS': OPTIONS, 'PUT': PUT, 'DELETE': DELETE, 'TRACE': TRACE} # server == web server, container == servlet SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2) COMMON_HEADERS = ["SC_REQ_ACCEPT", "SC_REQ_ACCEPT_CHARSET", "SC_REQ_ACCEPT_ENCODING", "SC_REQ_ACCEPT_LANGUAGE", "SC_REQ_AUTHORIZATION", "SC_REQ_CONNECTION", "SC_REQ_CONTENT_TYPE", "SC_REQ_CONTENT_LENGTH", "SC_REQ_COOKIE", "SC_REQ_COOKIE2", "SC_REQ_HOST", "SC_REQ_PRAGMA", "SC_REQ_REFERER", "SC_REQ_USER_AGENT" ] 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"] def __init__(self, data_direction=None): self.prefix_code = 0x02 self.method = None self.protocol = None self.req_uri = None self.remote_addr = None self.remote_host = None self.server_name = None self.server_port = None self.is_ssl = None self.num_headers = None self.request_headers = None self.attributes = None self.data_direction = data_direction def pack_headers(self): """ req_header_name := sc_req_header_name | (string) [see below for how this is parsed] sc_req_header_name := 0xA0xx (integer) req_header_value := (string) accept 0xA001 SC_REQ_ACCEPT accept-charset 0xA002 SC_REQ_ACCEPT_CHARSET accept-encoding 0xA003 SC_REQ_ACCEPT_ENCODING accept-language 0xA004 SC_REQ_ACCEPT_LANGUAGE authorization 0xA005 SC_REQ_AUTHORIZATION connection 0xA006 SC_REQ_CONNECTION content-type 0xA007 SC_REQ_CONTENT_TYPE content-length 0xA008 SC_REQ_CONTENT_LENGTH cookie 0xA009 SC_REQ_COOKIE cookie2 0xA00A SC_REQ_COOKIE2 host 0xA00B SC_REQ_HOST pragma 0xA00C SC_REQ_PRAGMA referer 0xA00D SC_REQ_REFERER user-agent 0xA00E SC_REQ_USER_AGENT store headers as dict """ self.num_headers = len(self.request_headers) res = "" res += struct.pack(">h", self.num_headers) for h_name in self.request_headers: if h_name.startswith("SC_REQ"): code = AjpForwardRequest.COMMON_HEADERS.index(h_name) + 1 res += struct.pack("BB", 0xA0, code) else: res += pack_string(h_name) res += pack_string(self.request_headers[h_name]) return res def pack_attributes(self): """ Information Code Value Note ?context 0x01 Not currently implemented ?servlet_path 0x02 Not currently implemented ?remote_user 0x03 ?auth_type 0x04 ?query_string 0x05 ?route 0x06 ?ssl_cert 0x07 ?ssl_cipher 0x08 ?ssl_session 0x09 ?req_attribute 0x0A Name (the name of the attribut follows) ?ssl_key_size 0x0B ?secret 0x0C ?stored_method 0x0D are_done 0xFF request_terminator """ res = "" for attr in self.attributes: a_name = attr['name'] code = AjpForwardRequest.ATTRIBUTES.index(a_name) + 1 res += struct.pack("b", code) if a_name == "req_attribute": aa_name, a_value = attr['value'] res += pack_string(aa_name) res += pack_string(a_value) else: res += pack_string(attr['value']) res += struct.pack("B", 0xFF) return res def serialize(self): res = "" res += struct.pack("bb", self.prefix_code, self.method) res += pack_string(self.protocol) res += pack_string(self.req_uri) res += pack_string(self.remote_addr) res += pack_string(self.remote_host) res += pack_string(self.server_name) res += struct.pack(">h", self.server_port) res += struct.pack("?", self.is_ssl) res += self.pack_headers() res += self.pack_attributes() if self.data_direction == AjpForwardRequest.SERVER_TO_CONTAINER: header = struct.pack(">bbh", 0x12, 0x34, len(res)) else: header = struct.pack(">bbh", 0x41, 0x42, len(res)) return header + res def parse(self, raw_packet): stream = StringIO(raw_packet) self.magic1, self.magic2, data_len = unpack(stream, "bbH") self.prefix_code, self.method = unpack(stream, "bb") self.protocol = unpack_string(stream) self.req_uri = unpack_string(stream) self.remote_addr = unpack_string(stream) self.remote_host = unpack_string(stream) self.server_name = unpack_string(stream) self.server_port = unpack(stream, ">h") self.is_ssl = unpack(stream, "?") self.num_headers, = unpack(stream, ">H") self.request_headers = {} for i in range(self.num_headers): code, = unpack(stream, ">H") if code > 0xA000: h_name = AjpForwardRequest.COMMON_HEADERS[code - 0xA001] else: h_name = unpack(stream, "%ds" % code) stream.read(1) # \0 h_value = unpack_string(stream) self.request_headers[h_name] = h_value def send_and_receive(self, socket, stream, save_cookies=False): res = [] i = socket.sendall(self.serialize()) if self.method == AjpForwardRequest.POST: return res r = AjpResponse.receive(stream) assert r.prefix_code == AjpResponse.SEND_HEADERS res.append(r) if save_cookies and 'Set-Cookie' in r.response_headers: self.headers['SC_REQ_COOKIE'] = r.response_headers['Set-Cookie'] # read body chunks and end response packets while True: r = AjpResponse.receive(stream) res.append(r) if r.prefix_code == AjpResponse.END_RESPONSE: break elif r.prefix_code == AjpResponse.SEND_BODY_CHUNK: continue else: raise NotImplementedError break return res class AjpResponse(object): """ AJP13_SEND_BODY_CHUNK := prefix_code 3 chunk_length (integer) chunk *(byte) AJP13_SEND_HEADERS := prefix_code 4 http_status_code (integer) http_status_msg (string) num_headers (integer) response_headers *(res_header_name header_value) res_header_name := sc_res_header_name | (string) [see below for how this is parsed] sc_res_header_name := 0xA0 (byte) header_value := (string) AJP13_END_RESPONSE := prefix_code 5 reuse (boolean) AJP13_GET_BODY_CHUNK := prefix_code 6 requested_length (integer) """ # prefix codes _,_,_,SEND_BODY_CHUNK, SEND_HEADERS, END_RESPONSE, GET_BODY_CHUNK = range(7) # send headers codes COMMON_SEND_HEADERS = [ "Content-Type", "Content-Language", "Content-Length", "Date", "Last-Modified", "Location", "Set-Cookie", "Set-Cookie2", "Servlet-Engine", "Status", "WWW-Authenticate" ] def parse(self, stream): # read headers self.magic, self.data_length, self.prefix_code = unpack(stream, ">HHb") if self.prefix_code == AjpResponse.SEND_HEADERS: self.parse_send_headers(stream) elif self.prefix_code == AjpResponse.SEND_BODY_CHUNK: self.parse_send_body_chunk(stream) elif self.prefix_code == AjpResponse.END_RESPONSE: self.parse_end_response(stream) elif self.prefix_code == AjpResponse.GET_BODY_CHUNK: self.parse_get_body_chunk(stream) else: raise NotImplementedError def parse_send_headers(self, stream): self.http_status_code, = unpack(stream, ">H") self.http_status_msg = unpack_string(stream) self.num_headers, = unpack(stream, ">H") self.response_headers = {} for i in range(self.num_headers): code, = unpack(stream, ">H") if code <= 0xA000: # custom header h_name, = unpack(stream, "%ds" % code) stream.read(1) # \0 h_value = unpack_string(stream) else: h_name = AjpResponse.COMMON_SEND_HEADERS[code-0xA001] h_value = unpack_string(stream) self.response_headers[h_name] = h_value def parse_send_body_chunk(self, stream): self.data_length, = unpack(stream, ">H") self.data = stream.read(self.data_length+1) def parse_end_response(self, stream): self.reuse, = unpack(stream, "b") def parse_get_body_chunk(self, stream): rlen, = unpack(stream, ">H") return rlen @staticmethod def receive(stream): r = AjpResponse() r.parse(stream) return r ajpy-0.0.4/PKG-INFO0000644000175000017500000000153613251440232014240 0ustar hypn0shypn0s00000000000000Metadata-Version: 1.1 Name: ajpy Version: 0.0.4 Summary: AJP package crafting library Home-page: https://github.com/hypn0s/AJPy/ Author: Julien Legras Author-email: julien.legras@synacktiv.com License: UNKNOWN Download-URL: https://github.com/hypn0s/AJPy/archive/master.zip Description-Content-Type: UNKNOWN Description: AJPy aims to craft AJP requests in order to communicate with AJP connectors. Keywords: ajp,java,network Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Other Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: POSIX Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Security Classifier: Topic :: System :: Networking ajpy-0.0.4/README.md0000644000175000017500000000611413245741042014425 0ustar hypn0shypn0s00000000000000# Intro AJPy aims to craft AJP requests in order to communicate with AJP connectors. Reference documentation: https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html # Tools At the moment, only one tool is provided for Tomcat with the following modules: * version fingerprint ``` $ python tomcat.py version 172.17.0.2 Apache Tomcat/8.0.35 ``` * authentication bruteforce ``` $ python tomcat.py -v bf -U tomcat_mgr_default_users.txt -P tomcat_mgr_default_pass.txt /manager/html 172.17.0.2 [2016-06-10 17:24:55.965] INFO Attacking a tomcat at ajp13://172.17.0.2:8009/manager/html [2016-06-10 17:24:56.017] DEBUG testing admin:admin [2016-06-10 17:24:56.069] INFO Found valid credz: admin:admin [2016-06-10 17:24:56.069] INFO Here is your cookie: JSESSIONID=1267BE97BFB5BFAEAFAAD76EE648FE06; Path=/manager/; HttpOnly [2016-06-10 17:24:56.069] DEBUG testing admin:manager [2016-06-10 17:24:56.152] DEBUG testing admin:role1 [2016-06-10 17:24:56.154] DEBUG testing admin:root [2016-06-10 17:24:56.155] DEBUG testing admin:tomcat [2016-06-10 17:24:56.157] DEBUG testing manager:admin [2016-06-10 17:24:56.158] DEBUG testing manager:manager [2016-06-10 17:24:56.159] DEBUG testing manager:role1 [2016-06-10 17:24:56.160] DEBUG testing manager:root [2016-06-10 17:24:56.161] DEBUG testing manager:tomcat [2016-06-10 17:24:56.164] DEBUG testing role1:admin [2016-06-10 17:24:56.164] DEBUG testing role1:manager [2016-06-10 17:24:56.165] DEBUG testing role1:role1 [2016-06-10 17:24:56.166] DEBUG testing role1:root [2016-06-10 17:24:56.167] DEBUG testing role1:tomcat [2016-06-10 17:24:56.169] DEBUG testing root:admin [2016-06-10 17:24:56.170] DEBUG testing root:manager [2016-06-10 17:24:56.171] DEBUG testing root:role1 [2016-06-10 17:24:56.172] DEBUG testing root:root [2016-06-10 17:24:56.173] DEBUG testing root:tomcat [2016-06-10 17:24:56.175] DEBUG testing tomcat:admin [2016-06-10 17:24:56.175] DEBUG testing tomcat:manager [2016-06-10 17:24:56.176] DEBUG testing tomcat:role1 [2016-06-10 17:24:56.177] DEBUG testing tomcat:root [2016-06-10 17:24:56.178] DEBUG testing tomcat:tomcat [2016-06-10 17:24:56.184] INFO Found valid credz: tomcat:tomcat [2016-06-10 17:24:56.184] INFO Here is your cookie: JSESSIONID=9944126F31E428B8847AFEBF2307BB09; Path=/manager/; HttpOnly [2016-06-10 17:24:56.184] DEBUG testing tomcat:sstic2016 [2016-06-10 17:24:56.186] DEBUG testing both:admin [2016-06-10 17:24:56.187] DEBUG testing both:manager [2016-06-10 17:24:56.188] DEBUG testing both:role1 [2016-06-10 17:24:56.189] DEBUG testing both:root [2016-06-10 17:24:56.190] DEBUG testing both:tomcat [2016-06-10 17:24:56.191] DEBUG Closing socket... ``` * WAR upload ``` $ python tomcat.py upload -u tomcat -p tomcat webshell.war 172.17.0.2 ``` * WAR undeploy ``` $ python tomcat.py undeploy -u tomcat -p tomcat /webshell 172.17.0.2 ``` * Application listing ``` $ python tomcat.py list -u tomcat -p tomcat 172.17.0.2 ``` # Thanks * @MrTchuss for the Tomcat WAR upload fix * @kalidor for the Tomcat WAR undeploy and application listing ajpy-0.0.4/ajpy.egg-info/0000755000175000017500000000000013251440232015573 5ustar hypn0shypn0s00000000000000ajpy-0.0.4/ajpy.egg-info/SOURCES.txt0000644000175000017500000000025113251440230017453 0ustar hypn0shypn0s00000000000000README.md setup.cfg setup.py ajpy/__init__.py ajpy/ajp.py ajpy.egg-info/PKG-INFO ajpy.egg-info/SOURCES.txt ajpy.egg-info/dependency_links.txt ajpy.egg-info/top_level.txtajpy-0.0.4/ajpy.egg-info/PKG-INFO0000644000175000017500000000153613251440230016673 0ustar hypn0shypn0s00000000000000Metadata-Version: 1.1 Name: ajpy Version: 0.0.4 Summary: AJP package crafting library Home-page: https://github.com/hypn0s/AJPy/ Author: Julien Legras Author-email: julien.legras@synacktiv.com License: UNKNOWN Download-URL: https://github.com/hypn0s/AJPy/archive/master.zip Description-Content-Type: UNKNOWN Description: AJPy aims to craft AJP requests in order to communicate with AJP connectors. Keywords: ajp,java,network Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Other Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: POSIX Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Security Classifier: Topic :: System :: Networking ajpy-0.0.4/ajpy.egg-info/dependency_links.txt0000644000175000017500000000000113251440230021637 0ustar hypn0shypn0s00000000000000 ajpy-0.0.4/ajpy.egg-info/top_level.txt0000644000175000017500000000000513251440230020316 0ustar hypn0shypn0s00000000000000ajpy ajpy-0.0.4/setup.cfg0000644000175000017500000000011713251440232014756 0ustar hypn0shypn0s00000000000000[metadata] description-file = README.md [egg_info] tag_build = tag_date = 0 ajpy-0.0.4/setup.py0000644000175000017500000000165113251440165014660 0ustar hypn0shypn0s00000000000000from setuptools import setup setup( name = "ajpy", packages = ["ajpy"], version = "0.0.4", description = "AJP package crafting library", author = "Julien Legras", author_email = "julien.legras@synacktiv.com", url = "https://github.com/hypn0s/AJPy/", download_url = "https://github.com/hypn0s/AJPy/archive/master.zip", keywords = ["ajp", "java", "network"], classifiers = [ "Programming Language :: Python", "Development Status :: 3 - Alpha", "Environment :: Other Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: POSIX", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Security", "Topic :: System :: Networking", ], long_description = """\ AJPy aims to craft AJP requests in order to communicate with AJP connectors. """ )