Share
## https://sploitus.com/exploit?id=MSF:AUXILIARY-SCANNER-SSL-SSL_VERSION-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::Tcp
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
include Msf::Module::Deprecated
moved_from 'auxiliary/scanner/http/ssl'
moved_from 'auxiliary/scanner/http/ssl_version'
def initialize
super(
'Name' => 'SSL/TLS Version Detection',
'Description' => %q{
Check if a server supports a given version of SSL/TLS and cipher suites.
The certificate is stored in loot, and any known vulnerabilities against that
SSL version and cipher suite combination are checked. These checks include
POODLE, deprecated protocols, expired/not valid certs, low key strength, null cipher suites,
certificates signed with MD5, DROWN, RC4 ciphers, exportable ciphers, LOGJAM, and BEAST.
},
'Author' => [
'todb', # original ssl scanner for poodle
'et', # original ssl certificate module
'Chris John Riley', # original ssl certificate additions
'Veit Hailperin <hailperv[at]gmail.com>', # original ssl certificate checks for public key size, valid time
'h00die' # combining, modernization
],
'License' => MSF_LICENSE,
'DefaultOptions' => {
'SSL' => true,
'RPORT' => 443
},
'References' => [
# poodle
[ 'URL', 'https://security.googleblog.com/2014/10/this-poodle-bites-exploiting-ssl-30.html' ],
[ 'CVE', '2014-3566' ],
[ 'URL', 'http://web.archive.org/web/20240319071045/https://www.openssl.org/~bodo/ssl-poodle.pdf' ],
# TLS v1.0 and v1.1 depreciation
[ 'URL', 'https://datatracker.ietf.org/doc/rfc8996/' ],
# SSLv2 deprecation
[ 'URL', 'https://datatracker.ietf.org/doc/html/rfc6176' ],
# SSLv3 deprecation
[ 'URL', 'https://datatracker.ietf.org/doc/html/rfc7568' ],
# MD5 signed certs
[ 'URL', 'https://www.win.tue.nl/hashclash/rogue-ca/' ],
[ 'CWE', '328' ],
# DROWN attack
[ 'URL', 'https://drownattack.com/' ],
[ 'CVE', '2016-0800' ],
# BEAST
[ 'CVE', '2011-3389' ],
# RC4
[ 'URL', 'http://web.archive.org/web/20240607160328/https://www.isg.rhul.ac.uk/tls/' ],
[ 'CVE', '2013-2566' ],
# LOGJAM
[ 'CVE', '2015-4000' ],
# NULL ciphers
[ 'CVE', '2022-3358' ],
[ 'CWE', '319'],
# certificate expired
[ 'CWE', '298' ],
# certificate broken or risky crypto algorithms
[ 'CWE', '327' ],
# certificate inadequate encryption strength
[ 'CWE', '326' ]
],
'DisclosureDate' => 'Oct 14 2014'
)
register_options(
[
OptString.new('SSLServerNameIndication', [ false, 'SSL/TLS Server Name Indication (SNI)', nil]),
OptEnum.new('SSLVersion', [ true, 'SSL version to test', 'All', ['All'] + Array.new(OpenSSL::SSL::SSLContext.new.ciphers.length) { |i| (OpenSSL::SSL::SSLContext.new.ciphers[i][1]).to_s }.uniq.reverse]),
OptEnum.new('SSLCipher', [ true, 'SSL cipher to test', 'All', ['All'] + Array.new(OpenSSL::SSL::SSLContext.new.ciphers.length) { |i| (OpenSSL::SSL::SSLContext.new.ciphers[i][0]).to_s }.uniq]),
]
)
end
def public_key_size(cert)
if cert.public_key.respond_to? :n
return cert.public_key.n.num_bytes * 8
end
0
end
def print_cert(cert, ip)
if cert && cert.instance_of?(OpenSSL::X509::Certificate)
print_status('Certificate Information:')
print_status("\tSubject: #{cert.subject}")
print_status("\tIssuer: #{cert.issuer}")
print_status("\tSignature Alg: #{cert.signature_algorithm}")
# If we use ECDSA rather than RSA, our metrics for key size are different
print_status("\tPublic Key Size: #{public_key_size(cert)} bits")
print_status("\tNot Valid Before: #{cert.not_before}")
print_status("\tNot Valid After: #{cert.not_after}")
# Checks for common properties of self signed certificates
# regex tried against a bunch of alexa top 100 and others.
# https://rubular.com/r/Yj6vyy1VqGWCL8
caissuer = nil
cert.extensions.each do |e|
next unless /CA Issuers - URI:([^, \n]*)/i =~ e.to_s
caissuer = ::Regexp.last_match(1)
break
end
if caissuer.blank?
print_good("\tCertificate contains no CA Issuers extension... possible self signed certificate")
else
print_status("\tCA Issuer: #{caissuer}")
end
if cert.issuer.to_s == cert.subject.to_s
print_good("\tCertificate Subject and Issuer match... possible self signed certificate")
end
alg = cert.signature_algorithm
if alg.downcase.include? 'md5'
print_status("\tWARNING: Signature algorithm using MD5 (#{alg})")
end
vhostn = nil
# Convert the certificate subject field into a series of arrays.
# For each array, which will represent one subject, then
# go ahead and check if the subject describes a CN entry.
#
# If it does, then assign the value of vhost name, aka the
# second entry in the array,to vhostn
cert.subject.to_a.each do |n|
vhostn = n[1] if n[0] == 'CN'
end
if vhostn
print_status("\tHas common name #{vhostn}")
# Store the virtual hostname for HTTP
report_note(
host: ip,
port: rport,
proto: 'tcp',
type: 'http.vhost',
data: { name: vhostn }
)
# Update the server hostname if necessary
# https://github.com/rapid7/metasploit-framework/pull/17149#discussion_r1000675472
if vhostn !~ /localhost|snakeoil/i
report_host(
host: ip,
name: vhostn
)
end
end
else
print_status("\tNo certificate subject or common name found.")
end
end
# Process certificate with enhanced analysis
def process_certificate(ip, cert)
print_cert(cert, ip)
# Store certificate in loot with rex-sslscan metadata
loot_cert = store_loot(
'ssl.certificate.rex_sslscan',
'application/x-pem-file',
ip,
cert.to_pem,
"ssl_cert_#{ip}_#{rport}.pem",
"SSL Certificate from #{ip}:#{rport}"
)
print_good("Certificate saved to loot: #{loot_cert}")
end
def check_vulnerabilities(ip, ssl_version, ssl_cipher, cert)
# POODLE
if ssl_version == 'SSLv3'
print_good('Accepts SSLv3, vulnerable to POODLE')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed SSLv3 is available. Vulnerable to POODLE, CVE-2014-3566.",
refs: ['CVE-2014-3566']
)
end
# DROWN
if ssl_version == 'SSLv2'
print_good('Accepts SSLv2, vulnerable to DROWN')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed SSLv2 is available. Vulnerable to DROWN, CVE-2016-0800.",
refs: ['CVE-2016-0800']
)
end
# BEAST
if ((ssl_version == 'SSLv3') || (ssl_version == 'TLSv1.0')) && ssl_cipher.include?('CBC')
print_good('Accepts SSLv3/TLSv1 and a CBC cipher, vulnerable to BEAST')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed SSLv3/TLSv1 and a CBC cipher. Vulnerable to BEAST, CVE-2011-3389.",
refs: ['CVE-2011-3389']
)
end
# RC4 ciphers
if ssl_cipher.upcase.include?('RC4')
print_good('Accepts RC4 cipher.')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed RC4 cipher.",
refs: ['CVE-2013-2566']
)
end
# export ciphers
if ssl_cipher.upcase.include?('EXPORT')
print_good('Accepts EXPORT based cipher.')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed EXPORT based cipher.",
refs: ['CWE-327']
)
end
# LOGJAM
if ssl_cipher.upcase.include?('DHE_EXPORT')
print_good('Accepts DHE_EXPORT based cipher.')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed DHE_EXPORT based cipher. Vulnerable to LOGJAM, CVE-2015-4000",
refs: ['CVE-2015-4000']
)
end
# Null ciphers
if ssl_cipher.upcase.include? 'NULL'
print_good('Accepts Null cipher')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed Null cipher.",
refs: ['CVE-2022-3358']
)
end
# deprecation
if ssl_version == 'SSLv2'
print_good('Accepts Deprecated SSLv2')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed SSLv2, which was deprecated in 2011.",
refs: ['https://datatracker.ietf.org/doc/html/rfc6176']
)
elsif ssl_version == 'SSLv3'
print_good('Accepts Deprecated SSLv3')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed SSLv3, which was deprecated in 2015.",
refs: ['https://datatracker.ietf.org/doc/html/rfc7568']
)
elsif ssl_version == 'TLSv1.0'
print_good('Accepts Deprecated TLSv1.0')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed TLSv1.0, which was widely deprecated in 2020.",
refs: ['https://datatracker.ietf.org/doc/rfc8996/']
)
end
return if cert.nil?
# certificate signed md5
alg = cert.signature_algorithm
if alg.downcase.include? 'md5'
print_good('Certificate signed with MD5')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed certificate signed with MD5 algo",
refs: ['CWE-328']
)
end
# expired
if cert.not_after < DateTime.now
print_good("Certificate expired: #{cert.not_after}")
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed certificate expired",
refs: ['CWE-298']
)
end
# not yet valid
if cert.not_before > DateTime.now
print_good("Certificate not yet valid: #{cert.not_after}")
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed certificate not yet valid",
refs: []
)
end
end
# Enhanced vulnerability checking leveraging rex-sslscan data
def check_vulnerabilities_enhanced(ip, ssl_version, cipher_name, cert, is_weak_cipher)
check_vulnerabilities(ip, ssl_version, cipher_name, cert)
if is_weak_cipher
print_good("#{ip}:#{rport} - Weak cipher detected: #{cipher_name}")
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} detected weak cipher: #{cipher_name}",
refs: ['CWE-327']
)
end
end
# Store comprehensive rex-sslscan results
def store_rex_sslscan_results(ip, scan_result)
# Create detailed report
report_data = {
host: ip,
port: rport,
scan_timestamp: Time.now.utc,
ssl_versions: {
sslv2_supported: scan_result.supports_sslv2?,
sslv3_supported: scan_result.supports_sslv3?,
tlsv1_supported: scan_result.supports_tlsv1?,
tlsv1_1_supported: scan_result.supports_tlsv1_1?,
tlsv1_2_supported: scan_result.supports_tlsv1_2?
},
cipher_summary: {
total_accepted: scan_result.accepted.length,
total_rejected: scan_result.rejected.length,
weak_ciphers: scan_result.weak_ciphers.length,
strong_ciphers: scan_result.strong_ciphers.length
},
detailed_ciphers: scan_result.ciphers.to_a
}
# Store as JSON loot
loot_file = store_loot(
'ssl.scan.rex_sslscan',
'application/json',
ip,
report_data.to_json,
"ssl_scan_#{ip}_#{rport}.json",
"Rex::SSLScan results for #{ip}:#{rport}"
)
print_good("Detailed scan results saved to loot: #{loot_file}")
end
# Process rex-sslscan results
def process_rex_sslscan_results(ip, scan_result)
# Report certificate if available
if scan_result.cert
process_certificate(ip, scan_result.cert)
end
# Process accepted ciphers by version
%i[SSLv2 SSLv3 TLSv1 TLSv1_1 TLSv1_2].each do |version|
accepted_ciphers = scan_result.accepted(version)
next if accepted_ciphers.empty?
print_good("#{ip}:#{rport} - #{version} supported with #{accepted_ciphers.length} cipher(s)")
key_size = public_key_size(scan_result.cert)
if key_size > 0
if key_size == 1024
print_good('Public Key only 1024 bits')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed certificate key size 1024 bits",
refs: ['CWE-326']
)
elsif key_size < 1024
print_good('Public Key < 1024 bits')
report_vuln(
host: ip,
port: rport,
proto: 'tcp',
name: name,
info: "Module #{fullname} confirmed certificate key size < 1024 bits",
refs: ['CWE-326']
)
end
end
accepted_ciphers.each do |cipher_info|
cipher_name = cipher_info[:cipher]
key_length = cipher_info[:key_length]
is_weak = cipher_info[:weak]
# Report the cipher
print_status(" #{version}: #{cipher_name} (#{key_length} bits)#{is_weak ? ' - WEAK' : ''}")
# Check for vulnerabilities using existing logic
check_vulnerabilities_enhanced(ip, version.to_s, cipher_name, scan_result.cert, is_weak)
end
end
# Report weak ciphers summary
weak_ciphers = scan_result.weak_ciphers
if weak_ciphers.any?
print_bad("#{ip}:#{rport} - #{weak_ciphers.length} weak cipher(s) detected")
end
# Store comprehensive scan results in loot
store_rex_sslscan_results(ip, scan_result)
end
# Fingerprint a single host
def run_host(ip)
print_status("Starting enhanced SSL/TLS scan of #{ip}:#{rport}")
begin
ctx = { 'Msf' => framework, 'MsfExploit' => self }
tls_server_name_indication = nil
tls_server_name_indication = datastore['SSLServerNameIndication'] if datastore['SSLServerNameIndication'].present?
tls_server_name_indication = datastore['RHOSTNAME'] if tls_server_name_indication.nil? && datastore['RHOSTNAME'].present?
# Initialize rex-sslscan scanner
scanner = Rex::SSLScan::Scanner.new(ip, rport, ctx, tls_server_name_indication: tls_server_name_indication)
# Perform the scan
scan_result = scanner.scan
# Check if SSL/TLS is supported
unless scan_result.supports_ssl?
print_error("#{ip}:#{rport} - Server does not appear to support SSL/TLS")
return
end
# Process and report results
process_rex_sslscan_results(ip, scan_result)
rescue StandardError => e
print_error("#{ip}:#{rport} - Scan error: #{e.message}")
vprint_error("#{ip}:#{rport} - Backtrace: #{e.backtrace}")
end
end
end