|
| 1 | + |
| 2 | +require 'rex/socket' |
| 3 | +require 'rex/ui/text/table' |
| 4 | + |
| 5 | +module Rex::SSLScan |
| 6 | +class Result |
| 7 | + |
| 8 | + attr_accessor :openssl_sslv2 |
| 9 | + |
| 10 | + attr_reader :ciphers |
| 11 | + attr_reader :supported_versions |
| 12 | + |
| 13 | + def initialize() |
| 14 | + @cert = nil |
| 15 | + @ciphers = Set.new |
| 16 | + @supported_versions = [:SSLv2, :SSLv3, :TLSv1] |
| 17 | + end |
| 18 | + |
| 19 | + def cert |
| 20 | + @cert |
| 21 | + end |
| 22 | + |
| 23 | + def cert=(input) |
| 24 | + unless input.kind_of? OpenSSL::X509::Certificate or input.nil? |
| 25 | + raise ArgumentError, "Must be an X509 Cert!" |
| 26 | + end |
| 27 | + @cert = input |
| 28 | + end |
| 29 | + |
| 30 | + def sslv2 |
| 31 | + @ciphers.reject{|cipher| cipher[:version] != :SSLv2 } |
| 32 | + end |
| 33 | + |
| 34 | + def sslv3 |
| 35 | + @ciphers.reject{|cipher| cipher[:version] != :SSLv3 } |
| 36 | + end |
| 37 | + |
| 38 | + def tlsv1 |
| 39 | + @ciphers.reject{|cipher| cipher[:version] != :TLSv1 } |
| 40 | + end |
| 41 | + |
| 42 | + def weak_ciphers |
| 43 | + accepted.reject{|cipher| cipher[:weak] == false } |
| 44 | + end |
| 45 | + |
| 46 | + def strong_ciphers |
| 47 | + accepted.reject{|cipher| cipher[:weak] } |
| 48 | + end |
| 49 | + |
| 50 | + # Returns all accepted ciphers matching the supplied version |
| 51 | + # @param version [Symbol, Array] The SSL Version to filter on |
| 52 | + # @raise [ArgumentError] if the version supplied is invalid |
| 53 | + # @return [Array] An array of accepted cipher details matching the supplied versions |
| 54 | + def accepted(version = :all) |
| 55 | + enum_ciphers(:accepted, version) |
| 56 | + end |
| 57 | + |
| 58 | + # Returns all rejected ciphers matching the supplied version |
| 59 | + # @param version [Symbol, Array] The SSL Version to filter on |
| 60 | + # @raise [ArgumentError] if the version supplied is invalid |
| 61 | + # @return [Array] An array of rejected cipher details matching the supplied versions |
| 62 | + def rejected(version = :all) |
| 63 | + enum_ciphers(:rejected, version) |
| 64 | + end |
| 65 | + |
| 66 | + def each_accepted(version = :all) |
| 67 | + accepted(version).each do |cipher_result| |
| 68 | + yield cipher_result |
| 69 | + end |
| 70 | + end |
| 71 | + |
| 72 | + def each_rejected(version = :all) |
| 73 | + rejected(version).each do |cipher_result| |
| 74 | + yield cipher_result |
| 75 | + end |
| 76 | + end |
| 77 | + |
| 78 | + def supports_sslv2? |
| 79 | + !(accepted(:SSLv2).empty?) |
| 80 | + end |
| 81 | + |
| 82 | + def supports_sslv3? |
| 83 | + !(accepted(:SSLv3).empty?) |
| 84 | + end |
| 85 | + |
| 86 | + def supports_tlsv1? |
| 87 | + !(accepted(:TLSv1).empty?) |
| 88 | + end |
| 89 | + |
| 90 | + def supports_ssl? |
| 91 | + supports_sslv2? or supports_sslv3? or supports_tlsv1? |
| 92 | + end |
| 93 | + |
| 94 | + def supports_weak_ciphers? |
| 95 | + !(weak_ciphers.empty?) |
| 96 | + end |
| 97 | + |
| 98 | + def standards_compliant? |
| 99 | + if supports_ssl? |
| 100 | + return false if supports_sslv2? |
| 101 | + return false if supports_weak_ciphers? |
| 102 | + end |
| 103 | + true |
| 104 | + end |
| 105 | + |
| 106 | + # Adds the details of a cipher test to the Result object. |
| 107 | + # @param version [Symbol] the SSL Version |
| 108 | + # @param cipher [String] the SSL cipher |
| 109 | + # @param key_length [Fixnum] the length of encryption key |
| 110 | + # @param status [Symbol] :accepted or :rejected |
| 111 | + def add_cipher(version, cipher, key_length, status) |
| 112 | + unless @supported_versions.include? version |
| 113 | + raise ArgumentError, "Must be a supported SSL Version" |
| 114 | + end |
| 115 | + unless OpenSSL::SSL::SSLContext.new(version).ciphers.flatten.include? cipher |
| 116 | + raise ArgumentError, "Must be a valid SSL Cipher for #{version}!" |
| 117 | + end |
| 118 | + unless key_length.kind_of? Fixnum |
| 119 | + raise ArgumentError, "Must supply a valid key length" |
| 120 | + end |
| 121 | + unless [:accepted, :rejected].include? status |
| 122 | + raise ArgumentError, "Status must be either :accepted or :rejected" |
| 123 | + end |
| 124 | + |
| 125 | + strong_cipher_ctx = OpenSSL::SSL::SSLContext.new(version) |
| 126 | + # OpenSSL Directive For Strong Ciphers |
| 127 | + # See: http://www.rapid7.com/vulndb/lookup/ssl-weak-ciphers |
| 128 | + strong_cipher_ctx.ciphers = "ALL:!aNULL:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM" |
| 129 | + |
| 130 | + if strong_cipher_ctx.ciphers.flatten.include? cipher |
| 131 | + weak = false |
| 132 | + else |
| 133 | + weak = true |
| 134 | + end |
| 135 | + |
| 136 | + cipher_details = {:version => version, :cipher => cipher, :key_length => key_length, :weak => weak, :status => status} |
| 137 | + @ciphers << cipher_details |
| 138 | + end |
| 139 | + |
| 140 | + def to_s |
| 141 | + unless supports_ssl? |
| 142 | + return "Server does not appear to support SSL on this port!" |
| 143 | + end |
| 144 | + table = Rex::Ui::Text::Table.new( |
| 145 | + 'Header' => 'SSL Ciphers', |
| 146 | + 'Indent' => 1, |
| 147 | + 'Columns' => ['Status', 'Weak', 'SSL Version', 'Key Length', 'Cipher'], |
| 148 | + 'SortIndex' => -1 |
| 149 | + ) |
| 150 | + ciphers.each do |cipher| |
| 151 | + if cipher[:weak] |
| 152 | + weak = '*' |
| 153 | + else |
| 154 | + weak = ' ' |
| 155 | + end |
| 156 | + table << [cipher[:status].to_s.capitalize, weak , cipher[:version], cipher[:key_length], cipher[:cipher]] |
| 157 | + end |
| 158 | + |
| 159 | + # Sort by SSL Version, then Key Length, and then Status |
| 160 | + table.rows.sort_by!{|row| [row[0],row[2],row[3]]} |
| 161 | + text = "#{table.to_s}" |
| 162 | + if @cert |
| 163 | + text << " \n\n #{@cert.to_text}" |
| 164 | + end |
| 165 | + if openssl_sslv2 == false |
| 166 | + text << "\n\n *** WARNING: Your OS hates freedom! Your OpenSSL libs are compiled without SSLv2 support!" |
| 167 | + end |
| 168 | + text |
| 169 | + end |
| 170 | + |
| 171 | + protected |
| 172 | + |
| 173 | + # @param state [Symbol] Either :accepted or :rejected |
| 174 | + # @param version [Symbol, Array] The SSL Version to filter on (:SSLv2, :SSLv3, :TLSv1, :all) |
| 175 | + # @return [Set] The Set of cipher results matching the filter criteria |
| 176 | + def enum_ciphers(state, version = :all) |
| 177 | + case version |
| 178 | + when Symbol |
| 179 | + case version |
| 180 | + when :all |
| 181 | + return @ciphers.select{|cipher| cipher[:status] == state} |
| 182 | + when :SSLv2, :SSLv3, :TLSv1 |
| 183 | + return @ciphers.select{|cipher| cipher[:status] == state and cipher[:version] == version} |
| 184 | + else |
| 185 | + raise ArgumentError, "Invalid SSL Version Supplied: #{version}" |
| 186 | + end |
| 187 | + when Array |
| 188 | + version = version.reject{|v| !(@supported_versions.include? v)} |
| 189 | + if version.empty? |
| 190 | + return @ciphers.select{|cipher| cipher[:status] == state} |
| 191 | + else |
| 192 | + return @ciphers.select{|cipher| cipher[:status] == state and version.include? cipher[:version]} |
| 193 | + end |
| 194 | + else |
| 195 | + raise ArgumentError, "Was expecting Symbol or Array and got #{version.class}" |
| 196 | + end |
| 197 | + end |
| 198 | + |
| 199 | +end |
| 200 | +end |
0 commit comments