Lucene search

K
packetstormDenis Kolegov, Francois Chagnon, metasploit.comPACKETSTORM:180622
HistoryAug 31, 2024 - 12:00 a.m.

SSL Labs API Client

2024-08-3100:00:00
Denis Kolegov, Francois Chagnon, metasploit.com
packetstormsecurity.com
44
ssl labs
metasploit
api
security
vulnerabilities
assessment

CVSS2

5.8

Attack Vector

NETWORK

Attack 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

CVSS3

7.4

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

AI Score

7.5

Confidence

Low

EPSS

0.973

Percentile

99.9%

`##  
# 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  
`

CVSS2

5.8

Attack Vector

NETWORK

Attack 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

CVSS3

7.4

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

AI Score

7.5

Confidence

Low

EPSS

0.973

Percentile

99.9%