Share
## https://sploitus.com/exploit?id=PACKETSTORM:180622
##  
# 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