diff --git a/nselib/tls.lua b/nselib/tls.lua
index fcef5d7c7..9d64ca7c6 100644
--- a/nselib/tls.lua
+++ b/nselib/tls.lua
@@ -583,6 +583,18 @@ CIPHERS = {
["SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA"] = 0xFEFF,
}
+local function find_key(t, value)
+ local k, v
+
+ for k, v in pairs(t) do
+ if v == value then
+ return k
+ end
+ end
+
+ return nil
+end
+
-- Keep this local to enforce use of the cipher_info function
local cipher_info_cache = {
-- pre-populate the special cases that break the parser below
@@ -637,13 +649,323 @@ local cipher_info_cache = {
},
}
+
+-- A couple helpers for server_key_exchange parsing
+local function unpack_dhparams (blob, pos)
+ local p, g, y
+ pos, p, g, y = bin.unpack(">PPP", blob)
+ return pos, {p=p, g=g, y=y}, rsa_equiv("dh", #p)
+end
+
+local function unpack_ecdhparams (blob, pos)
+ local eccurvetype
+ pos, eccurvetype = bin.unpack("C", blob, pos)
+ local ret = {}
+ local strength
+ if eccurvetype == 1 then
+ local p, a, b, base, order, cofactor
+ pos, p, a, b, base, order, cofactor = bin.unpack("pppppp", blob, pos)
+ strength = rsa_equiv("ec", #p)
+ ret.curve_params = {
+ ec_curve_type = "explicit_prime",
+ prime_p=p, curve={a=a, b=b}, base=base, order=order, cofactor=cofactor
+ }
+ elseif eccurvetype == 2 then
+ local p = {}
+ local m, basis
+ pos, m, basis = bin.unpack(">SC", blob, pos)
+ strength = rsa_equiv("ec", m)
+ if basis == 1 then -- ec_trinomial
+ pos, p.k = bin.unpack("p", blob, pos)
+ elseif basis == 2 then -- ec_pentanomial
+ pos, p.k1, p.k2, p.k3 = bin.unpack("ppp", blob, pos)
+ end
+ local a, b, base, order, cofactor
+ pos, a, b, base, order, cofactor = bin.unpack("ppppp", blob, pos)
+ ret.curve_params = {
+ ec_curve_type = "explicit_char2",
+ m=m, basis=basis, field=p, curve={a=a, b=b}, base=base, order=order, cofactor=cofactor
+ }
+ elseif eccurvetype == 3 then
+ local curve
+ pos, curve = bin.unpack(">S", blob, pos)
+ ret.curve_params = {
+ ec_curve_type = "namedcurve",
+ curve = find_key(ELLIPTIC_CURVES, curve)
+ }
+ local size = ret.curve_params.curve:match("(%d+)[rk]%d$")
+ if size then
+ strength = rsa_equiv("ec", tonumber(size))
+ end
+ end
+ pos, ret.public = bin.unpack("p", blob, pos)
+ return pos, ret, strength
+end
+
+local function unpack_signed (blob, pos, protocol)
+ if pos > #blob then -- not-signed
+ return pos, nil
+ end
+ local hash_alg, sig_alg, sig
+ -- TLSv1.2 changed to allow arbitrary hash and sig algorithms
+ if protocol and PROTOCOLS[protocol] >= 0x0303 then
+ pos, hash_alg, sig_alg, sig = bin.unpack("CC>P", blob, pos)
+ else
+ pos, sig = bin.unpack(">P", blob, pos)
+ end
+ return pos, {hash_algorithm=hash_alg, signature_algorithm=sig_alg, signature=sig}
+end
+
+--- Get the strength-equivalent RSA key size
+--
+-- Based on NIST SP800-57 part 1 rev 3
+-- @param ktype Key type ("dh", "ec", "rsa", "dsa")
+-- @param bits Size of key in bits
+-- @return Size in bits of RSA key with equivalent strength
+function rsa_equiv (ktype, bits)
+ if ktype == "rsa" or ktype == "dsa" or ktype == "dh" then
+ return bits
+ elseif ktype == "ec" then
+ if bits < 160 then
+ return 512 -- Possibly down to 0, but details not published
+ elseif bits < 224 then
+ return 1024
+ elseif bits < 256 then
+ return 2048
+ elseif bits < 384 then
+ return 3072
+ elseif bits < 512 then
+ return 7680
+ else -- 512+
+ return 15360
+ end
+ end
+ return nil
+end
+
+KEX_ALGORITHMS = {}
+
+-- RFC 5246
+KEX_ALGORITHMS.NULL = { anon = true }
+KEX_ALGORITHMS.DH_anon = {
+ anon = true,
+ type = "dh",
+ server_key_exchange = function (blob, protocol)
+ local pos
+ local ret = {}
+ pos, ret.dhparams, ret.strength = unpack_dhparams(blob)
+ return ret
+ end
+}
+KEX_ALGORITHMS.DH_anon_EXPORT = {
+ anon=true,
+ export=true,
+ type = "dh",
+ server_key_exchange = KEX_ALGORITHMS.DH_anon.server_key_exchange
+}
+KEX_ALGORITHMS.ECDH_anon = {
+ anon=true,
+ type = "ecdh",
+ server_key_exchange = function (blob, protocol)
+ local pos
+ local ret = {}
+ pos, ret.ecdhparams, ret.strength = unpack_ecdhparams(blob)
+ return ret
+ end
+}
+KEX_ALGORITHMS.ECDH_anon_EXPORT = {
+ anon=true,
+ export=true,
+ type = "ecdh",
+ server_key_exchange = KEX_ALGORITHMS.ECDH_anon.server_key_exchange
+}
+
+KEX_ALGORITHMS.RSA = {
+ pubkey="rsa",
+}
+-- http://www-archive.mozilla.org/projects/security/pki/nss/ssl/fips-ssl-ciphersuites.html
+KEX_ALGORITHMS.RSA_FIPS = KEX_ALGORITHMS.RSA
+KEX_ALGORITHMS.RSA_EXPORT = {
+ export=true,
+ pubkey="rsa",
+ type = "rsa",
+ server_key_exchange = function (blob, protocol)
+ local pos
+ local ret = {rsa={}}
+ pos, ret.rsa.modulus, ret.rsa.exponent = bin.unpack(">PP", blob)
+ pos, ret.signed = unpack_signed(blob, pos)
+ ret.strength = #ret.rsa.modulus
+ return ret
+ end
+}
+KEX_ALGORITHMS.RSA_EXPORT1024 = KEX_ALGORITHMS.RSA_EXPORT
+KEX_ALGORITHMS.DHE_RSA={
+ pubkey="rsa",
+ type = "dh",
+ server_key_exchange = function (blob, protocol)
+ local pos
+ local ret = {}
+ pos, ret.dhparams, ret.strength = unpack_dhparams(blob)
+ pos, ret.signed = unpack_signed(blob, pos)
+ return ret
+ end
+}
+KEX_ALGORITHMS.DHE_RSA_EXPORT={
+ export=true,
+ pubkey="rsa",
+ type = "dh",
+ server_key_exchange = KEX_ALGORITHMS.DHE_RSA.server_key_exchange
+}
+KEX_ALGORITHMS.DHE_DSS={
+ pubkey="dsa",
+ type = "dh",
+ server_key_exchange = KEX_ALGORITHMS.DHE_RSA.server_key_exchange
+}
+KEX_ALGORITHMS.DHE_DSS_EXPORT={
+ export=true,
+ pubkey="dsa",
+ type = "dh",
+ server_key_exchange = KEX_ALGORITHMS.DHE_RSA.server_key_exchange
+}
+KEX_ALGORITHMS.DHE_DSS_EXPORT1024 = KEX_ALGORITHMS.DHE_DSS_EXPORT1024
+
+KEX_ALGORITHMS.DH_DSS={
+ pubkey="dh",
+}
+KEX_ALGORITHMS.DH_DSS_EXPORT={
+ export=true,
+ pubkey="dh",
+}
+KEX_ALGORITHMS.DH_RSA={
+ pubkey="dh",
+}
+KEX_ALGORITHMS.DH_RSA_EXPORT={
+ export=true,
+ pubkey="dh",
+}
+
+KEX_ALGORITHMS.ECDHE_RSA={
+ pubkey="rsa",
+ type = "ecdh",
+ server_key_exchange = function (blob, protocol)
+ local pos
+ local ret = {}
+ pos, ret.ecdhparams, ret.strength = unpack_ecdhparams(blob)
+ pos, ret.signed = unpack_signed(blob, pos)
+ return ret
+ end
+}
+KEX_ALGORITHMS.ECDHE_ECDSA={
+ pubkey="ec",
+ type = "ecdh",
+ server_key_exchange = KEX_ALGORITHMS.ECDHE_RSA.server_key_exchange
+}
+KEX_ALGORITHMS.ECDH_ECDSA={
+ pubkey="ec",
+}
+KEX_ALGORITHMS.ECDH_RSA={
+ pubkey="ec",
+}
+
+-- draft-ietf-tls-ecc-00
+KEX_ALGORITHMS.ECDH_ECNRA={
+ pubkey="ec",
+}
+KEX_ALGORITHMS.ECMQV_ECDSA={
+ pubkey="ec",
+ type = "ecmqv",
+ server_key_exchange = function (blob, protocol)
+ local pos
+ local ret = {}
+ pos, ret.mqvparams = bin.unpack("p", blob)
+ return ret
+ end
+}
+KEX_ALGORITHMS.ECMQV_ECNRA={
+ pubkey="ec",
+}
+
+-- rfc4279
+KEX_ALGORITHMS.PSK = {
+ type = "psk",
+ server_key_exchange = function (blob, protocol)
+ local pos, hint = bin.unpack(">P", blob)
+ return {psk_identity_hint=hint}
+ end
+}
+KEX_ALGORITHMS.RSA_PSK = {
+ pubkey="rsa",
+ type = "psk",
+ server_key_exchange = KEX_ALGORITHMS.PSK.server_key_exchange
+}
+KEX_ALGORITHMS.DHE_PSK = {
+ type = "dh",
+ server_key_exchange = function (blob, protocol)
+ local pos
+ local ret = {}
+ pos, ret.psk_identity_hint = bin.unpack(">P", blob)
+ pos, ret.dhparams, ret.strength = unpack_dhparams(blob)
+ return ret
+ end
+}
+--nomenclature change
+KEX_ALGORITHMS.PSK_DHE = KEX_ALGORITHMS.DHE_PSK
+
+--rfc5489
+KEX_ALGORITHMS.ECDHE_PSK={
+ type = "ecdh",
+ server_key_exchange = function (blob, protocol)
+ local pos
+ local ret = {}
+ pos, ret.psk_identity_hint = bin.unpack(">P", blob)
+ pos, ret.ecdhparams, ret.strength = unpack_ecdhparams(blob)
+ return ret
+ end
+}
+
+-- RFC 5054
+KEX_ALGORITHMS.SRP_SHA = {
+ type = "srp",
+ server_key_exchange = function (blob, protocol)
+ local pos
+ local ret = {srp={}}
+ pos, ret.srp.N, ret.srp.g, ret.srp.s, ret.srp.B = bin.unpack(">PPpP", blob)
+ pos, ret.signed = unpack_signed(blob, pos)
+ ret.strength = #ret.srp.N
+ return ret
+ end
+}
+KEX_ALGORITHMS.SRP_SHA_DSS = {
+ pubkey="dsa",
+ type = "srp",
+ server_key_exchange = KEX_ALGORITHMS.SRP_SHA.server_key_exchange
+}
+KEX_ALGORITHMS.SRP_SHA_RSA = {
+ pubkey="rsa",
+ type = "srp",
+ server_key_exchange = KEX_ALGORITHMS.SRP_SHA.server_key_exchange
+}
+
+-- RFC 6101
+KEX_ALGORITHMS.FORTEZZA_KEA={}
+
+-- RFC 4491
+KEX_ALGORITHMS.GOSTR341001={}
+KEX_ALGORITHMS.GOSTR341094={}
+
+-- RFC 2712
+KEX_ALGORITHMS.KRB5={}
+KEX_ALGORITHMS.KRB5_EXPORT={
+ export=true,
+}
+
+
--- Get info about a cipher suite
--
--- Returned table has "kex", "server_auth", "cipher", "mode", "size", and
--- "hash" keys, as well as boolean flags "dh", "ec", and "draft". The "draft"
+-- Returned table has "kex", "cipher", "mode", "size", and
+-- "hash" keys, as well as boolean flag "draft". The "draft"
-- flag is only supported for some suites that have different enumeration
--- values in draft versus final RFC. The "export" key may be present with
--- value either "EXPORT" or "EXPORT1024".
+-- values in draft versus final RFC.
-- @param c The cipher suite name, e.g. TLS_RSA_WITH_AES_128_GCM_SHA256
-- @return A table of info as described above.
function cipher_info (c)
@@ -656,50 +978,12 @@ function cipher_info (c)
stdnse.debug2("cipher_info: Not a TLS ciphersuite: %s", c)
return nil
end
- -- kex, server_auth, cipher, size, mode, hash
- -- flags: dh, ec, export
+ -- kex, cipher, size, mode, hash
+ i = i + 1
while tokens[i] and tokens[i] ~= "WITH" do
i = i + 1
- local t = tokens[i]
- if t == "RSA" or t == "DSS" then
- info.server_auth = t
- info.kex = info.kex or t
- elseif t:sub(1,2) == "EC" then
- info.ec = true
- if t == "ECDH" or t == "ECDHE" then
- info.dh = true
- info.kex = t
- elseif t == "ECDSA" or t == "ECNRA" then
- info.server_auth = t
- elseif t == "ECMQV" then
- info.kex = t
- end
- elseif t == "DH" or t == "DHE" then
- info.dh = true
- info.kex = t
- elseif t == "PSK" then
- info.kex = (info.kex and info.kex .. "_" .. t) or t
- info.server_auth = info.server_auth or t
- elseif t == "EXPORT" or t == "EXPORT1024" then
- info.export = t
- elseif t == "SRP" then
- info.kex = "SRP_SHA"
- info.server_auth = "SRP_SHA"
- i = i + 1 -- consume _SHA
- elseif t == "FORTEZZA" then
- info.kex = "FORTEZZA_KEA"
- info.server_auth = "FORTEZZA"
- i = i + 1 -- consume _KEA
- elseif t == "NULL" then
- info.kex = t
- info.server_auth = "anon"
- elseif t == "anon" then
- info.server_auth = t
- else
- info.kex = info.kex or t
- info.server_auth = info.server_auth or t
- end
end
+ info.kex = table.concat(tokens, "_", 2, i-1)
if tokens[i] and tokens[i] ~= "WITH" then
stdnse.debug2("cipher_info: Can't parse (no WITH): %s", c)
@@ -777,18 +1061,6 @@ SCSVS = {
["TLS_FALLBACK_SCSV"] = 0x5600, -- draft-ietf-tls-downgrade-scsv-00
}
-local function find_key(t, value)
- local k, v
-
- for k, v in pairs(t) do
- if v == value then
- return k
- end
- end
-
- return nil
-end
-
-- Helper function to unpack a 3-byte integer value
local function unpack_3byte (buffer, pos)
local low, high
@@ -991,7 +1263,7 @@ function client_hello(t)
if type(cipher) == "string" then
cipher = CIPHERS[cipher] or SCSVS[cipher]
end
- if type(cipher) == "number" and cipher > 0 and cipher <= 0xffff then
+ if type(cipher) == "number" and cipher >= 0 and cipher <= 0xffff then
table.insert(ciphers, bin.pack(">S", cipher))
else
stdnse.debug1("Unknown cipher in client_hello: %s", cipher)
diff --git a/scripts/ssl-enum-ciphers.nse b/scripts/ssl-enum-ciphers.nse
index aa477fa3a..cec1d56ad 100644
--- a/scripts/ssl-enum-ciphers.nse
+++ b/scripts/ssl-enum-ciphers.nse
@@ -12,17 +12,13 @@ local tls = require "tls"
description = [[
This script repeatedly initiates SSLv3/TLS connections, each time trying a new
cipher or compressor while recording whether a host accepts or rejects it. The
-end result is a list of all the ciphers and compressors that a server accepts.
+end result is a list of all the ciphersuites and compressors that a server accepts.
-Each cipher is shown with a strength rating: one of strong,
-weak, or unknown strength. The output line
-beginning with Least strength shows the strength of the
-weakest cipher offered. If you are auditing for weak ciphers, you would
-want to look more closely at any port where Least strength
-is not strong. The cipher strength database is in the file
-nselib/data/ssl-ciphers, or you can use a different file
-through the script argument
-ssl-enum-ciphers.rankedcipherlist.
+Each ciphersuite is shown with a letter grade (A through F) indicating the
+strength of the connection. The grade is based on the cryptographic strength of
+the key exchange and of the stream cipher. The message integrity (hash)
+algorithm choice is not a factor. The output line beginning with
+Least strength shows the strength of the weakest cipher offered.
SSLv3/TLSv1 requires more effort to determine which ciphers and compression
methods a server supports than SSLv2. A client lists the ciphers and compressors
@@ -44,46 +40,44 @@ and therefore is quite noisy.
-- @usage
-- nmap --script ssl-enum-ciphers -p 443
--
--- @args ssl-enum-ciphers.rankedcipherlist A path to a file of cipher names and strength ratings
---
-- @output
-- PORT STATE SERVICE REASON
-- 443/tcp open https syn-ack
-- | ssl-enum-ciphers:
-- | SSLv3:
-- | ciphers:
--- | TLS_RSA_WITH_RC4_128_MD5 - strong
--- | TLS_RSA_WITH_RC4_128_SHA - strong
--- | TLS_RSA_WITH_3DES_EDE_CBC_SHA - strong
+-- | TLS_RSA_WITH_RC4_128_MD5 - A
+-- | TLS_RSA_WITH_RC4_128_SHA - A
+-- | TLS_RSA_WITH_3DES_EDE_CBC_SHA - E
-- | compressors:
-- | NULL
-- | cipher preference: server
-- | TLSv1.0:
-- | ciphers:
--- | TLS_RSA_WITH_RC4_128_MD5 - strong
--- | TLS_RSA_WITH_RC4_128_SHA - strong
--- | TLS_RSA_WITH_3DES_EDE_CBC_SHA - strong
--- | TLS_RSA_WITH_AES_256_CBC_SHA - strong
--- | TLS_RSA_WITH_AES_128_CBC_SHA - strong
+-- | TLS_RSA_WITH_RC4_128_MD5 - A
+-- | TLS_RSA_WITH_RC4_128_SHA - A
+-- | TLS_RSA_WITH_3DES_EDE_CBC_SHA - E
+-- | TLS_RSA_WITH_AES_256_CBC_SHA - A
+-- | TLS_RSA_WITH_AES_128_CBC_SHA - A
-- | compressors:
-- | NULL
-- | cipher preference: server
--- |_ least strength: strong
+-- |_ least strength: E
--
-- @xmloutput
--
--
--
-- TLS_RSA_WITH_RC4_128_MD5
--- strong
+-- A
--
--
-- TLS_RSA_WITH_RC4_128_SHA
--- strong
+-- A
--
--
-- TLS_RSA_WITH_3DES_EDE_CBC_SHA
--- strong
+-- E
--
--
--
@@ -95,23 +89,23 @@ and therefore is quite noisy.
--
--
-- TLS_RSA_WITH_RC4_128_MD5
--- strong
+-- A
--
--
-- TLS_RSA_WITH_RC4_128_SHA
--- strong
+-- A
--
--
-- TLS_RSA_WITH_3DES_EDE_CBC_SHA
--- strong
+-- E
--
--
-- TLS_RSA_WITH_AES_256_CBC_SHA
--- strong
+-- A
--
--
-- TLS_RSA_WITH_AES_128_CBC_SHA
--- strong
+-- A
--
--
--
@@ -119,7 +113,7 @@ and therefore is quite noisy.
--
-- server
--
--- strong
+-- E
author = "Mak Kolybabi , Gabriel Lawrence"
@@ -133,18 +127,6 @@ categories = {"discovery", "intrusive"}
-- http://seclists.org/nmap-dev/2010/q1/859
local CHUNK_SIZE = 64
-
-cipherstrength = {
- ["broken"] = 0,
- ["weak"] = 1,
- ["unknown strength"] = 2,
- ["strong"] = 3
- }
-
-local rankedciphers={}
-local mincipherstrength=9999 --artificial "highest value"
-local rankedciphersfilename=false
-
-- Add additional context (protocol) to debug output
local function ctx_log(level, protocol, fmt, ...)
return stdnse.debug(level, "(%s) " .. fmt, protocol, ...)
@@ -367,8 +349,59 @@ local function get_body(record, property, value)
return nil
end
+-- Score a ciphersuite implementation (including key exchange info)
+local function score_cipher (kex_strength, cipher_info)
+ local kex_score, cipher_score
+ if not kex_strength or not cipher_info.size then
+ return "unknown"
+ end
+ if kex_strength == 0 then
+ return 0
+ elseif kex_strength < 512 then
+ kex_score = 0.2
+ elseif kex_strength < 1024 then
+ kex_score = 0.4
+ elseif kex_strength < 2048 then
+ kex_score = 0.8
+ elseif kex_strength < 4096 then
+ kex_score = 0.9
+ else
+ kex_score = 1.0
+ end
+
+ if cipher_info.size == 0 then
+ return 0
+ elseif cipher_info.size < 128 then
+ cipher_score = 0.2
+ elseif cipher_info.size < 256 then
+ cipher_score = 0.8
+ else
+ cipher_score = 1.0
+ end
+
+ -- Based on SSL Labs' 30-30-40 rating without the first 30% (protocol support)
+ return 0.43 * kex_score + 0.57 * cipher_score
+end
+
+local function letter_grade (score)
+ if not tonumber(score) then return "unknown" end
+ if score >= 0.80 then
+ return "A"
+ elseif score >= 0.65 then
+ return "B"
+ elseif score >= 0.50 then
+ return "C"
+ elseif score >= 0.35 then
+ return "D"
+ elseif score >= 0.20 then
+ return "E"
+ else
+ return "F"
+ end
+end
+
-- Find which ciphers out of group are supported by the server.
-local function find_ciphers_group(host, port, protocol, group)
+local function find_ciphers_group(host, port, protocol, group, scores)
local results = {}
local t = {
["protocol"] = protocol,
@@ -435,6 +468,72 @@ local function find_ciphers_group(host, port, protocol, group)
else
-- Add cipher to the list of accepted ciphers.
table.insert(results, name)
+ if scores then
+ local info = tls.cipher_info(name)
+ -- Some warnings:
+ if info.hash and info.hash == "MD5" then
+ scores.warnings["Ciphersuite uses MD5 for message integrity"] = true
+ end
+ if protocol == "SSLv3" and info.mode and info.mode == "CBC" then
+ scores.warnings["CBC-mode cipher in SSLv3 (CVE-2014-3566)"] = true
+ elseif info.cipher == "RC4" and tls.PROTOCOLS[protocol] >= 0x0302 then
+ scores.warnings["Weak cipher RC4 in TLSv1.1 or newer not needed for BEAST mitigation"] = true
+ end
+ local kex = tls.KEX_ALGORITHMS[info.kex]
+ local extra, kex_strength
+ if kex.anon then
+ kex_strength = 0
+ elseif kex.export then
+ if info.kex:find("1024$") then
+ kex_strength = 1024
+ else
+ kex_strength = 512
+ end
+ else
+ if kex.pubkey then
+ local certs = get_body(handshake, "type", "certificate")
+ -- Assume RFC compliance:
+ -- "The sender's certificate MUST come first in the list."
+ -- This may not always be the case, so
+ -- TODO: reorder certificates and validate entire chain
+ -- TODO: certificate validation (date, self-signed, etc)
+ local c = sslcert.parse_ssl_certificate(certs.certificates[1])
+ if c.pubkey.type == kex.pubkey then
+ local sigalg = c.sig_algorithm:match("([mM][dD][245])")
+ if sigalg then
+ -- MD2 and MD5 are broken
+ kex_strength = 0
+ scores.warnings["Insecure certificate signature: " .. string.upper(sigalg)] = true
+ else
+ sigalg = c.sig_algorithm:match("([sS][hH][aA]1)")
+ -- TODO: Update this when SHA-1 is deprecated in 2016
+ -- kex_strength = 0
+ scores.warnings["Weak certificate signature: SHA1"] = true
+ kex_strength = tls.rsa_equiv(kex.pubkey, c.pubkey.bits)
+ extra = string.format("%s %d", kex.pubkey, c.pubkey.bits)
+ end
+ end
+ end
+ local ske = get_body(handshake, "type", "server_key_exchange")
+ if kex.server_key_exchange and ske then
+ local kex_info = kex.server_key_exchange(ske.data)
+ if kex_info.strength then
+ if kex_strength and kex_strength > kex_info.strength then
+ kex_strength = kex_info.strength
+ scores.warnings["Key exchange parameters of lower strength than certificate key"] = true
+ end
+ kex_strength = kex_strength or kex_info.strength
+ extra = string.format("%s %d", kex.type, kex_info.strength)
+ end
+ end
+ end
+ scores[name] = {
+ cipher_strength=info.size,
+ kex_strength = kex_strength,
+ extra = extra,
+ letter_grade = letter_grade(score_cipher(kex_strength, info))
+ }
+ end
end
end
end
@@ -449,10 +548,11 @@ local function find_ciphers(host, port, protocol)
local ciphers = in_chunks(sorted_keys(tls.CIPHERS), CHUNK_SIZE)
local results = {}
+ local scores = {warnings={}}
-- Try every cipher.
for _, group in ipairs(ciphers) do
- local chunk, protocol_worked = find_ciphers_group(host, port, protocol, group)
+ local chunk, protocol_worked = find_ciphers_group(host, port, protocol, group, scores)
if protocol_worked == nil then return nil end
for _, name in ipairs(chunk) do
table.insert(results, name)
@@ -460,7 +560,7 @@ local function find_ciphers(host, port, protocol)
end
if not next(results) then return nil end
- return results
+ return results, scores
end
local function find_compressors(host, port, protocol, good_ciphers)
@@ -634,7 +734,7 @@ local function try_protocol(host, port, protocol, upresults)
local results = stdnse.output_table()
-- Find all valid ciphers.
- local ciphers = find_ciphers(host, port, protocol)
+ local ciphers, scores = find_ciphers(host, port, protocol)
if ciphers == nil then
condvar "signal"
return nil
@@ -682,19 +782,15 @@ local function try_protocol(host, port, protocol, upresults)
-- Add rankings to ciphers
local cipherstr
for i, name in ipairs(ciphers) do
- if rankedciphersfilename and rankedciphers[name] then
- cipherstr=rankedciphers[name]
- else
- cipherstr="unknown strength"
- end
- stdnse.debug2("Strength of %s rated %d.",cipherstr,cipherstrength[cipherstr])
- if mincipherstrength>cipherstrength[cipherstr] then
- stdnse.debug2("Downgrading min cipher strength to %d.",cipherstrength[cipherstr])
- mincipherstrength=cipherstrength[cipherstr]
- end
- local outcipher = {name=name, strength=cipherstr}
+ local outcipher = {name=name, kex_info=scores[name].extra, strength=scores[name].letter_grade}
setmetatable(outcipher,{
- __tostring=function(t) return string.format("%s - %s", t.name, t.strength) end
+ __tostring=function(t)
+ if t.kex_info then
+ return string.format("%s (%s) - %s", t.name, t.kex_info, t.strength)
+ else
+ return string.format("%s - %s", t.name, t.strength)
+ end
+ end
})
ciphers[i]=outcipher
end
@@ -707,47 +803,15 @@ local function try_protocol(host, port, protocol, upresults)
results["cipher preference"] = cipher_pref
results["cipher preference error"] = cipher_pref_err
+ if next(scores.warnings) then
+ results["warnings"] = sorted_keys(scores.warnings)
+ end
upresults[protocol] = results
condvar "signal"
return nil
end
--- Shamelessly stolen from nselib/unpwdb.lua and changed a bit. (Gabriel Lawrence)
-local filltable = function(filename,table)
- if #table ~= 0 then
- return true
- end
-
- local file = io.open(filename, "r")
-
- if not file then
- return false
- end
-
- while true do
- local l = file:read()
-
- if not l then
- break
- end
-
- -- Comments takes up a whole line
- if not l:match("#!comment:") then
- local lsplit=stdnse.strsplit("%s+", l)
- if cipherstrength[lsplit[2]] then
- table[lsplit[1]] = lsplit[2]
- else
- stdnse.debug1("Strength not defined, ignoring: %s:%s",lsplit[1],lsplit[2])
- end
- end
- end
-
- file:close()
-
- return true
-end
-
portrule = function (host, port)
return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
end
@@ -774,15 +838,6 @@ end
action = function(host, port)
- rankedciphersfilename=stdnse.get_script_args("ssl-enum-ciphers.rankedcipherlist")
- if rankedciphersfilename then
- filltable(rankedciphersfilename,rankedciphers)
- else
- rankedciphersfilename = nmap.fetchfile( "nselib/data/ssl-ciphers" )
- stdnse.debug1("Ranked ciphers filename: %s", rankedciphersfilename)
- filltable(rankedciphersfilename,rankedciphers)
- end
-
local results = {}
local condvar = nmap.condvar(results)
@@ -807,14 +862,14 @@ action = function(host, port)
return nil
end
- if rankedciphersfilename then
- for k, v in pairs(cipherstrength) do
- if v == mincipherstrength then
- -- Should sort before or after SSLv3, TLSv*
- results["least strength"] = k
- end
+ local least = "A"
+ for p, r in pairs(results) do
+ for i, c in ipairs(r.ciphers) do
+ -- counter-intuitive: "A" < "B", so really looking for max
+ least = least < c.strength and c.strength or least
end
end
+ results["least strength"] = least
return sorted_by_key(results)
end