1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-08 05:31:31 +00:00

Use internal cipher/handshake scoring system instead of static datafile

This commit is contained in:
dmiller
2014-11-07 16:39:26 +00:00
parent e11a8609a7
commit 222b2a009d
2 changed files with 493 additions and 166 deletions

View File

@@ -583,6 +583,18 @@ CIPHERS = {
["SSL_RSA_FIPS_WITH_3DES_EDE_CBC_SHA"] = 0xFEFF, ["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 -- Keep this local to enforce use of the cipher_info function
local cipher_info_cache = { local cipher_info_cache = {
-- pre-populate the special cases that break the parser below -- 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 --- Get info about a cipher suite
-- --
-- Returned table has "kex", "server_auth", "cipher", "mode", "size", and -- Returned table has "kex", "cipher", "mode", "size", and
-- "hash" keys, as well as boolean flags "dh", "ec", and "draft". The "draft" -- "hash" keys, as well as boolean flag "draft". The "draft"
-- flag is only supported for some suites that have different enumeration -- flag is only supported for some suites that have different enumeration
-- values in draft versus final RFC. The "export" key may be present with -- values in draft versus final RFC.
-- value either "EXPORT" or "EXPORT1024".
-- @param c The cipher suite name, e.g. TLS_RSA_WITH_AES_128_GCM_SHA256 -- @param c The cipher suite name, e.g. TLS_RSA_WITH_AES_128_GCM_SHA256
-- @return A table of info as described above. -- @return A table of info as described above.
function cipher_info (c) function cipher_info (c)
@@ -656,50 +978,12 @@ function cipher_info (c)
stdnse.debug2("cipher_info: Not a TLS ciphersuite: %s", c) stdnse.debug2("cipher_info: Not a TLS ciphersuite: %s", c)
return nil return nil
end end
-- kex, server_auth, cipher, size, mode, hash -- kex, cipher, size, mode, hash
-- flags: dh, ec, export i = i + 1
while tokens[i] and tokens[i] ~= "WITH" do while tokens[i] and tokens[i] ~= "WITH" do
i = i + 1 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 end
info.kex = table.concat(tokens, "_", 2, i-1)
if tokens[i] and tokens[i] ~= "WITH" then if tokens[i] and tokens[i] ~= "WITH" then
stdnse.debug2("cipher_info: Can't parse (no WITH): %s", c) 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 ["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 -- Helper function to unpack a 3-byte integer value
local function unpack_3byte (buffer, pos) local function unpack_3byte (buffer, pos)
local low, high local low, high
@@ -991,7 +1263,7 @@ function client_hello(t)
if type(cipher) == "string" then if type(cipher) == "string" then
cipher = CIPHERS[cipher] or SCSVS[cipher] cipher = CIPHERS[cipher] or SCSVS[cipher]
end 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)) table.insert(ciphers, bin.pack(">S", cipher))
else else
stdnse.debug1("Unknown cipher in client_hello: %s", cipher) stdnse.debug1("Unknown cipher in client_hello: %s", cipher)

View File

@@ -12,17 +12,13 @@ local tls = require "tls"
description = [[ description = [[
This script repeatedly initiates SSLv3/TLS connections, each time trying a new 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 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 <code>strong</code>, Each ciphersuite is shown with a letter grade (A through F) indicating the
<code>weak</code>, or <code>unknown strength</code>. The output line strength of the connection. The grade is based on the cryptographic strength of
beginning with <code>Least strength</code> shows the strength of the the key exchange and of the stream cipher. The message integrity (hash)
weakest cipher offered. If you are auditing for weak ciphers, you would algorithm choice is not a factor. The output line beginning with
want to look more closely at any port where <code>Least strength</code> <code>Least strength</code> shows the strength of the weakest cipher offered.
is not <code>strong</code>. The cipher strength database is in the file
<code>nselib/data/ssl-ciphers</code>, or you can use a different file
through the script argument
<code>ssl-enum-ciphers.rankedcipherlist</code>.
SSLv3/TLSv1 requires more effort to determine which ciphers and compression SSLv3/TLSv1 requires more effort to determine which ciphers and compression
methods a server supports than SSLv2. A client lists the ciphers and compressors methods a server supports than SSLv2. A client lists the ciphers and compressors
@@ -44,46 +40,44 @@ and therefore is quite noisy.
-- @usage -- @usage
-- nmap --script ssl-enum-ciphers -p 443 <host> -- nmap --script ssl-enum-ciphers -p 443 <host>
-- --
-- @args ssl-enum-ciphers.rankedcipherlist A path to a file of cipher names and strength ratings
--
-- @output -- @output
-- PORT STATE SERVICE REASON -- PORT STATE SERVICE REASON
-- 443/tcp open https syn-ack -- 443/tcp open https syn-ack
-- | ssl-enum-ciphers: -- | ssl-enum-ciphers:
-- | SSLv3: -- | SSLv3:
-- | ciphers: -- | ciphers:
-- | TLS_RSA_WITH_RC4_128_MD5 - strong -- | TLS_RSA_WITH_RC4_128_MD5 - A
-- | TLS_RSA_WITH_RC4_128_SHA - strong -- | TLS_RSA_WITH_RC4_128_SHA - A
-- | TLS_RSA_WITH_3DES_EDE_CBC_SHA - strong -- | TLS_RSA_WITH_3DES_EDE_CBC_SHA - E
-- | compressors: -- | compressors:
-- | NULL -- | NULL
-- | cipher preference: server -- | cipher preference: server
-- | TLSv1.0: -- | TLSv1.0:
-- | ciphers: -- | ciphers:
-- | TLS_RSA_WITH_RC4_128_MD5 - strong -- | TLS_RSA_WITH_RC4_128_MD5 - A
-- | TLS_RSA_WITH_RC4_128_SHA - strong -- | TLS_RSA_WITH_RC4_128_SHA - A
-- | TLS_RSA_WITH_3DES_EDE_CBC_SHA - strong -- | TLS_RSA_WITH_3DES_EDE_CBC_SHA - E
-- | TLS_RSA_WITH_AES_256_CBC_SHA - strong -- | TLS_RSA_WITH_AES_256_CBC_SHA - A
-- | TLS_RSA_WITH_AES_128_CBC_SHA - strong -- | TLS_RSA_WITH_AES_128_CBC_SHA - A
-- | compressors: -- | compressors:
-- | NULL -- | NULL
-- | cipher preference: server -- | cipher preference: server
-- |_ least strength: strong -- |_ least strength: E
-- --
-- @xmloutput -- @xmloutput
-- <table key="SSLv3"> -- <table key="SSLv3">
-- <table key="ciphers"> -- <table key="ciphers">
-- <table> -- <table>
-- <elem key="name">TLS_RSA_WITH_RC4_128_MD5</elem> -- <elem key="name">TLS_RSA_WITH_RC4_128_MD5</elem>
-- <elem key="strength">strong</elem> -- <elem key="strength">A</elem>
-- </table> -- </table>
-- <table> -- <table>
-- <elem key="name">TLS_RSA_WITH_RC4_128_SHA</elem> -- <elem key="name">TLS_RSA_WITH_RC4_128_SHA</elem>
-- <elem key="strength">strong</elem> -- <elem key="strength">A</elem>
-- </table> -- </table>
-- <table> -- <table>
-- <elem key="name">TLS_RSA_WITH_3DES_EDE_CBC_SHA</elem> -- <elem key="name">TLS_RSA_WITH_3DES_EDE_CBC_SHA</elem>
-- <elem key="strength">strong</elem> -- <elem key="strength">E</elem>
-- </table> -- </table>
-- </table> -- </table>
-- <table key="compressors"> -- <table key="compressors">
@@ -95,23 +89,23 @@ and therefore is quite noisy.
-- <table key="ciphers"> -- <table key="ciphers">
-- <table> -- <table>
-- <elem key="name">TLS_RSA_WITH_RC4_128_MD5</elem> -- <elem key="name">TLS_RSA_WITH_RC4_128_MD5</elem>
-- <elem key="strength">strong</elem> -- <elem key="strength">A</elem>
-- </table> -- </table>
-- <table> -- <table>
-- <elem key="name">TLS_RSA_WITH_RC4_128_SHA</elem> -- <elem key="name">TLS_RSA_WITH_RC4_128_SHA</elem>
-- <elem key="strength">strong</elem> -- <elem key="strength">A</elem>
-- </table> -- </table>
-- <table> -- <table>
-- <elem key="name">TLS_RSA_WITH_3DES_EDE_CBC_SHA</elem> -- <elem key="name">TLS_RSA_WITH_3DES_EDE_CBC_SHA</elem>
-- <elem key="strength">strong</elem> -- <elem key="strength">E</elem>
-- </table> -- </table>
-- <table> -- <table>
-- <elem key="name">TLS_RSA_WITH_AES_256_CBC_SHA</elem> -- <elem key="name">TLS_RSA_WITH_AES_256_CBC_SHA</elem>
-- <elem key="strength">strong</elem> -- <elem key="strength">A</elem>
-- </table> -- </table>
-- <table> -- <table>
-- <elem key="name">TLS_RSA_WITH_AES_128_CBC_SHA</elem> -- <elem key="name">TLS_RSA_WITH_AES_128_CBC_SHA</elem>
-- <elem key="strength">strong</elem> -- <elem key="strength">A</elem>
-- </table> -- </table>
-- </table> -- </table>
-- <table key="compressors"> -- <table key="compressors">
@@ -119,7 +113,7 @@ and therefore is quite noisy.
-- </table> -- </table>
-- <elem key="cipher preference">server</elem> -- <elem key="cipher preference">server</elem>
-- </table> -- </table>
-- <elem key="least strength">strong</elem> -- <elem key="least strength">E</elem>
author = "Mak Kolybabi <mak@kolybabi.com>, Gabriel Lawrence" author = "Mak Kolybabi <mak@kolybabi.com>, Gabriel Lawrence"
@@ -133,18 +127,6 @@ categories = {"discovery", "intrusive"}
-- http://seclists.org/nmap-dev/2010/q1/859 -- http://seclists.org/nmap-dev/2010/q1/859
local CHUNK_SIZE = 64 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 -- Add additional context (protocol) to debug output
local function ctx_log(level, protocol, fmt, ...) local function ctx_log(level, protocol, fmt, ...)
return stdnse.debug(level, "(%s) " .. fmt, protocol, ...) return stdnse.debug(level, "(%s) " .. fmt, protocol, ...)
@@ -367,8 +349,59 @@ local function get_body(record, property, value)
return nil return nil
end 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. -- 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 results = {}
local t = { local t = {
["protocol"] = protocol, ["protocol"] = protocol,
@@ -435,6 +468,72 @@ local function find_ciphers_group(host, port, protocol, group)
else else
-- Add cipher to the list of accepted ciphers. -- Add cipher to the list of accepted ciphers.
table.insert(results, name) 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 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 ciphers = in_chunks(sorted_keys(tls.CIPHERS), CHUNK_SIZE)
local results = {} local results = {}
local scores = {warnings={}}
-- Try every cipher. -- Try every cipher.
for _, group in ipairs(ciphers) do 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 if protocol_worked == nil then return nil end
for _, name in ipairs(chunk) do for _, name in ipairs(chunk) do
table.insert(results, name) table.insert(results, name)
@@ -460,7 +560,7 @@ local function find_ciphers(host, port, protocol)
end end
if not next(results) then return nil end if not next(results) then return nil end
return results return results, scores
end end
local function find_compressors(host, port, protocol, good_ciphers) 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() local results = stdnse.output_table()
-- Find all valid ciphers. -- Find all valid ciphers.
local ciphers = find_ciphers(host, port, protocol) local ciphers, scores = find_ciphers(host, port, protocol)
if ciphers == nil then if ciphers == nil then
condvar "signal" condvar "signal"
return nil return nil
@@ -682,19 +782,15 @@ local function try_protocol(host, port, protocol, upresults)
-- Add rankings to ciphers -- Add rankings to ciphers
local cipherstr local cipherstr
for i, name in ipairs(ciphers) do for i, name in ipairs(ciphers) do
if rankedciphersfilename and rankedciphers[name] then local outcipher = {name=name, kex_info=scores[name].extra, strength=scores[name].letter_grade}
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}
setmetatable(outcipher,{ 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 ciphers[i]=outcipher
end end
@@ -707,47 +803,15 @@ local function try_protocol(host, port, protocol, upresults)
results["cipher preference"] = cipher_pref results["cipher preference"] = cipher_pref
results["cipher preference error"] = cipher_pref_err results["cipher preference error"] = cipher_pref_err
if next(scores.warnings) then
results["warnings"] = sorted_keys(scores.warnings)
end
upresults[protocol] = results upresults[protocol] = results
condvar "signal" condvar "signal"
return nil return nil
end 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) portrule = function (host, port)
return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port) return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
end end
@@ -774,15 +838,6 @@ end
action = function(host, port) 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 results = {}
local condvar = nmap.condvar(results) local condvar = nmap.condvar(results)
@@ -807,14 +862,14 @@ action = function(host, port)
return nil return nil
end end
if rankedciphersfilename then local least = "A"
for k, v in pairs(cipherstrength) do for p, r in pairs(results) do
if v == mincipherstrength then for i, c in ipairs(r.ciphers) do
-- Should sort before or after SSLv3, TLSv* -- counter-intuitive: "A" < "B", so really looking for max
results["least strength"] = k least = least < c.strength and c.strength or least
end
end end
end end
results["least strength"] = least
return sorted_by_key(results) return sorted_by_key(results)
end end