## # 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 ', # 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' => '2014-10-14' ) 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