## # 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 info Info.load request(:info) end def analyse(params = {}) Host.load request(:analyze, params) end def get_endpoint_data(params = {}) Endpoint.load request(:get_endpoint_data, params) end def get_status_codes StatusCodes.load request(:get_status_codes) 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 attributes.each do |name, value| if @fields.include?(name) obj.instance_variable_set("@#{name}", value) elsif @lists.key?(name) obj.instance_variable_set("@#{name}", value.map { |v| @lists[name].load(v) }) unless value.nil? elsif @refs.key?(name) obj.instance_variable_set("@#{name}", @refs[name].load(value)) unless value.nil? else fail ArgumentError, "#{name} is not an attribute of object #{self.name}" end end obj 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? 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 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 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 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 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 ', '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 # Renegotioation 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}\n" 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 print_error "Invalid parameters" 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" end end