Skip to content

Commit 6c77f74

Browse files
committed
Fixed showing weak ciphers in ssl_version scan
1 parent cfaaa16 commit 6c77f74

File tree

1 file changed

+152
-181
lines changed

1 file changed

+152
-181
lines changed

modules/auxiliary/scanner/ssl/ssl_version.rb

Lines changed: 152 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
##
55

66
class MetasploitModule < Msf::Auxiliary
7-
87
include Msf::Exploit::Remote::Tcp
98
include Msf::Auxiliary::Scanner
109
include Msf::Auxiliary::Report
@@ -40,7 +39,7 @@ def initialize
4039
# poodle
4140
[ 'URL', 'https://security.googleblog.com/2014/10/this-poodle-bites-exploiting-ssl-30.html' ],
4241
[ 'CVE', '2014-3566' ],
43-
[ 'URL', 'https://www.openssl.org/~bodo/ssl-poodle.pdf' ],
42+
[ 'URL', 'http://web.archive.org/web/20240319071045/https://www.openssl.org/~bodo/ssl-poodle.pdf' ],
4443
# TLS v1.0 and v1.1 depreciation
4544
[ 'URL', 'https://datatracker.ietf.org/doc/rfc8996/' ],
4645
# SSLv2 deprecation
@@ -56,7 +55,7 @@ def initialize
5655
# BEAST
5756
[ 'CVE', '2011-3389' ],
5857
# RC4
59-
[ 'URL', 'http://www.isg.rhul.ac.uk/tls/' ],
58+
[ 'URL', 'http://web.archive.org/web/20240607160328/https://www.isg.rhul.ac.uk/tls/' ],
6059
[ 'CVE', '2013-2566' ],
6160
# LOGJAM
6261
[ 'CVE', '2015-4000' ],
@@ -81,80 +80,6 @@ def initialize
8180
)
8281
end
8382

84-
def get_metasploit_ssl_versions
85-
# There are two ways to generate a list of valid SSL Versions (SSLv3, TLS1.1, etc) and cipher suites (AES256-GCM-SHA384,
86-
# ECDHE-RSA-CHACHA20-POLY1305, etc). The first would be to generate them independently. It's possible to
87-
# pull all SSLContext methods (SSL Versions) via OpenSSL::SSL::SSLContext::METHODS here, as referenced in
88-
# https://github.com/rapid7/rex-socket/blob/6ea0bb3b4e19c53d73e4337617be72c0ed351ceb/lib/rex/socket/ssl_tcp.rb#L46
89-
# then pull all ciphers with OpenSSL::Cipher.ciphers. Now in theory you have a nice easy loop:
90-
#
91-
# OpenSSL::SSL::SSLContext::METHODS.each do |ssl_version|
92-
# OpenSSL::Cipher.ciphers.each do |cipher_suite|
93-
# # do something
94-
# end
95-
# end
96-
#
97-
# However, in practice we find that OpenSSL::SSL::SSLContext::METHODS includes '_client' and '_server' variants
98-
# such as :TLSv1, :TLSv1_client, :TLSv1_server. In this case, we only need :TLSv1, so we need to remove ~2/3 of the list.
99-
#
100-
# Next, we'll find that many ciphers in OpenSSL::Cipher.ciphers are not applicable for various SSL versions.
101-
# The loop we previously looked at has (at the time of writing on Kali Rollin, msf 6.2.23) 3060 rounds.
102-
# This is a lot of iterations when we already know there are many combinations that will not be applicable for our
103-
# use. Luckily there is a 2nd way which is much more efficient.
104-
#
105-
# The OpenSSL library includes https://docs.ruby-lang.org/en/2.4.0/OpenSSL/SSL/SSLContext.html#method-i-ciphers
106-
# which we can use to generate a list of all ciphers, and SSL versions they work with. The structure is:
107-
#
108-
# [[name, version, bits, alg_bits], ...]
109-
#
110-
# which makes it very easy to just pull the 2nd element (version, or SSL version) from each list item, and unique it.
111-
# This gives us the list of all SSL versions which also have at least one working cipher on our system.
112-
# Using this method we produce no unusable SSL versions or matching cipher suites and the list is 60 items long, so 1/51 the size.
113-
# Later in get_metasploit_ssl_cipher_suites, we can grab all cipher suites to a SSL version easily by simply filtering
114-
# the 2nd element (version, or SSL version) from each list item.
115-
116-
if datastore['SSLVersion'] == 'All'
117-
return Array.new(OpenSSL::SSL::SSLContext.new.ciphers.length) { |i| (OpenSSL::SSL::SSLContext.new.ciphers[i][1]).to_s }.uniq.reverse
118-
end
119-
120-
[datastore['SSLVersion']]
121-
end
122-
123-
def get_metasploit_ssl_cipher_suites(ssl_version)
124-
# See comments in get_metasploit_ssl_versions for details on the use of
125-
# OpenSSL::SSL::SSLContext.new.ciphers vs other methods to generate
126-
# valid ciphers for a given SSL version
127-
128-
# First find all valid ciphers that the Metasploit host supports.
129-
# Also transform the SSL version to a standard format.
130-
ssl_version = ssl_version.to_s.gsub('_', '.')
131-
all_ciphers = OpenSSL::SSL::SSLContext.new.ciphers
132-
valid_ciphers = []
133-
134-
# For each cipher that the Metasploit host supports, determine if that cipher
135-
# is supported for use with the SSL version passed into this function. If it is,
136-
# then add it to the valid_ciphers list.
137-
all_ciphers.each do |cipher|
138-
# cipher list has struct of [cipher, ssl_version, <int>, <int>]
139-
if cipher[1] == ssl_version
140-
valid_ciphers << cipher[0]
141-
end
142-
end
143-
144-
# If the user wants to use all ciphers then return all valid ciphers.
145-
# Otherwise return only the one that matches the one the user specified
146-
# in the SSLCipher datastore option.
147-
#
148-
# If no match is found for some reason then we will return an empty array.
149-
if datastore['SSLCipher'] == 'All'
150-
return valid_ciphers
151-
elsif valid_ciphers.contains? datastore['SSLCipher']
152-
return [datastore['SSLCipher']]
153-
end
154-
155-
[]
156-
end
157-
15883
def public_key_size(cert)
15984
if cert.public_key.respond_to? :n
16085
return cert.public_key.n.num_bytes * 8
@@ -241,6 +166,22 @@ def print_cert(cert, ip)
241166
end
242167
end
243168

169+
# Process certificate with enhanced analysis
170+
def process_certificate(ip, cert)
171+
print_cert(cert, ip)
172+
173+
# Store certificate in loot with rex-sslscan metadata
174+
loot_cert = store_loot(
175+
'ssl.certificate.rex_sslscan',
176+
'application/x-pem-file',
177+
ip,
178+
cert.to_pem,
179+
"ssl_cert_#{ip}_#{rport}.pem",
180+
"SSL Certificate from #{ip}:#{rport}"
181+
)
182+
print_good("Certificate saved to loot: #{loot_cert}")
183+
end
184+
244185
def check_vulnerabilities(ip, ssl_version, ssl_cipher, cert)
245186
# POODLE
246187
if ssl_version == 'SSLv3'
@@ -368,31 +309,6 @@ def check_vulnerabilities(ip, ssl_version, ssl_cipher, cert)
368309

369310
return if cert.nil?
370311

371-
key_size = public_key_size(cert)
372-
if key_size > 0
373-
if key_size == 1024
374-
print_good('Public Key only 1024 bits')
375-
report_vuln(
376-
host: ip,
377-
port: rport,
378-
proto: 'tcp',
379-
name: name,
380-
info: "Module #{fullname} confirmed certificate key size 1024 bits",
381-
refs: ['CWE-326']
382-
)
383-
elsif key_size < 1024
384-
print_good('Public Key < 1024 bits')
385-
report_vuln(
386-
host: ip,
387-
port: rport,
388-
proto: 'tcp',
389-
name: name,
390-
info: "Module #{fullname} confirmed certificate key size < 1024 bits",
391-
refs: ['CWE-326']
392-
)
393-
end
394-
end
395-
396312
# certificate signed md5
397313
alg = cert.signature_algorithm
398314

@@ -435,88 +351,143 @@ def check_vulnerabilities(ip, ssl_version, ssl_cipher, cert)
435351
end
436352
end
437353

354+
# Enhanced vulnerability checking leveraging rex-sslscan data
355+
def check_vulnerabilities_enhanced(ip, ssl_version, cipher_name, cert, is_weak_cipher)
356+
check_vulnerabilities(ip, ssl_version, cipher_name, cert)
357+
358+
if is_weak_cipher
359+
print_good("#{ip}:#{rport} - Weak cipher detected: #{cipher_name}")
360+
report_vuln(
361+
host: ip,
362+
port: rport,
363+
proto: 'tcp',
364+
name: name,
365+
info: "Module #{fullname} detected weak cipher: #{cipher_name}",
366+
refs: ['CWE-327']
367+
)
368+
end
369+
end
370+
371+
# Store comprehensive rex-sslscan results
372+
def store_rex_sslscan_results(ip, scan_result)
373+
# Create detailed report
374+
report_data = {
375+
host: ip,
376+
port: rport,
377+
scan_timestamp: Time.now.utc,
378+
ssl_versions: {
379+
sslv2_supported: scan_result.supports_sslv2?,
380+
sslv3_supported: scan_result.supports_sslv3?,
381+
tlsv1_supported: scan_result.supports_tlsv1?,
382+
tlsv1_1_supported: scan_result.supports_tlsv1_1?,
383+
tlsv1_2_supported: scan_result.supports_tlsv1_2?
384+
},
385+
cipher_summary: {
386+
total_accepted: scan_result.accepted.length,
387+
total_rejected: scan_result.rejected.length,
388+
weak_ciphers: scan_result.weak_ciphers.length,
389+
strong_ciphers: scan_result.strong_ciphers.length
390+
},
391+
detailed_ciphers: scan_result.ciphers.to_a
392+
}
393+
394+
# Store as JSON loot
395+
loot_file = store_loot(
396+
'ssl.scan.rex_sslscan',
397+
'application/json',
398+
ip,
399+
report_data.to_json,
400+
"ssl_scan_#{ip}_#{rport}.json",
401+
"Rex::SSLScan results for #{ip}:#{rport}"
402+
)
403+
print_good("Detailed scan results saved to loot: #{loot_file}")
404+
end
405+
406+
# Process rex-sslscan results
407+
def process_rex_sslscan_results(ip, scan_result)
408+
# Report certificate if available
409+
if scan_result.cert
410+
process_certificate(ip, scan_result.cert)
411+
end
412+
413+
# Process accepted ciphers by version
414+
%i[SSLv2 SSLv3 TLSv1 TLSv1_1 TLSv1_2].each do |version|
415+
accepted_ciphers = scan_result.accepted(version)
416+
next if accepted_ciphers.empty?
417+
418+
print_good("#{ip}:#{rport} - #{version} supported with #{accepted_ciphers.length} cipher(s)")
419+
420+
key_size = public_key_size(scan_result.cert)
421+
if key_size > 0
422+
if key_size == 1024
423+
print_good('Public Key only 1024 bits')
424+
report_vuln(
425+
host: ip,
426+
port: rport,
427+
proto: 'tcp',
428+
name: name,
429+
info: "Module #{fullname} confirmed certificate key size 1024 bits",
430+
refs: ['CWE-326']
431+
)
432+
elsif key_size < 1024
433+
print_good('Public Key < 1024 bits')
434+
report_vuln(
435+
host: ip,
436+
port: rport,
437+
proto: 'tcp',
438+
name: name,
439+
info: "Module #{fullname} confirmed certificate key size < 1024 bits",
440+
refs: ['CWE-326']
441+
)
442+
end
443+
end
444+
445+
accepted_ciphers.each do |cipher_info|
446+
cipher_name = cipher_info[:cipher]
447+
key_length = cipher_info[:key_length]
448+
is_weak = cipher_info[:weak]
449+
450+
# Report the cipher
451+
print_status(" #{version}: #{cipher_name} (#{key_length} bits)#{is_weak ? ' - WEAK' : ''}")
452+
453+
# Check for vulnerabilities using existing logic
454+
check_vulnerabilities_enhanced(ip, version.to_s, cipher_name, scan_result.cert, is_weak)
455+
end
456+
end
457+
458+
# Report weak ciphers summary
459+
weak_ciphers = scan_result.weak_ciphers
460+
if weak_ciphers.any?
461+
print_bad("#{ip}:#{rport} - #{weak_ciphers.length} weak cipher(s) detected")
462+
end
463+
464+
# Store comprehensive scan results in loot
465+
store_rex_sslscan_results(ip, scan_result)
466+
end
467+
438468
# Fingerprint a single host
439469
def run_host(ip)
440-
# Get the available SSL/TLS versions that that Metasploit host supports
441-
versions = get_metasploit_ssl_versions
442-
443-
certs_found = {}
444-
skip_ssl_version = false
445-
vprint_status("Scanning #{ip} for: #{versions.map(&:to_s).join(', ')}")
446-
447-
# For each SSL/TLS version...
448-
versions.each do |version|
449-
skip_ssl_version = false
450-
451-
# Get the cipher suites that SSL/TLS can use on the Metasploit host
452-
# and print them out.
453-
ciphers = get_metasploit_ssl_cipher_suites(version)
454-
vprint_status("Scanning #{ip} #{version} with ciphers: #{ciphers.map(&:to_s).join(', ')}")
455-
456-
# For each cipher attempt to connect to the server. If we could connect with the given SSL version,
457-
# then skip it and move onto the next one. If the cipher isn't supported, then note this.
458-
# If the server responds with a peer certificate, make a new certificate object from it and find
459-
# its fingerprint, then check it for vulnerabilities, before saving it to loot if it hasn't been
460-
# saved already (check done using the certificate's SHA1 hash).
461-
#
462-
# In all cases the SSL version and cipher combination will also be checked for vulnerabilities
463-
# using the check_vulnerabilities function.
464-
ciphers.each do |cipher|
465-
break if skip_ssl_version
466-
467-
vprint_status("Attempting connection with SSL Version: #{version}, Cipher: #{cipher}")
468-
begin
469-
# setting the connect global to false means we can't see the socket, therefore the cert
470-
connect(true, { 'SSL' => true, 'SSLVersion' => version.sub('.', '_').to_sym, 'SSLCipher' => cipher }) # Force SSL
471-
print_good("Connected with SSL Version: #{version}, Cipher: #{cipher}")
472-
473-
if sock.respond_to? :peer_cert
474-
cert = OpenSSL::X509::Certificate.new(sock.peer_cert)
475-
# https://stackoverflow.com/questions/16516555/ruby-code-for-openssl-to-generate-fingerprint
476-
cert_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
477-
if certs_found.key? cert_fingerprint
478-
# dont check the cert more than once if its the same cert
479-
check_vulnerabilities(ip, version, cipher, nil)
480-
else
481-
loot_cert = store_loot('ssl.certificate', 'text/plain', ip, cert.to_text)
482-
print_good("Certificate saved to loot: #{loot_cert}")
483-
print_cert(cert, ip)
484-
check_vulnerabilities(ip, version, cipher, cert)
485-
end
486-
certs_found[cert_fingerprint] = cert
487-
end
488-
rescue ::OpenSSL::SSL::SSLError => e
489-
error_message = e.message.match(/ state=(.+)$/)
490-
491-
if error_message.nil?
492-
vprint_error("\tSSL Connection Error: #{e}")
493-
next
494-
end
495-
496-
# catch if the ssl_version/protocol isn't allowed and then we can skip out of it.
497-
if error_message[1].include? 'no protocols available'
498-
skip_ssl_version = true
499-
vprint_error("\tDoesn't accept #{version} connections, Skipping")
500-
break
501-
end
502-
vprint_error("\tDoes not accept #{version} using cipher #{cipher}, error message: #{error_message[1]}")
503-
rescue ArgumentError => e
504-
if e.message.match(%r{This version of Ruby does not support the requested SSL/TLS version})
505-
skip_ssl_version = true
506-
vprint_error("\t#{e.message}, Skipping")
507-
break
508-
end
509-
print_error("Exception encountered: #{e}")
510-
rescue StandardError => e
511-
if e.message.match(/connection was refused/) || e.message.match(/timed out/)
512-
print_error("\tPort closed or timeout occurred.")
513-
return 'Port closed or timeout occurred.'
514-
end
515-
print_error("\tException encountered: #{e}")
516-
ensure
517-
disconnect
518-
end
470+
print_status("Starting enhanced SSL/TLS scan of #{ip}:#{rport}")
471+
472+
begin
473+
ctx = { 'Msf' => framework, 'MsfExploit' => self }
474+
# Initialize rex-sslscan scanner
475+
scanner = Rex::SSLScan::Scanner.new(ip, rport, ctx)
476+
477+
# Perform the scan
478+
scan_result = scanner.scan
479+
480+
# Check if SSL/TLS is supported
481+
unless scan_result.supports_ssl?
482+
print_error("#{ip}:#{rport} - Server does not appear to support SSL/TLS")
483+
return
519484
end
485+
486+
# Process and report results
487+
process_rex_sslscan_results(ip, scan_result)
488+
rescue StandardError => e
489+
print_error("#{ip}:#{rport} - Scan error: #{e.message}")
490+
vprint_error("#{ip}:#{rport} - Backtrace: #{e.backtrace}")
520491
end
521492
end
522493
end

0 commit comments

Comments
 (0)