2022-10-16 15:57:49 -04:00
##
# 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
2022-10-18 09:51:05 -05:00
SSL version and cipher suite combination are checked. These checks include
POODLE, deprecated protocols, expired/not valid certs, low key strength, null cipher suites,
2022-10-16 15:57:49 -04:00
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' , 'https://www.openssl.org/~bodo/ssl-poodle.pdf' ] ,
2022-10-27 05:22:35 -04:00
# TLS v1.0 and v1.1 depreciation
2022-10-16 15:57:49 -04:00
[ 'URL' , 'https://datatracker.ietf.org/doc/rfc8996/' ] ,
2022-10-27 05:22:35 -04:00
# SSLv2 deprecation
2022-10-23 19:30:52 -04:00
[ 'URL' , 'https://datatracker.ietf.org/doc/html/rfc6176' ] ,
2022-10-27 05:22:35 -04:00
# SSLv3 deprecation
2022-10-23 19:30:52 -04:00
[ 'URL' , 'https://datatracker.ietf.org/doc/html/rfc7568' ] ,
2022-10-27 05:22:35 -04:00
# MD5 signed certs
2022-10-16 15:57:49 -04:00
[ 'URL' , 'https://www.win.tue.nl/hashclash/rogue-ca/' ] ,
2022-10-23 19:30:52 -04:00
[ 'CWE' , '328' ] ,
2022-10-16 15:57:49 -04:00
# DROWN attack
[ 'URL' , 'https://drownattack.com/' ] ,
[ 'CVE' , '2016-0800' ] ,
# BEAST
[ 'CVE' , '2011-3389' ] ,
# RC4
[ 'URL' , 'http://www.isg.rhul.ac.uk/tls/' ] ,
[ 'CVE' , '2013-2566' ] ,
# LOGJAM
[ 'CVE' , '2015-4000' ] ,
2022-10-23 19:30:52 -04:00
# NULL ciphers
[ 'CVE' , '2022-3358' ] ,
[ 'CWE' , '319' ] ,
# certificate expired
2022-11-05 07:23:14 -04:00
[ 'CWE' , '298' ] ,
2024-01-07 15:02:53 -05:00
# certificate broken or risky crypto algorithms
2022-10-23 19:30:52 -04:00
[ 'CWE' , '327' ] ,
# certificate inadequate encryption strength
[ 'CWE' , '326' ]
2022-10-16 15:57:49 -04:00
] ,
'DisclosureDate' = > 'Oct 14 2014'
)
register_options (
[
2022-11-05 07:23:14 -04:00
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 ] ) ,
2022-10-16 15:57:49 -04:00
]
)
end
2022-10-18 09:51:05 -05:00
def get_metasploit_ssl_versions
2022-10-27 05:22:35 -04:00
# There are two ways to generate a list of valid SSL Versions (SSLv3, TLS1.1, etc) and cipher suites (AES256-GCM-SHA384,
# ECDHE-RSA-CHACHA20-POLY1305, etc). The first would be to generate them independently. It's possible to
# pull all SSLContext methods (SSL Versions) via OpenSSL::SSL::SSLContext::METHODS here, as referenced in
2022-10-16 15:57:49 -04:00
# https://github.com/rapid7/rex-socket/blob/6ea0bb3b4e19c53d73e4337617be72c0ed351ceb/lib/rex/socket/ssl_tcp.rb#L46
2022-10-27 05:22:35 -04:00
# then pull all ciphers with OpenSSL::Cipher.ciphers. Now in theory you have a nice easy loop:
#
# OpenSSL::SSL::SSLContext::METHODS.each do |ssl_version|
# OpenSSL::Cipher.ciphers.each do |cipher_suite|
# # do something
# end
# end
#
# However, in practice we find that OpenSSL::SSL::SSLContext::METHODS includes '_client' and '_server' variants
# such as :TLSv1, :TLSv1_client, :TLSv1_server. In this case, we only need :TLSv1, so we need to remove ~2/3 of the list.
#
# Next, we'll find that many ciphers in OpenSSL::Cipher.ciphers are not applicable for various SSL versions.
# The loop we previously looked at has (at the time of writing on Kali Rollin, msf 6.2.23) 3060 rounds.
# This is a lot of iterations when we already know there are many combinations that will not be applicable for our
2024-01-07 15:02:53 -05:00
# use. Luckily there is a 2nd way which is much more efficient.
2022-10-27 05:22:35 -04:00
#
# The OpenSSL library includes https://docs.ruby-lang.org/en/2.4.0/OpenSSL/SSL/SSLContext.html#method-i-ciphers
# which we can use to generate a list of all ciphers, and SSL versions they work with. The structure is:
#
# [[name, version, bits, alg_bits], ...]
#
# which makes it very easy to just pull the 2nd element (version, or SSL version) from each list item, and unique it.
# This gives us the list of all SSL versions which also have at least one working cipher on our system.
# 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.
# Later in get_metasploit_ssl_cipher_suites, we can grab all cipher suites to a SSL version easily by simply filtering
# the 2nd element (version, or SSL version) from each list item.
2022-10-16 15:57:49 -04:00
if datastore [ 'SSLVersion' ] == 'All'
return Array . new ( OpenSSL :: SSL :: SSLContext . new . ciphers . length ) { | i | ( OpenSSL :: SSL :: SSLContext . new . ciphers [ i ] [ 1 ] ) . to_s } . uniq . reverse
end
2023-08-28 16:49:31 -04:00
[ datastore [ 'SSLVersion' ] ]
2022-10-16 15:57:49 -04:00
end
2022-10-18 09:51:05 -05:00
def get_metasploit_ssl_cipher_suites ( ssl_version )
2022-10-27 05:22:35 -04:00
# See comments in get_metasploit_ssl_versions for details on the use of
# OpenSSL::SSL::SSLContext.new.ciphers vs other methods to generate
# valid ciphers for a given SSL version
2022-10-18 09:51:05 -05:00
# First find all valid ciphers that the Metasploit host supports.
2022-10-27 05:22:35 -04:00
# Also transform the SSL version to a standard format.
2022-10-16 15:57:49 -04:00
ssl_version = ssl_version . to_s . gsub ( '_' , '.' )
all_ciphers = OpenSSL :: SSL :: SSLContext . new . ciphers
valid_ciphers = [ ]
2022-10-23 19:30:52 -04:00
# For each cipher that the Metasploit host supports, determine if that cipher
2022-10-27 05:22:35 -04:00
# is supported for use with the SSL version passed into this function. If it is,
# then add it to the valid_ciphers list.
2022-10-16 15:57:49 -04:00
all_ciphers . each do | cipher |
# cipher list has struct of [cipher, ssl_version, <int>, <int>]
if cipher [ 1 ] == ssl_version
valid_ciphers << cipher [ 0 ]
end
end
2022-10-18 09:51:05 -05:00
# If the user wants to use all ciphers then return all valid ciphers.
2022-11-05 07:23:14 -04:00
# Otherwise return only the one that matches the one the user specified
2022-10-18 09:51:05 -05:00
# in the SSLCipher datastore option.
#
# If no match is found for some reason then we will return an empty array.
2022-10-16 15:57:49 -04:00
if datastore [ 'SSLCipher' ] == 'All'
return valid_ciphers
elsif valid_ciphers . contains? datastore [ 'SSLCipher' ]
return [ datastore [ 'SSLCipher' ] ]
end
[ ]
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 )
2022-10-23 19:30:52 -04:00
if cert && cert . instance_of? ( OpenSSL :: X509 :: Certificate )
2022-10-16 15:57:49 -04:00
print_status ( 'Certificate Information:' )
print_status ( " \t Subject: #{ cert . subject } " )
print_status ( " \t Issuer: #{ cert . issuer } " )
print_status ( " \t Signature Alg: #{ cert . signature_algorithm } " )
# If we use ECDSA rather than RSA, our metrics for key size are different
print_status ( " \t Public Key Size: #{ public_key_size ( cert ) } bits " )
print_status ( " \t Not Valid Before: #{ cert . not_before } " )
print_status ( " \t Not Valid After: #{ cert . not_after } " )
# Checks for common properties of self signed certificates
2022-10-24 15:22:51 -04:00
# regex tried against a bunch of alexa top 100 and others.
# https://rubular.com/r/Yj6vyy1VqGWCL8
2022-10-16 15:57:49 -04:00
caissuer = nil
cert . extensions . each do | e |
2022-10-24 15:22:51 -04:00
next unless / CA Issuers - URI:([^, \ n]*) /i =~ e . to_s
caissuer = :: Regexp . last_match ( 1 )
break
2022-10-16 15:57:49 -04:00
end
2022-10-27 05:22:35 -04:00
if caissuer . blank?
2022-10-16 15:57:49 -04:00
print_good ( " \t Certificate contains no CA Issuers extension... possible self signed certificate " )
else
2022-10-24 15:22:51 -04:00
print_status ( " \t CA Issuer: #{ caissuer } " )
2022-10-16 15:57:49 -04:00
end
if cert . issuer . to_s == cert . subject . to_s
print_good ( " \t Certificate Subject and Issuer match... possible self signed certificate " )
end
alg = cert . signature_algorithm
if alg . downcase . include? 'md5'
print_status ( " \t WARNING: Signature algorithm using MD5 ( #{ alg } ) " )
end
vhostn = nil
2022-10-18 09:51:05 -05:00
# 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.
#
2022-10-23 19:30:52 -04:00
# If it does, then assign the value of vhost name, aka the
2022-10-18 09:51:05 -05:00
# second entry in the array,to vhostn
2022-10-16 15:57:49 -04:00
cert . subject . to_a . each do | n |
vhostn = n [ 1 ] if n [ 0 ] == 'CN'
end
if vhostn
print_status ( " \t Has 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
2022-10-27 05:22:35 -04:00
# https://github.com/rapid7/metasploit-framework/pull/17149#discussion_r1000675472
2022-10-16 15:57:49 -04:00
if vhostn !~ / localhost|snakeoil /i
report_host (
host : ip ,
name : vhostn
)
end
end
else
print_status ( " \t No certificate subject or common name found. " )
end
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 ,
2022-10-23 19:30:52 -04:00
info : " Module #{ fullname } confirmed SSLv3 is available. Vulnerable to POODLE, CVE-2014-3566. " ,
2022-10-27 05:22:35 -04:00
refs : [ 'CVE-2014-3566' ]
2022-10-16 15:57:49 -04:00
)
end
# DROWN
if ssl_version == 'SSLv2'
print_good ( 'Accepts SSLv2, vulnerable to DROWN' )
report_vuln (
host : ip ,
port : rport ,
proto : 'tcp' ,
name : name ,
2022-10-23 19:30:52 -04:00
info : " Module #{ fullname } confirmed SSLv2 is available. Vulnerable to DROWN, CVE-2016-0800. " ,
2022-10-27 05:22:35 -04:00
refs : [ 'CVE-2016-0800' ]
2022-10-16 15:57:49 -04:00
)
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 ,
2022-10-23 19:30:52 -04:00
info : " Module #{ fullname } confirmed SSLv3/TLSv1 and a CBC cipher. Vulnerable to BEAST, CVE-2011-3389. " ,
2022-10-27 05:22:35 -04:00
refs : [ 'CVE-2011-3389' ]
2022-10-16 15:57:49 -04:00
)
end
# RC4 ciphers
if ssl_cipher . upcase . include? ( 'RC4' )
print_good ( 'Accepts RC4 cipher.' )
report_vuln (
host : ip ,
port : rport ,
proto : 'tcp' ,
name : name ,
2022-10-27 05:22:35 -04:00
info : " Module #{ fullname } confirmed RC4 cipher. " ,
refs : [ 'CVE-2013-2566' ]
2022-10-16 15:57:49 -04:00
)
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. " ,
2022-10-27 05:22:35 -04:00
refs : [ 'CWE-327' ]
2022-10-16 15:57:49 -04:00
)
end
2022-10-23 19:30:52 -04:00
2022-10-16 15:57:49 -04:00
# 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 " ,
2022-10-27 05:22:35 -04:00
refs : [ 'CVE-2015-4000' ]
2022-10-16 15:57:49 -04:00
)
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. " ,
2022-10-27 05:22:35 -04:00
refs : [ 'CVE-2022-3358' ]
2022-10-16 15:57:49 -04:00
)
end
# deprecation
if ssl_version == 'SSLv2'
print_good ( 'Accepts Deprecated SSLv2' )
report_vuln (
host : ip ,
port : rport ,
proto : 'tcp' ,
name : name ,
2022-10-27 05:22:35 -04:00
info : " Module #{ fullname } confirmed SSLv2, which was deprecated in 2011. " ,
refs : [ 'https://datatracker.ietf.org/doc/html/rfc6176' ]
2022-10-16 15:57:49 -04:00
)
elsif ssl_version == 'SSLv3'
print_good ( 'Accepts Deprecated SSLv3' )
report_vuln (
host : ip ,
port : rport ,
proto : 'tcp' ,
name : name ,
2022-10-27 05:22:35 -04:00
info : " Module #{ fullname } confirmed SSLv3, which was deprecated in 2015. " ,
refs : [ 'https://datatracker.ietf.org/doc/html/rfc7568' ]
2022-10-16 15:57:49 -04:00
)
elsif ssl_version == 'TLSv1.0'
print_good ( 'Accepts Deprecated TLSv1.0' )
report_vuln (
host : ip ,
port : rport ,
proto : 'tcp' ,
name : name ,
2022-10-27 05:22:35 -04:00
info : " Module #{ fullname } confirmed TLSv1.0, which was widely deprecated in 2020. " ,
refs : [ 'https://datatracker.ietf.org/doc/rfc8996/' ]
2022-10-16 15:57:49 -04:00
)
end
return if cert . nil?
key_size = public_key_size ( 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 " ,
2022-10-27 05:22:35 -04:00
refs : [ 'CWE-326' ]
2022-10-16 15:57:49 -04:00
)
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 " ,
2022-10-27 05:22:35 -04:00
refs : [ 'CWE-326' ]
2022-10-16 15:57:49 -04:00
)
end
end
# 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 " ,
2022-10-27 05:22:35 -04:00
refs : [ 'CWE-328' ]
2022-10-16 15:57:49 -04:00
)
end
# expired
2022-10-23 19:30:52 -04:00
if cert . not_after < DateTime . now
2022-10-16 15:57:49 -04:00
print_good ( " Certificate expired: #{ cert . not_after } " )
report_vuln (
host : ip ,
port : rport ,
proto : 'tcp' ,
name : name ,
info : " Module #{ fullname } confirmed certificate expired " ,
2022-11-05 07:23:14 -04:00
refs : [ 'CWE-298' ]
2022-10-16 15:57:49 -04:00
)
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 " ,
2022-11-05 07:23:14 -04:00
refs : [ ]
2022-10-16 15:57:49 -04:00
)
end
end
# Fingerprint a single host
def run_host ( ip )
2022-10-18 09:51:05 -05:00
# Get the available SSL/TLS versions that that Metasploit host supports
versions = get_metasploit_ssl_versions
2022-10-16 15:57:49 -04:00
certs_found = { }
skip_ssl_version = false
vprint_status ( " Scanning #{ ip } for: #{ versions . map ( & :to_s ) . join ( ', ' ) } " )
2022-10-18 09:51:05 -05:00
# For each SSL/TLS version...
2022-10-16 15:57:49 -04:00
versions . each do | version |
skip_ssl_version = false
2022-10-18 09:51:05 -05:00
# Get the cipher suites that SSL/TLS can use on the Metasploit host
# and print them out.
ciphers = get_metasploit_ssl_cipher_suites ( version )
2022-10-16 15:57:49 -04:00
vprint_status ( " Scanning #{ ip } #{ version } with ciphers: #{ ciphers . map ( & :to_s ) . join ( ', ' ) } " )
2022-10-18 09:51:05 -05:00
# For each cipher attempt to connect to the server. If we could connect with the given SSL version,
# then skip it and move onto the next one. If the cipher isn't supported, then note this.
# If the server responds with a peer certificate, make a new certificate object from it and find
# its fingerprint, then check it for vulnerabilities, before saving it to loot if it hasn't been
# saved already (check done using the certificate's SHA1 hash).
2022-10-23 19:30:52 -04:00
#
# In all cases the SSL version and cipher combination will also be checked for vulnerabilities
2022-10-18 09:51:05 -05:00
# using the check_vulnerabilities function.
2022-10-16 15:57:49 -04:00
ciphers . each do | cipher |
break if skip_ssl_version
vprint_status ( " Attempting connection with SSL Version: #{ version } , Cipher: #{ cipher } " )
begin
# setting the connect global to false means we can't see the socket, therefore the cert
connect ( true , { 'SSL' = > true , 'SSLVersion' = > version . sub ( '.' , '_' ) . to_sym , 'SSLCipher' = > cipher } ) # Force SSL
print_good ( " Connected with SSL Version: #{ version } , Cipher: #{ cipher } " )
if sock . respond_to? :peer_cert
cert = OpenSSL :: X509 :: Certificate . new ( sock . peer_cert )
# https://stackoverflow.com/questions/16516555/ruby-code-for-openssl-to-generate-fingerprint
cert_fingerprint = OpenSSL :: Digest :: SHA1 . new ( cert . to_der ) . to_s
if certs_found . key? cert_fingerprint
# dont check the cert more than once if its the same cert
check_vulnerabilities ( ip , version , cipher , nil )
else
loot_cert = store_loot ( 'ssl.certificate' , 'text/plain' , ip , cert . to_text )
print_good ( " Certificate saved to loot: #{ loot_cert } " )
print_cert ( cert , ip )
check_vulnerabilities ( ip , version , cipher , cert )
end
certs_found [ cert_fingerprint ] = cert
end
rescue :: OpenSSL :: SSL :: SSLError = > e
error_message = e . message . match ( / state=(.+)$ / )
if error_message . nil?
vprint_error ( " \t SSL Connection Error: #{ e } " )
next
end
# catch if the ssl_version/protocol isn't allowed and then we can skip out of it.
if error_message [ 1 ] . include? 'no protocols available'
skip_ssl_version = true
vprint_error ( " \t Doesn't accept #{ version } connections, Skipping " )
break
end
2022-10-23 19:30:52 -04:00
vprint_error ( " \t Does not accept #{ version } using cipher #{ cipher } , error message: #{ error_message [ 1 ] } " )
2022-10-16 15:57:49 -04:00
rescue ArgumentError = > e
if e . message . match ( %r{ This version of Ruby does not support the requested SSL/TLS version } )
skip_ssl_version = true
vprint_error ( " \t #{ e . message } , Skipping " )
break
end
print_error ( " Exception encountered: #{ e } " )
rescue StandardError = > e
if e . message . match ( / connection was refused / ) || e . message . match ( / timed out / )
2024-01-07 15:02:53 -05:00
print_error ( " \t Port closed or timeout occurred. " )
return 'Port closed or timeout occurred.'
2022-10-16 15:57:49 -04:00
end
print_error ( " \t Exception encountered: #{ e } " )
ensure
disconnect
end
end
end
end
end