7.4 High
CVSS3
Attack Vector
NETWORK
Attack Complexity
HIGH
Privileges Required
NONE
User Interaction
NONE
Scope
UNCHANGED
Confidentiality Impact
HIGH
Integrity Impact
HIGH
Availability Impact
NONE
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
8.2 High
AI Score
Confidence
High
5.8 Medium
CVSS2
Access Vector
NETWORK
Access Complexity
MEDIUM
Authentication
NONE
Confidentiality Impact
PARTIAL
Integrity Impact
PARTIAL
Availability Impact
NONE
AV:N/AC:M/Au:N/C:P/I:P/A:N
0.974 High
EPSS
Percentile
99.9%
This module is a simple client for the SSL Labs APIs, designed for SSL/TLS assessment during a penetration test.
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'active_support/inflector'
require 'json'
require 'active_support/core_ext/hash'
class MetasploitModule < Msf::Auxiliary
class InvocationError < StandardError; end
class RequestRateTooHigh < StandardError; end
class InternalError < StandardError; end
class ServiceNotAvailable < StandardError; end
class ServiceOverloaded < StandardError; end
class Api
attr_reader :max_assessments, :current_assessments
def initialize
@max_assessments = 0
@current_assessments = 0
end
def request(name, params = {})
api_host = "api.ssllabs.com"
api_port = "443"
api_path = "/api/v2/"
user_agent = "Msf_ssllabs_scan"
name = name.to_s.camelize(:lower)
uri = api_path + name
cli = Rex::Proto::Http::Client.new(api_host, api_port, {}, true, 'TLS')
cli.connect
req = cli.request_cgi({
'uri' => uri,
'agent' => user_agent,
'method' => 'GET',
'vars_get' => params
})
res = cli.send_recv(req)
cli.close
if res && res.code.to_i == 200
@max_assessments = res.headers['X-Max-Assessments']
@current_assessments = res.headers['X-Current-Assessments']
r = JSON.load(res.body)
fail InvocationError, "API returned: #{r['errors']}" if r.key?('errors')
return r
end
case res.code.to_i
when 400
fail InvocationError
when 429
fail RequestRateTooHigh
when 500
fail InternalError
when 503
fail ServiceNotAvailable
when 529
fail ServiceOverloaded
else
fail StandardError, "HTTP error code #{r.code}", caller
end
end
def report_unused_attrs(type, unused_attrs)
unused_attrs.each do | attr |
# $stderr.puts "#{type} request returned unknown parameter #{attr}"
end
end
def info
obj, unused_attrs = Info.load request(:info)
report_unused_attrs('info', unused_attrs)
obj
end
def analyse(params = {})
obj, unused_attrs = Host.load request(:analyze, params)
report_unused_attrs('analyze', unused_attrs)
obj
end
def get_endpoint_data(params = {})
obj, unused_attrs = Endpoint.load request(:get_endpoint_data, params)
report_unused_attrs('get_endpoint_data', unused_attrs)
obj
end
def get_status_codes
obj, unused_attrs = StatusCodes.load request(:get_status_codes)
report_unused_attrs('get_status_codes', unused_attrs)
obj
end
end
class ApiObject
class << self;
attr_accessor :all_attributes
attr_accessor :fields
attr_accessor :lists
attr_accessor :refs
end
def self.inherited(base)
base.all_attributes = []
base.fields = []
base.lists = {}
base.refs = {}
end
def self.to_api_name(name)
name.to_s.gsub(/\?$/, '').camelize(:lower)
end
def self.to_attr_name(name)
name.to_s.gsub(/\?$/, '').underscore
end
def self.field_methods(name)
is_bool = name.to_s.end_with?('?')
attr_name = to_attr_name(name)
api_name = to_api_name(name)
class_eval <<-EOF, __FILE__, __LINE__
def #{attr_name}#{'?' if is_bool}
@#{api_name}
end
def #{attr_name}=(value)
@#{api_name} = value
end
EOF
end
def self.has_fields(*names)
names.each do |name|
@all_attributes << to_api_name(name)
@fields << to_api_name(name)
field_methods(name)
end
end
def self.has_objects_list(name, klass)
@all_attributes << to_api_name(name)
@lists[to_api_name(name)] = klass
field_methods(name)
end
def self.has_object_ref(name, klass)
@all_attributes << to_api_name(name)
@refs[to_api_name(name)] = klass
field_methods(name)
end
def self.load(attributes = {})
obj = self.new
unused_attrs = []
attributes.each do |name, value|
if @fields.include?(name)
obj.instance_variable_set("@#{name}", value)
elsif @lists.key?(name)
unless value.nil?
var = value.map do |v|
val, ua = @lists[name].load(v)
unused_attrs.concat ua
val
end
obj.instance_variable_set("@#{name}", var)
end
elsif @refs.key?(name)
unless value.nil?
val, ua = @refs[name].load(value)
unused_attrs.concat ua
obj.instance_variable_set("@#{name}", val)
end
else
unused_attrs << name
end
end
return obj, unused_attrs
end
def to_json(opts = {})
obj = {}
self.class.all_attributes.each do |api_name|
v = instance_variable_get("@#{api_name}")
obj[api_name] = v
end
obj.to_json
end
end
class Cert < ApiObject
has_fields :subject,
:commonNames,
:altNames,
:notBefore,
:notAfter,
:issuerSubject,
:sigAlg,
:issuerLabel,
:revocationInfo,
:crlURIs,
:ocspURIs,
:revocationStatus,
:crlRevocationStatus,
:ocspRevocationStatus,
:sgc?,
:validationType,
:issues,
:sct?,
:mustStaple,
:sha1Hash,
:pinSha256
def valid?
issues == 0
end
def invalid?
!valid?
end
end
class ChainCert < ApiObject
has_fields :subject,
:label,
:notBefore,
:notAfter,
:issuerSubject,
:issuerLabel,
:sigAlg,
:issues,
:keyAlg,
:keySize,
:keyStrength,
:revocationStatus,
:crlRevocationStatus,
:ocspRevocationStatus,
:raw,
:sha1Hash,
:pinSha256
def valid?
issues == 0
end
def invalid?
!valid?
end
end
class Chain < ApiObject
has_objects_list :certs, ChainCert
has_fields :issues
def valid?
issues == 0
end
def invalid?
!valid?
end
end
class Key < ApiObject
has_fields :size,
:strength,
:alg,
:debianFlaw?,
:q
def insecure?
debian_flaw? || q == 0
end
def secure?
!insecure?
end
end
class Protocol < ApiObject
has_fields :id,
:name,
:version,
:v2SuitesDisabled?,
:q
def insecure?
q == 0
end
def secure?
!insecure?
end
end
class Info < ApiObject
has_fields :engineVersion,
:criteriaVersion,
:clientMaxAssessments,
:maxAssessments,
:currentAssessments,
:messages,
:newAssessmentCoolOff
end
class SimClient < ApiObject
has_fields :id,
:name,
:platform,
:version,
:isReference?
end
class Simulation < ApiObject
has_object_ref :client, SimClient
has_fields :errorCode,
:attempts,
:protocolId,
:suiteId,
:kxInfo
def success?
error_code == 0
end
def error?
!success?
end
end
class SimDetails < ApiObject
has_objects_list :results, Simulation
end
class StatusCodes < ApiObject
has_fields :statusDetails
def [](name)
status_details[name]
end
end
class Suite < ApiObject
has_fields :id,
:name,
:cipherStrength,
:dhStrength,
:dhP,
:dhG,
:dhYs,
:ecdhBits,
:ecdhStrength,
:q
def insecure?
q == 0
end
def secure?
!insecure?
end
end
class Suites < ApiObject
has_objects_list :list, Suite
has_fields :preference?
end
class EndpointDetails < ApiObject
has_fields :hostStartTime
has_object_ref :key, Key
has_object_ref :cert, Cert
has_object_ref :chain, Chain
has_objects_list :protocols, Protocol
has_object_ref :suites, Suites
has_fields :serverSignature,
:prefixDelegation?,
:nonPrefixDelegation?,
:vulnBeast?,
:renegSupport,
:stsResponseHeader,
:stsMaxAge,
:stsSubdomains?,
:pkpResponseHeader,
:sessionResumption,
:compressionMethods,
:supportsNpn?,
:npnProtocols,
:sessionTickets,
:ocspStapling?,
:staplingRevocationStatus,
:staplingRevocationErrorMessage,
:sniRequired?,
:httpStatusCode,
:httpForwarding,
:supportsRc4?,
:forwardSecrecy,
:rc4WithModern?
has_object_ref :sims, SimDetails
has_fields :heartbleed?,
:heartbeat?,
:openSslCcs,
:poodle?,
:poodleTls,
:fallbackScsv?,
:freak?,
:hasSct,
:stsStatus,
:stsPreload,
:supportsAlpn,
:rc4Only,
:protocolIntolerance,
:miscIntolerance,
:openSSLLuckyMinus20,
:logjam,
:chaCha20Preference,
:hstsPolicy,
:hstsPreloads,
:hpkpPolicy,
:hpkpRoPolicy,
:drownHosts,
:drownErrors,
:drownVulnerable
end
class Endpoint < ApiObject
has_fields :ipAddress,
:serverName,
:statusMessage,
:statusDetails,
:statusDetailsMessage,
:grade,
:gradeTrustIgnored,
:hasWarnings?,
:isExceptional?,
:progress,
:duration,
:eta,
:delegation
has_object_ref :details, EndpointDetails
end
class Host < ApiObject
has_fields :host,
:port,
:protocol,
:isPublic?,
:status,
:statusMessage,
:startTime,
:testTime,
:engineVersion,
:criteriaVersion,
:cacheExpiryTime
has_objects_list :endpoints, Endpoint
has_fields :certHostnames
end
def initialize(info = {})
super(update_info(info,
'Name' => 'SSL Labs API Client',
'Description' => %q{
This module is a simple client for the SSL Labs APIs, designed for
SSL/TLS assessment during a penetration test.
},
'License' => MSF_LICENSE,
'Author' =>
[
'Denis Kolegov <dnkolegov[at]gmail.com>',
'Francois Chagnon' # ssllab.rb author (https://github.com/Shopify/ssllabs.rb)
],
'DefaultOptions' =>
{
'RPORT' => 443,
'SSL' => true,
}
))
register_options(
[
OptString.new('HOSTNAME', [true, 'The target hostname']),
OptInt.new('DELAY', [true, 'The delay in seconds between API requests', 5]),
OptBool.new('USECACHE', [true, 'Use cached results (if available), else force live scan', true]),
OptBool.new('GRADE', [true, 'Output only the hostname: grade', false]),
OptBool.new('IGNOREMISMATCH', [true, 'Proceed with assessments even when the server certificate doesn\'t match the assessment hostname', true])
])
end
def report_good(line)
print_good line
end
def report_warning(line)
print_warning line
end
def report_bad(line)
print_warning line
end
def report_status(line)
print_status line
end
def output_endpoint_data(r)
ssl_protocols = [
{ id: 771, name: "TLS", version: "1.2", secure: true, active: false },
{ id: 770, name: "TLS", version: "1.1", secure: true, active: false },
{ id: 769, name: "TLS", version: "1.0", secure: true, active: false },
{ id: 768, name: "SSL", version: "3.0", secure: false, active: false },
{ id: 2, name: "SSL", version: "2.0", secure: false, active: false }
]
report_status "-----------------------------------------------------------------"
report_status "Report for #{r.server_name} (#{r.ip_address})"
report_status "-----------------------------------------------------------------"
case r.grade.to_s
when "A+", "A", "A-"
report_good "Overall rating: #{r.grade}"
when "B"
report_warning "Overall rating: #{r.grade}"
when "C", "D", "E", "F"
report_bad "Overall rating: #{r.grade}"
when "M"
report_bad "Overall rating: #{r.grade} - Certificate name mismatch"
when "T"
report_bad "Overall rating: #{r.grade} - Server's certificate is not trusted"
end
report_warning "Grade is #{r.grade_trust_ignored}, if trust issues are ignored)" if r.grade.to_s != r.grade_trust_ignored.to_s
# Supported protocols
r.details.protocols.each do |i|
p = ssl_protocols.detect { |x| x[:id] == i.id }
p.store(:active, true) if p
end
ssl_protocols.each do |proto|
if proto[:active]
if proto[:secure]
report_good "#{proto[:name]} #{proto[:version]} - Yes"
else
report_bad "#{proto[:name]} #{proto[:version]} - Yes"
end
else
report_good "#{proto[:name]} #{proto[:version]} - No"
end
end
# Renegotiation
case
when r.details.reneg_support == 0
report_warning "Secure renegotiation is not supported"
when r.details.reneg_support[0] == 1
report_bad "Insecure client-initiated renegotiation is supported"
when r.details.reneg_support[1] == 1
report_good "Secure renegotiation is supported"
when r.details.reneg_support[2] == 1
report_warning "Secure client-initiated renegotiation is supported"
when r.details.reneg_support[3] == 1
report_warning "Server requires secure renegotiation support"
end
# BEAST
if r.details.vuln_beast?
report_bad "BEAST attack - Yes"
else
report_good "BEAST attack - No"
end
# POODLE (SSLv3)
if r.details.poodle?
report_bad "POODLE SSLv3 - Vulnerable"
else
report_good "POODLE SSLv3 - Not vulnerable"
end
# POODLE TLS
case r.details.poodle_tls
when -1
report_warning "POODLE TLS - Test failed"
when 0
report_warning "POODLE TLS - Unknown"
when 1
report_good "POODLE TLS - Not vulnerable"
when 2
report_bad "POODLE TLS - Vulnerable"
end
# Downgrade attack prevention
if r.details.fallback_scsv?
report_good "Downgrade attack prevention - Yes, TLS_FALLBACK_SCSV supported"
else
report_bad "Downgrade attack prevention - No, TLS_FALLBACK_SCSV not supported"
end
# Freak
if r.details.freak?
report_bad "Freak - Vulnerable"
else
report_good "Freak - Not vulnerable"
end
# RC4
if r.details.supports_rc4?
report_warning "RC4 - Server supports at least one RC4 suite"
else
report_good "RC4 - No"
end
# RC4 with modern browsers
report_warning "RC4 is used with modern clients" if r.details.rc4_with_modern?
# Heartbeat
if r.details.heartbeat?
report_status "Heartbeat (extension) - Yes"
else
report_status "Heartbeat (extension) - No"
end
# Heartbleed
if r.details.heartbleed?
report_bad "Heartbleed (vulnerability) - Yes"
else
report_good "Heartbleed (vulnerability) - No"
end
# OpenSSL CCS
case r.details.open_ssl_ccs
when -1
report_warning "OpenSSL CCS vulnerability (CVE-2014-0224) - Test failed"
when 0
report_warning "OpenSSL CCS vulnerability (CVE-2014-0224) - Unknown"
when 1
report_good "OpenSSL CCS vulnerability (CVE-2014-0224) - No"
when 2
report_bad "OpenSSL CCS vulnerability (CVE-2014-0224) - Possibly vulnerable, but not exploitable"
when 3
report_bad "OpenSSL CCS vulnerability (CVE-2014-0224) - Vulnerable and exploitable"
end
# Forward Secrecy
case
when r.details.forward_secrecy == 0
report_bad "Forward Secrecy - No"
when r.details.forward_secrecy[0] == 1
report_bad "Forward Secrecy - With some browsers"
when r.details.forward_secrecy[1] == 1
report_good "Forward Secrecy - With modern browsers"
when r.details.forward_secrecy[2] == 1
report_good "Forward Secrecy - Yes (with most browsers)"
end
# HSTS
if r.details.sts_response_header
str = "Strict Transport Security (HSTS) - Yes"
if r.details.sts_max_age && r.details.sts_max_age != -1
str += ":max-age=#{r.details.sts_max_age}"
end
str += ":includeSubdomains" if r.details.sts_subdomains?
report_good str
else
report_bad "Strict Transport Security (HSTS) - No"
end
# HPKP
if r.details.pkp_response_header
report_good "Public Key Pinning (HPKP) - Yes"
else
report_warning "Public Key Pinning (HPKP) - No"
end
# Compression
if r.details.compression_methods == 0
report_good "Compression - No"
elsif (r.details.session_tickets & 1) != 0
report_warning "Compression - Yes (Deflate)"
end
# Session Resumption
case r.details.session_resumption
when 0
print_status "Session resumption - No"
when 1
report_warning "Session resumption - No (IDs assigned but not accepted)"
when 2
print_status "Session resumption - Yes"
end
# Session Tickets
case
when r.details.session_tickets == 0
print_status "Session tickets - No"
when r.details.session_tickets[0] == 1
print_status "Session tickets - Yes"
when r.details.session_tickets[1] == 1
report_good "Session tickets - Implementation is faulty"
when r.details.session_tickets[2] == 1
report_warning "Session tickets - Server is intolerant to the extension"
end
# OCSP stapling
if r.details.ocsp_stapling?
print_status "OCSP Stapling - Yes"
else
print_status "OCSP Stapling - No"
end
# NPN
if r.details.supports_npn?
print_status "Next Protocol Negotiation (NPN) - Yes (#{r.details.npn_protocols})"
else
print_status "Next Protocol Negotiation (NPN) - No"
end
# SNI
print_status "SNI Required - Yes" if r.details.sni_required?
end
def output_grades_only(r)
r.endpoints.each do |e|
if e.status_message == "Ready"
print_status "Server: #{e.server_name} (#{e.ip_address}) - Grade:#{e.grade}"
else
print_status "Server: #{e.server_name} (#{e.ip_address} - Status:#{e.status_message}"
end
end
end
def output_common_info(r)
return unless r
print_status "Host: #{r.host}"
r.endpoints.each do |e|
print_status "\t #{e.ip_address}"
end
end
def output_result(r, grade)
return unless r
output_common_info(r)
if grade
output_grades_only(r)
else
r.endpoints.each do |e|
if e.status_message == "Ready"
output_endpoint_data(e)
else
print_status "#{e.status_message}"
end
end
end
end
def output_testing_details(r)
return unless r.status == "IN_PROGRESS"
if r.endpoints.length == 1
print_status "#{r.host} (#{r.endpoints[0].ip_address}) - Progress #{[r.endpoints[0].progress, 0].max}% (#{r.endpoints[0].status_details_message})"
elsif r.endpoints.length > 1
in_progress_srv_num = 0
ready_srv_num = 0
pending_srv_num = 0
r.endpoints.each do |e|
case e.status_message.to_s
when "In progress"
in_progress_srv_num += 1
print_status "Scanned host: #{e.ip_address} (#{e.server_name})- #{[e.progress, 0].max}% complete (#{e.status_details_message})"
when "Pending"
pending_srv_num += 1
when "Ready"
ready_srv_num += 1
end
end
progress = ((ready_srv_num.to_f / (pending_srv_num + in_progress_srv_num + ready_srv_num)) * 100.0).round(0)
print_status "Ready: #{ready_srv_num}, In progress: #{in_progress_srv_num}, Pending: #{pending_srv_num}"
print_status "#{r.host} - Progress #{progress}%"
end
end
def valid_hostname?(hostname)
hostname =~ /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/
end
def run
delay = datastore['DELAY']
hostname = datastore['HOSTNAME']
unless valid_hostname?(hostname)
print_status "Invalid hostname"
return
end
usecache = datastore['USECACHE']
grade = datastore['GRADE']
# Use cached results
if usecache
from_cache = 'on'
start_new = 'off'
else
from_cache = 'off'
start_new = 'on'
end
# Ignore mismatch
ignore_mismatch = datastore['IGNOREMISMATCH'] ? 'on' : 'off'
api = Api.new
info = api.info
print_status "SSL Labs API info"
print_status "API version: #{info.engine_version}"
print_status "Evaluation criteria: #{info.criteria_version}"
print_status "Running assessments: #{info.current_assessments} (max #{info.max_assessments})"
if api.current_assessments >= api.max_assessments
print_status "Too many active assessments"
return
end
if usecache
r = api.analyse(host: hostname, fromCache: from_cache, ignoreMismatch: ignore_mismatch, all: 'done')
else
r = api.analyse(host: hostname, startNew: start_new, ignoreMismatch: ignore_mismatch, all: 'done')
end
loop do
case r.status
when "DNS"
print_status "Server: #{r.host} - #{r.status_message}"
when "IN_PROGRESS"
output_testing_details(r)
when "READY"
output_result(r, grade)
return
when "ERROR"
print_error "#{r.status_message}"
return
else
print_error "Unknown assessment status"
return
end
sleep delay
r = api.analyse(host: hostname, all: 'done')
end
rescue RequestRateTooHigh
print_error "Request rate is too high, please slow down"
rescue InternalError
print_error "Service encountered an error, sleep 5 minutes"
rescue ServiceNotAvailable
print_error "Service is not available, sleep 15 minutes"
rescue ServiceOverloaded
print_error "Service is overloaded, sleep 30 minutes"
rescue
print_error "Invalid parameters"
end
end
7.4 High
CVSS3
Attack Vector
NETWORK
Attack Complexity
HIGH
Privileges Required
NONE
User Interaction
NONE
Scope
UNCHANGED
Confidentiality Impact
HIGH
Integrity Impact
HIGH
Availability Impact
NONE
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
8.2 High
AI Score
Confidence
High
5.8 Medium
CVSS2
Access Vector
NETWORK
Access Complexity
MEDIUM
Authentication
NONE
Confidentiality Impact
PARTIAL
Integrity Impact
PARTIAL
Availability Impact
NONE
AV:N/AC:M/Au:N/C:P/I:P/A:N
0.974 High
EPSS
Percentile
99.9%