1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-06 04:31:29 +00:00

TLS 1.3 support for NSE. Fixes #1691

This commit is contained in:
dmiller
2021-07-02 20:01:30 +00:00
parent 61f17067a3
commit 7c61f7c9c3
10 changed files with 545 additions and 201 deletions

View File

@@ -75,7 +75,7 @@ local client_hello = function(host, port)
status, err = sock:connect(host, port)
if not status then
sock:close()
stdnse.debug("Can't send: %s", err)
stdnse.debug("Can't connect: %s", err)
return false
end
else
@@ -86,23 +86,24 @@ local client_hello = function(host, port)
end
-- Send Client Hello to the target server
status, err = sock:send(cli_h)
if not status then
stdnse.debug("Couldn't send: %s", err)
sock:close()
return false
end
repeat -- only once
-- Send Client Hello to the target server
status, err = sock:send(cli_h)
if not status then
stdnse.debug("Couldn't send: %s", err)
break
end
-- Read response
status, response, err = tls.record_buffer(sock)
if not status then
stdnse.debug("Couldn't receive: %s", err)
sock:close()
return false
end
-- Read response
status, response, err = tls.record_buffer(sock)
if not status then
stdnse.debug("Couldn't receive: %s", err)
break
end
until true
return true, response
sock:close()
return status, response
end
-- extract time from ServerHello response

View File

@@ -883,6 +883,11 @@ parameters.]],
}
for protocol in pairs(tls.PROTOCOLS) do
if protocol == "TLSv1.3" then
-- TLSv1.3 does not allow anonymous key exchange and only allows specific
-- DHE groups named in RFC 7919
goto NEXT_PROTOCOL
end
-- Try anonymous DH ciphersuites
cipher, dhparams = get_dhe_params(host, port, protocol, dh_anons)
-- Explicit test for false needed because nil just means no ciphers supported.

View File

@@ -400,7 +400,10 @@ local function try_params(host, port, t)
for j = 1, #record.body do -- no ipairs because we append below
local b = record.body[j]
done = ((record.type == "alert" and b.level == "fatal") or
(record.type == "handshake" and b.type == "server_hello_done"))
(record.type == "handshake" and (b.type == "server_hello_done" or
-- TLSv1.3 does not have server_hello_done
(t.protocol == "TLSv1.3" and b.type == "server_hello")))
)
table.insert(records[record.type].body, b)
end
if done then
@@ -544,7 +547,7 @@ local function score_cipher (kex_strength, cipher_info)
if not kex_strength or not cipher_info.size then
return "unknown"
end
if kex_strength == 0 then
if kex_strength <= 0 then
return 0
elseif kex_strength < 512 then
kex_score = 0.2
@@ -558,7 +561,7 @@ local function score_cipher (kex_strength, cipher_info)
kex_score = 1.0
end
if cipher_info.size == 0 then
if cipher_info.size <= 0 then
return 0
elseif cipher_info.size < 128 then
cipher_score = 0.2
@@ -589,14 +592,27 @@ local function letter_grade (score)
end
end
local tls13proto = tls.PROTOCOLS["TLSv1.3"]
local tls13supported = tls.EXTENSION_HELPERS.supported_versions({"TLSv1.3"})
local function get_hello_table(host, protocol)
local t = {
protocol = protocol,
record_protocol = protocol, -- improve chances of immediate rejection
extensions = base_extensions(host),
}
-- supported_versions extension required for TLSv1.3
if (tls.PROTOCOLS[protocol] >= tls13proto) then
t.extensions.supported_versions = tls13supported
end
return t
end
-- Find which ciphers out of group are supported by the server.
local function find_ciphers_group(host, port, protocol, group, scores)
local results = {}
local t = {
["protocol"] = protocol,
["record_protocol"] = protocol, -- improve chances of immediate rejection
["extensions"] = base_extensions(host),
}
local t = get_hello_table(host, protocol)
-- This is a hacky sort of tristate variable. There are three conditions:
-- 1. false = either ciphers or protocol is bad. Keep trying with new ciphers
@@ -616,14 +632,15 @@ local function find_ciphers_group(host, port, protocol, group, scores)
local alert = records.alert
if alert then
ctx_log(2, protocol, "Got alert: %s", alert.body[1].description)
if alert["protocol"] ~= protocol then
if not tls.record_version_ok(alert["protocol"], protocol) then
ctx_log(1, protocol, "Protocol mismatch (received %s)", alert.protocol)
-- Sometimes this is not an actual rejection of the protocol. Check specifically:
if get_body(alert, "description", "protocol_version") then
protocol_worked = nil
end
break
elseif get_body(alert, "description", "handshake_failure") then
elseif get_body(alert, "description", "handshake_failure")
or get_body(alert, "description", "insufficient_security") then
protocol_worked = true
ctx_log(2, protocol, "%d ciphers rejected.", #group)
break
@@ -680,98 +697,97 @@ local function find_ciphers_group(host, port, protocol, group, scores)
elseif info.cipher == "RC4" then
scores.warnings["Broken cipher RC4 is deprecated by RFC 7465"] = true
end
if protocol == "TLSv1.3" and not info.tls13ok then
scores.warnings["Non-TLSv1.3 ciphersuite chosen for TLSv1.3"] = true
end
local kex = tls.KEX_ALGORITHMS[info.kex]
scores.any_pfs_ciphers = kex.pfs or scores.any_pfs_ciphers
local extra, kex_strength
if kex.anon then
kex_strength = 0
elseif kex.export then
if kex.export then
scores.warnings["Export key exchange"] = true
if info.kex:find("1024$") then
kex_strength = 1024
else
kex_strength = 512
end
else
if have_ssl and 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, err
if certs == nil then
err = "no certificate message"
else
c, err = sslcert.parse_ssl_certificate(certs.certificates[1])
end
if kex.anon then
scores.warnings["Anonymous key exchange, score capped at F"] = true
kex_strength = 0
elseif have_ssl and 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, err
if certs == nil then
err = "no certificate message"
else
c, err = sslcert.parse_ssl_certificate(certs.certificates[1])
end
if not c then
ctx_log(1, protocol, "Failed to parse certificate: %s", err)
elseif c.pubkey.type == kex.pubkey then
local sigalg = c.sig_algorithm:match("([mM][dD][245])") or c.sig_algorithm:match("([sS][hH][aA]1)")
if sigalg then
kex_strength = 0
scores.warnings[("Insecure certificate signature (%s), score capped at F"):format(string.upper(sigalg))] = true
end
if not c then
stdnse.debug1("Failed to parse certificate: %s", err)
elseif c.pubkey.type == kex.pubkey then
local sigalg = c.sig_algorithm:match("([mM][dD][245])")
if sigalg then
-- MD2 and MD5 are broken
local rsa_bits = tls.rsa_equiv(kex.pubkey, c.pubkey.bits)
kex_strength = math.min(kex_strength or rsa_bits, rsa_bits)
if c.pubkey.exponent then
if openssl.bignum_bn2dec(c.pubkey.exponent) == "1" then
kex_strength = 0
scores.warnings["Insecure certificate signature: " .. string.upper(sigalg)] = true
else
sigalg = c.sig_algorithm:match("([sS][hH][aA]1)")
if sigalg then
-- TODO: Update this when SHA-1 is fully deprecated in 2017
if type(c.notBefore) == "table" and c.notBefore.year >= 2016 then
kex_strength = 0
scores.warnings["Deprecated SHA1 signature in certificate issued after January 1, 2016"] = true
end
scores.warnings["Weak certificate signature: SHA1"] = true
end
kex_strength = tls.rsa_equiv(kex.pubkey, c.pubkey.bits)
if c.pubkey.exponent then
if openssl.bignum_bn2dec(c.pubkey.exponent) == "1" then
kex_strength = 0
scores.warnings["Certificate RSA exponent is 1, score capped at F"] = true
end
end
if c.pubkey.ecdhparams then
if c.pubkey.ecdhparams.curve_params.ec_curve_type == "namedcurve" then
extra = c.pubkey.ecdhparams.curve_params.curve
else
extra = string.format("%s %d", c.pubkey.ecdhparams.curve_params.ec_curve_type, c.pubkey.bits)
end
else
extra = string.format("%s %d", kex.pubkey, c.pubkey.bits)
end
scores.warnings["Certificate RSA exponent is 1, score capped at F"] = true
end
end
if c.pubkey.ecdhparams then
if c.pubkey.ecdhparams.curve_params.ec_curve_type == "namedcurve" then
extra = c.pubkey.ecdhparams.curve_params.curve
else
extra = string.format("%s %d", c.pubkey.ecdhparams.curve_params.ec_curve_type, c.pubkey.bits)
end
else
extra = string.format("%s %d", kex.pubkey, c.pubkey.bits)
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, protocol)
if kex_info.strength then
local rsa_bits = tls.rsa_equiv(kex.type, kex_info.strength)
local low_strength_warning = false
if kex_strength and kex_strength > rsa_bits then
kex_strength = rsa_bits
low_strength_warning = true
end
kex_strength = kex_strength or rsa_bits
if kex_info.ecdhparams then
if kex_info.ecdhparams.curve_params.ec_curve_type == "namedcurve" then
extra = kex_info.ecdhparams.curve_params.curve
else
extra = string.format("%s %d", kex_info.ecdhparams.curve_params.ec_curve_type, kex_info.strength)
end
end
local ske
if protocol == "TLSv1.3" then
ske = server_hello.extensions.key_share
elseif kex.server_key_exchange then
ske = get_body(handshake, "type", "server_key_exchange")
if ske then
ske = ske.data
end
end
if ske then
local kex_info = kex.server_key_exchange(ske, protocol)
if kex_info.strength then
local kex_type = kex_info.type or kex.type
if kex_info.ecdhparams then
if kex_info.ecdhparams.curve_params.ec_curve_type == "namedcurve" then
extra = kex_info.ecdhparams.curve_params.curve
else
extra = string.format("%s %d", kex.type, kex_info.strength)
end
if low_strength_warning then
scores.warnings[(
"Key exchange (%s) of lower strength than certificate key"
):format(extra)] = true
extra = string.format("%s %d", kex_info.ecdhparams.curve_params.ec_curve_type, kex_info.strength)
end
else
extra = string.format("%s %d", kex_type, kex_info.strength)
end
if kex_info.rsa and kex_info.rsa.exponent == 1 then
kex_strength = 0
scores.warnings["Certificate RSA exponent is 1, score capped at F"] = true
local rsa_bits = tls.rsa_equiv(kex_type, kex_info.strength)
if kex_strength and kex_strength > rsa_bits then
kex_strength = rsa_bits
scores.warnings[(
"Key exchange (%s) of lower strength than certificate key"
):format(extra)] = true
end
kex_strength = math.min(kex_strength or rsa_bits, rsa_bits)
end
if kex_info.rsa and kex_info.rsa.exponent == 1 then
kex_strength = 0
scores.warnings["Certificate RSA exponent is 1, score capped at F"] = true
end
end
scores[name] = {
@@ -791,11 +807,8 @@ end
local function get_chunk_size(host, protocol)
-- Try to make sure we don't send too big of a handshake
-- https://github.com/ssllabs/research/wiki/Long-Handshake-Intolerance
local len_t = {
protocol = protocol,
ciphers = {},
extensions = base_extensions(host),
}
local len_t = get_hello_table(host, protocol)
len_t.ciphers = {}
local cipher_len_remaining = 255 - #tls.client_hello(len_t)
-- if we're over 255 anyway, just go for it.
-- Each cipher adds 2 bytes
@@ -809,7 +822,17 @@ end
-- each chunk.
local function find_ciphers(host, port, protocol)
local ciphers = in_chunks(sorted_keys(tls.CIPHERS), get_chunk_size(host, protocol))
local candidates = {}
-- TLSv1.3 ciphers are different, though some are shared (ECCPWD)
local tls13 = protocol == "TLSv1.3"
for _, c in ipairs(sorted_keys(tls.CIPHERS)) do
local info = tls.cipher_info(c)
if (not tls13 and not info.tls13only)
or (tls13 and info.tls13ok) then
candidates[#candidates+1] = c
end
end
local ciphers = in_chunks(candidates, get_chunk_size(host, protocol))
local results = {}
local scores = {warnings={}}
@@ -830,11 +853,8 @@ end
local function find_compressors(host, port, protocol, good_ciphers)
local compressors = sorted_keys(tls.COMPRESSORS)
local t = {
["protocol"] = protocol,
["ciphers"] = good_ciphers,
["extensions"] = base_extensions(host),
}
local t = get_hello_table(host, protocol)
t.ciphers = good_ciphers
local results = {}
@@ -852,7 +872,7 @@ local function find_compressors(host, port, protocol, good_ciphers)
local alert = records.alert
if alert then
ctx_log(2, protocol, "Got alert: %s", alert.body[1].description)
if alert["protocol"] ~= protocol then
if not tls.record_version_ok(alert["protocol"], protocol) then
ctx_log(1, protocol, "Protocol rejected.")
protocol_worked = nil
break
@@ -908,11 +928,8 @@ end
-- Offer two ciphers and return the one chosen by the server. Returns nil and
-- an error message in case of a server error.
local function compare_ciphers(host, port, protocol, cipher_a, cipher_b)
local t = {
["protocol"] = protocol,
["ciphers"] = {cipher_a, cipher_b},
["extensions"] = base_extensions(host),
}
local t = get_hello_table(host, protocol)
t.ciphers = {cipher_a, cipher_b}
local records = try_params(host, port, t)
local server_hello = records.handshake and get_body(records.handshake, "type", "server_hello")
if server_hello then
@@ -1010,14 +1027,18 @@ local function try_protocol(host, port, protocol, upresults)
end
-- Find all valid compression methods.
local compressors
-- Reduce chunk size by 1 to allow extra room for the extra compressors (2 bytes)
for _, c in ipairs(in_chunks(ciphers, get_chunk_size(host, protocol) - 1)) do
compressors = find_compressors(host, port, protocol, c)
-- I observed a weird interaction between ECDSA ciphers and DEFLATE compression.
-- Some servers would reject the handshake if no non-ECDSA ciphers were available.
-- Sending 64 ciphers at a time should be sufficient, but we'll try them all if necessary.
if compressors and #compressors ~= 0 then
break
-- RFC 8446: "For every TLS 1.3 ClientHello, this vector MUST contain exactly
-- one byte, set to zero"
if (tls.PROTOCOLS[protocol] < tls13proto) then
-- Reduce chunk size by 1 to allow extra room for the extra compressors (2 bytes)
for _, c in ipairs(in_chunks(ciphers, get_chunk_size(host, protocol) - 1)) do
compressors = find_compressors(host, port, protocol, c)
-- I observed a weird interaction between ECDSA ciphers and DEFLATE compression.
-- Some servers would reject the handshake if no non-ECDSA ciphers were available.
-- Sending 64 ciphers at a time should be sufficient, but we'll try them all if necessary.
if compressors and #compressors ~= 0 then
break
end
end
end

View File

@@ -43,6 +43,7 @@ license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = { "vuln", "safe" }
dependencies = {"https-redirect"}
-- TLSv1.3 was not implemented by affected versions of OpenSSL.
local arg_protocols = stdnse.get_script_args(SCRIPT_NAME .. ".protocols") or {'TLSv1.0', 'TLSv1.1', 'TLSv1.2'}
portrule = function(host, port)

View File

@@ -48,6 +48,7 @@ your TLS ciphersuites.
-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3566
-- |_ https://www.openssl.org/~bodo/ssl-poodle.pdf
--
-- @see ssl-enum-ciphers.nse
author = "Daniel Miller"

View File

@@ -56,10 +56,13 @@ local client_hello = function(host, port, protos)
local sock, status, response, err, cli_h
cli_h = tls.client_hello({
["extensions"] = {
[ALPN_NAME] = tls.EXTENSION_HELPERS[ALPN_NAME](protos)
},
})
-- TLSv1.3 does not send this extension plaintext.
-- TODO: implement key exchange crypto to retrieve encrypted extensions
protocol = "TLSv1.2",
["extensions"] = {
[ALPN_NAME] = tls.EXTENSION_HELPERS[ALPN_NAME](protos)
},
})
-- Connect to the target server
local status, err

View File

@@ -58,6 +58,9 @@ local client_hello = function(host, port)
local sock, status, response, err, cli_h
cli_h = tls.client_hello({
-- TLSv1.3 does not send this extension plaintext.
-- TODO: implement key exchange crypto to retrieve encrypted extensions
protocol = "TLSv1.2",
["extensions"] = {
["next_protocol_negotiation"] = "",
},