|
4 | 4 | ##
|
5 | 5 |
|
6 | 6 | class MetasploitModule < Msf::Auxiliary
|
7 |
| - |
8 | 7 | include Msf::Exploit::Remote::Tcp
|
9 | 8 | include Msf::Auxiliary::Scanner
|
10 | 9 | include Msf::Auxiliary::Report
|
@@ -81,80 +80,6 @@ def initialize
|
81 | 80 | )
|
82 | 81 | end
|
83 | 82 |
|
84 |
| - def get_metasploit_ssl_versions |
85 |
| - # There are two ways to generate a list of valid SSL Versions (SSLv3, TLS1.1, etc) and cipher suites (AES256-GCM-SHA384, |
86 |
| - # ECDHE-RSA-CHACHA20-POLY1305, etc). The first would be to generate them independently. It's possible to |
87 |
| - # pull all SSLContext methods (SSL Versions) via OpenSSL::SSL::SSLContext::METHODS here, as referenced in |
88 |
| - # https://github.com/rapid7/rex-socket/blob/6ea0bb3b4e19c53d73e4337617be72c0ed351ceb/lib/rex/socket/ssl_tcp.rb#L46 |
89 |
| - # then pull all ciphers with OpenSSL::Cipher.ciphers. Now in theory you have a nice easy loop: |
90 |
| - # |
91 |
| - # OpenSSL::SSL::SSLContext::METHODS.each do |ssl_version| |
92 |
| - # OpenSSL::Cipher.ciphers.each do |cipher_suite| |
93 |
| - # # do something |
94 |
| - # end |
95 |
| - # end |
96 |
| - # |
97 |
| - # However, in practice we find that OpenSSL::SSL::SSLContext::METHODS includes '_client' and '_server' variants |
98 |
| - # such as :TLSv1, :TLSv1_client, :TLSv1_server. In this case, we only need :TLSv1, so we need to remove ~2/3 of the list. |
99 |
| - # |
100 |
| - # Next, we'll find that many ciphers in OpenSSL::Cipher.ciphers are not applicable for various SSL versions. |
101 |
| - # The loop we previously looked at has (at the time of writing on Kali Rollin, msf 6.2.23) 3060 rounds. |
102 |
| - # This is a lot of iterations when we already know there are many combinations that will not be applicable for our |
103 |
| - # use. Luckily there is a 2nd way which is much more efficient. |
104 |
| - # |
105 |
| - # The OpenSSL library includes https://docs.ruby-lang.org/en/2.4.0/OpenSSL/SSL/SSLContext.html#method-i-ciphers |
106 |
| - # which we can use to generate a list of all ciphers, and SSL versions they work with. The structure is: |
107 |
| - # |
108 |
| - # [[name, version, bits, alg_bits], ...] |
109 |
| - # |
110 |
| - # which makes it very easy to just pull the 2nd element (version, or SSL version) from each list item, and unique it. |
111 |
| - # This gives us the list of all SSL versions which also have at least one working cipher on our system. |
112 |
| - # Using this method we produce no unusable SSL versions or matching cipher suites and the list is 60 items long, so 1/51 the size. |
113 |
| - # Later in get_metasploit_ssl_cipher_suites, we can grab all cipher suites to a SSL version easily by simply filtering |
114 |
| - # the 2nd element (version, or SSL version) from each list item. |
115 |
| - |
116 |
| - if datastore['SSLVersion'] == 'All' |
117 |
| - return Array.new(OpenSSL::SSL::SSLContext.new.ciphers.length) { |i| (OpenSSL::SSL::SSLContext.new.ciphers[i][1]).to_s }.uniq.reverse |
118 |
| - end |
119 |
| - |
120 |
| - [datastore['SSLVersion']] |
121 |
| - end |
122 |
| - |
123 |
| - def get_metasploit_ssl_cipher_suites(ssl_version) |
124 |
| - # See comments in get_metasploit_ssl_versions for details on the use of |
125 |
| - # OpenSSL::SSL::SSLContext.new.ciphers vs other methods to generate |
126 |
| - # valid ciphers for a given SSL version |
127 |
| - |
128 |
| - # First find all valid ciphers that the Metasploit host supports. |
129 |
| - # Also transform the SSL version to a standard format. |
130 |
| - ssl_version = ssl_version.to_s.gsub('_', '.') |
131 |
| - all_ciphers = OpenSSL::SSL::SSLContext.new.ciphers |
132 |
| - valid_ciphers = [] |
133 |
| - |
134 |
| - # For each cipher that the Metasploit host supports, determine if that cipher |
135 |
| - # is supported for use with the SSL version passed into this function. If it is, |
136 |
| - # then add it to the valid_ciphers list. |
137 |
| - all_ciphers.each do |cipher| |
138 |
| - # cipher list has struct of [cipher, ssl_version, <int>, <int>] |
139 |
| - if cipher[1] == ssl_version |
140 |
| - valid_ciphers << cipher[0] |
141 |
| - end |
142 |
| - end |
143 |
| - |
144 |
| - # If the user wants to use all ciphers then return all valid ciphers. |
145 |
| - # Otherwise return only the one that matches the one the user specified |
146 |
| - # in the SSLCipher datastore option. |
147 |
| - # |
148 |
| - # If no match is found for some reason then we will return an empty array. |
149 |
| - if datastore['SSLCipher'] == 'All' |
150 |
| - return valid_ciphers |
151 |
| - elsif valid_ciphers.contains? datastore['SSLCipher'] |
152 |
| - return [datastore['SSLCipher']] |
153 |
| - end |
154 |
| - |
155 |
| - [] |
156 |
| - end |
157 |
| - |
158 | 83 | def public_key_size(cert)
|
159 | 84 | if cert.public_key.respond_to? :n
|
160 | 85 | return cert.public_key.n.num_bytes * 8
|
@@ -241,6 +166,22 @@ def print_cert(cert, ip)
|
241 | 166 | end
|
242 | 167 | end
|
243 | 168 |
|
| 169 | + # Process certificate with enhanced analysis |
| 170 | + def process_certificate(ip, cert) |
| 171 | + print_cert(cert, ip) |
| 172 | + |
| 173 | + # Store certificate in loot with rex-sslscan metadata |
| 174 | + loot_cert = store_loot( |
| 175 | + 'ssl.certificate.rex_sslscan', |
| 176 | + 'application/x-pem-file', |
| 177 | + ip, |
| 178 | + cert.to_pem, |
| 179 | + "ssl_cert_#{ip}_#{rport}.pem", |
| 180 | + "SSL Certificate from #{ip}:#{rport}" |
| 181 | + ) |
| 182 | + print_good("Certificate saved to loot: #{loot_cert}") |
| 183 | + end |
| 184 | + |
244 | 185 | def check_vulnerabilities(ip, ssl_version, ssl_cipher, cert)
|
245 | 186 | # POODLE
|
246 | 187 | if ssl_version == 'SSLv3'
|
@@ -368,31 +309,6 @@ def check_vulnerabilities(ip, ssl_version, ssl_cipher, cert)
|
368 | 309 |
|
369 | 310 | return if cert.nil?
|
370 | 311 |
|
371 |
| - key_size = public_key_size(cert) |
372 |
| - if key_size > 0 |
373 |
| - if key_size == 1024 |
374 |
| - print_good('Public Key only 1024 bits') |
375 |
| - report_vuln( |
376 |
| - host: ip, |
377 |
| - port: rport, |
378 |
| - proto: 'tcp', |
379 |
| - name: name, |
380 |
| - info: "Module #{fullname} confirmed certificate key size 1024 bits", |
381 |
| - refs: ['CWE-326'] |
382 |
| - ) |
383 |
| - elsif key_size < 1024 |
384 |
| - print_good('Public Key < 1024 bits') |
385 |
| - report_vuln( |
386 |
| - host: ip, |
387 |
| - port: rport, |
388 |
| - proto: 'tcp', |
389 |
| - name: name, |
390 |
| - info: "Module #{fullname} confirmed certificate key size < 1024 bits", |
391 |
| - refs: ['CWE-326'] |
392 |
| - ) |
393 |
| - end |
394 |
| - end |
395 |
| - |
396 | 312 | # certificate signed md5
|
397 | 313 | alg = cert.signature_algorithm
|
398 | 314 |
|
@@ -435,88 +351,143 @@ def check_vulnerabilities(ip, ssl_version, ssl_cipher, cert)
|
435 | 351 | end
|
436 | 352 | end
|
437 | 353 |
|
| 354 | + # Enhanced vulnerability checking leveraging rex-sslscan data |
| 355 | + def check_vulnerabilities_enhanced(ip, ssl_version, cipher_name, cert, is_weak_cipher) |
| 356 | + check_vulnerabilities(ip, ssl_version, cipher_name, cert) |
| 357 | + |
| 358 | + if is_weak_cipher |
| 359 | + print_good("#{ip}:#{rport} - Weak cipher detected: #{cipher_name}") |
| 360 | + report_vuln( |
| 361 | + host: ip, |
| 362 | + port: rport, |
| 363 | + proto: 'tcp', |
| 364 | + name: name, |
| 365 | + info: "Module #{fullname} detected weak cipher: #{cipher_name}", |
| 366 | + refs: ['CWE-327'] |
| 367 | + ) |
| 368 | + end |
| 369 | + end |
| 370 | + |
| 371 | + # Store comprehensive rex-sslscan results |
| 372 | + def store_rex_sslscan_results(ip, scan_result) |
| 373 | + # Create detailed report |
| 374 | + report_data = { |
| 375 | + host: ip, |
| 376 | + port: rport, |
| 377 | + scan_timestamp: Time.now.utc, |
| 378 | + ssl_versions: { |
| 379 | + sslv2_supported: scan_result.supports_sslv2?, |
| 380 | + sslv3_supported: scan_result.supports_sslv3?, |
| 381 | + tlsv1_supported: scan_result.supports_tlsv1?, |
| 382 | + tlsv1_1_supported: scan_result.supports_tlsv1_1?, |
| 383 | + tlsv1_2_supported: scan_result.supports_tlsv1_2? |
| 384 | + }, |
| 385 | + cipher_summary: { |
| 386 | + total_accepted: scan_result.accepted.length, |
| 387 | + total_rejected: scan_result.rejected.length, |
| 388 | + weak_ciphers: scan_result.weak_ciphers.length, |
| 389 | + strong_ciphers: scan_result.strong_ciphers.length |
| 390 | + }, |
| 391 | + detailed_ciphers: scan_result.ciphers.to_a |
| 392 | + } |
| 393 | + |
| 394 | + # Store as JSON loot |
| 395 | + loot_file = store_loot( |
| 396 | + 'ssl.scan.rex_sslscan', |
| 397 | + 'application/json', |
| 398 | + ip, |
| 399 | + report_data.to_json, |
| 400 | + "ssl_scan_#{ip}_#{rport}.json", |
| 401 | + "Rex::SSLScan results for #{ip}:#{rport}" |
| 402 | + ) |
| 403 | + print_good("Detailed scan results saved to loot: #{loot_file}") |
| 404 | + end |
| 405 | + |
| 406 | + # Process rex-sslscan results |
| 407 | + def process_rex_sslscan_results(ip, scan_result) |
| 408 | + # Report certificate if available |
| 409 | + if scan_result.cert |
| 410 | + process_certificate(ip, scan_result.cert) |
| 411 | + end |
| 412 | + |
| 413 | + # Process accepted ciphers by version |
| 414 | + %i[SSLv2 SSLv3 TLSv1 TLSv1_1 TLSv1_2].each do |version| |
| 415 | + accepted_ciphers = scan_result.accepted(version) |
| 416 | + next if accepted_ciphers.empty? |
| 417 | + |
| 418 | + print_good("#{ip}:#{rport} - #{version} supported with #{accepted_ciphers.length} cipher(s)") |
| 419 | + |
| 420 | + key_size = public_key_size(scan_result.cert) |
| 421 | + if key_size > 0 |
| 422 | + if key_size == 1024 |
| 423 | + print_good('Public Key only 1024 bits') |
| 424 | + report_vuln( |
| 425 | + host: ip, |
| 426 | + port: rport, |
| 427 | + proto: 'tcp', |
| 428 | + name: name, |
| 429 | + info: "Module #{fullname} confirmed certificate key size 1024 bits", |
| 430 | + refs: ['CWE-326'] |
| 431 | + ) |
| 432 | + elsif key_size < 1024 |
| 433 | + print_good('Public Key < 1024 bits') |
| 434 | + report_vuln( |
| 435 | + host: ip, |
| 436 | + port: rport, |
| 437 | + proto: 'tcp', |
| 438 | + name: name, |
| 439 | + info: "Module #{fullname} confirmed certificate key size < 1024 bits", |
| 440 | + refs: ['CWE-326'] |
| 441 | + ) |
| 442 | + end |
| 443 | + end |
| 444 | + |
| 445 | + accepted_ciphers.each do |cipher_info| |
| 446 | + cipher_name = cipher_info[:cipher] |
| 447 | + key_length = cipher_info[:key_length] |
| 448 | + is_weak = cipher_info[:weak] |
| 449 | + |
| 450 | + # Report the cipher |
| 451 | + print_status(" #{version}: #{cipher_name} (#{key_length} bits)#{is_weak ? ' - WEAK' : ''}") |
| 452 | + |
| 453 | + # Check for vulnerabilities using existing logic |
| 454 | + check_vulnerabilities_enhanced(ip, version.to_s, cipher_name, scan_result.cert, is_weak) |
| 455 | + end |
| 456 | + end |
| 457 | + |
| 458 | + # Report weak ciphers summary |
| 459 | + weak_ciphers = scan_result.weak_ciphers |
| 460 | + if weak_ciphers.any? |
| 461 | + print_bad("#{ip}:#{rport} - #{weak_ciphers.length} weak cipher(s) detected") |
| 462 | + end |
| 463 | + |
| 464 | + # Store comprehensive scan results in loot |
| 465 | + store_rex_sslscan_results(ip, scan_result) |
| 466 | + end |
| 467 | + |
438 | 468 | # Fingerprint a single host
|
439 | 469 | def run_host(ip)
|
440 |
| - # Get the available SSL/TLS versions that that Metasploit host supports |
441 |
| - versions = get_metasploit_ssl_versions |
442 |
| - |
443 |
| - certs_found = {} |
444 |
| - skip_ssl_version = false |
445 |
| - vprint_status("Scanning #{ip} for: #{versions.map(&:to_s).join(', ')}") |
446 |
| - |
447 |
| - # For each SSL/TLS version... |
448 |
| - versions.each do |version| |
449 |
| - skip_ssl_version = false |
450 |
| - |
451 |
| - # Get the cipher suites that SSL/TLS can use on the Metasploit host |
452 |
| - # and print them out. |
453 |
| - ciphers = get_metasploit_ssl_cipher_suites(version) |
454 |
| - vprint_status("Scanning #{ip} #{version} with ciphers: #{ciphers.map(&:to_s).join(', ')}") |
455 |
| - |
456 |
| - # For each cipher attempt to connect to the server. If we could connect with the given SSL version, |
457 |
| - # then skip it and move onto the next one. If the cipher isn't supported, then note this. |
458 |
| - # If the server responds with a peer certificate, make a new certificate object from it and find |
459 |
| - # its fingerprint, then check it for vulnerabilities, before saving it to loot if it hasn't been |
460 |
| - # saved already (check done using the certificate's SHA1 hash). |
461 |
| - # |
462 |
| - # In all cases the SSL version and cipher combination will also be checked for vulnerabilities |
463 |
| - # using the check_vulnerabilities function. |
464 |
| - ciphers.each do |cipher| |
465 |
| - break if skip_ssl_version |
466 |
| - |
467 |
| - vprint_status("Attempting connection with SSL Version: #{version}, Cipher: #{cipher}") |
468 |
| - begin |
469 |
| - # setting the connect global to false means we can't see the socket, therefore the cert |
470 |
| - connect(true, { 'SSL' => true, 'SSLVersion' => version.sub('.', '_').to_sym, 'SSLCipher' => cipher }) # Force SSL |
471 |
| - print_good("Connected with SSL Version: #{version}, Cipher: #{cipher}") |
472 |
| - |
473 |
| - if sock.respond_to? :peer_cert |
474 |
| - cert = OpenSSL::X509::Certificate.new(sock.peer_cert) |
475 |
| - # https://stackoverflow.com/questions/16516555/ruby-code-for-openssl-to-generate-fingerprint |
476 |
| - cert_fingerprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s |
477 |
| - if certs_found.key? cert_fingerprint |
478 |
| - # dont check the cert more than once if its the same cert |
479 |
| - check_vulnerabilities(ip, version, cipher, nil) |
480 |
| - else |
481 |
| - loot_cert = store_loot('ssl.certificate', 'text/plain', ip, cert.to_text) |
482 |
| - print_good("Certificate saved to loot: #{loot_cert}") |
483 |
| - print_cert(cert, ip) |
484 |
| - check_vulnerabilities(ip, version, cipher, cert) |
485 |
| - end |
486 |
| - certs_found[cert_fingerprint] = cert |
487 |
| - end |
488 |
| - rescue ::OpenSSL::SSL::SSLError => e |
489 |
| - error_message = e.message.match(/ state=(.+)$/) |
490 |
| - |
491 |
| - if error_message.nil? |
492 |
| - vprint_error("\tSSL Connection Error: #{e}") |
493 |
| - next |
494 |
| - end |
495 |
| - |
496 |
| - # catch if the ssl_version/protocol isn't allowed and then we can skip out of it. |
497 |
| - if error_message[1].include? 'no protocols available' |
498 |
| - skip_ssl_version = true |
499 |
| - vprint_error("\tDoesn't accept #{version} connections, Skipping") |
500 |
| - break |
501 |
| - end |
502 |
| - vprint_error("\tDoes not accept #{version} using cipher #{cipher}, error message: #{error_message[1]}") |
503 |
| - rescue ArgumentError => e |
504 |
| - if e.message.match(%r{This version of Ruby does not support the requested SSL/TLS version}) |
505 |
| - skip_ssl_version = true |
506 |
| - vprint_error("\t#{e.message}, Skipping") |
507 |
| - break |
508 |
| - end |
509 |
| - print_error("Exception encountered: #{e}") |
510 |
| - rescue StandardError => e |
511 |
| - if e.message.match(/connection was refused/) || e.message.match(/timed out/) |
512 |
| - print_error("\tPort closed or timeout occurred.") |
513 |
| - return 'Port closed or timeout occurred.' |
514 |
| - end |
515 |
| - print_error("\tException encountered: #{e}") |
516 |
| - ensure |
517 |
| - disconnect |
518 |
| - end |
| 470 | + print_status("Starting enhanced SSL/TLS scan of #{ip}:#{rport}") |
| 471 | + |
| 472 | + begin |
| 473 | + ctx = { 'Msf' => framework, 'MsfExploit' => self } |
| 474 | + # Initialize rex-sslscan scanner |
| 475 | + scanner = Rex::SSLScan::Scanner.new(ip, rport, ctx) |
| 476 | + |
| 477 | + # Perform the scan |
| 478 | + scan_result = scanner.scan |
| 479 | + |
| 480 | + # Check if SSL/TLS is supported |
| 481 | + unless scan_result.supports_ssl? |
| 482 | + print_error("#{ip}:#{rport} - Server does not appear to support SSL/TLS") |
| 483 | + return |
519 | 484 | end
|
| 485 | + |
| 486 | + # Process and report results |
| 487 | + process_rex_sslscan_results(ip, scan_result) |
| 488 | + rescue StandardError => e |
| 489 | + print_error("#{ip}:#{rport} - Scan error: #{e.message}") |
| 490 | + vprint_error("#{ip}:#{rport} - Backtrace: #{e.backtrace}") |
520 | 491 | end
|
521 | 492 | end
|
522 | 493 | end
|
0 commit comments