1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-08 21:51:28 +00:00

Add ssl-poodle

This commit is contained in:
dmiller
2014-10-21 14:08:34 +00:00
parent dc7d16ca4c
commit fefcca1623
3 changed files with 352 additions and 0 deletions

View File

@@ -1,5 +1,7 @@
# Nmap Changelog ($Id$); -*-text-*- # Nmap Changelog ($Id$); -*-text-*-
o [NSE] Added ssl-poodle to detect CVE-2014-3566 [Daniel Miller]
o [NSE] vulns.Report:make_output() now generates XML structured output o [NSE] vulns.Report:make_output() now generates XML structured output
reports automatically. [Paulino Calderon] reports automatically. [Paulino Calderon]

View File

@@ -443,6 +443,7 @@ Entry { filename = "ssl-enum-ciphers.nse", categories = { "discovery", "intrusiv
Entry { filename = "ssl-google-cert-catalog.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "ssl-google-cert-catalog.nse", categories = { "discovery", "external", "safe", } }
Entry { filename = "ssl-heartbleed.nse", categories = { "safe", "vuln", } } Entry { filename = "ssl-heartbleed.nse", categories = { "safe", "vuln", } }
Entry { filename = "ssl-known-key.nse", categories = { "default", "discovery", "safe", "vuln", } } Entry { filename = "ssl-known-key.nse", categories = { "default", "discovery", "safe", "vuln", } }
Entry { filename = "ssl-poodle.nse", categories = { "safe", "vuln", } }
Entry { filename = "sslv2.nse", categories = { "default", "safe", } } Entry { filename = "sslv2.nse", categories = { "default", "safe", } }
Entry { filename = "sstp-discover.nse", categories = { "default", "discovery", } } Entry { filename = "sstp-discover.nse", categories = { "default", "discovery", } }
Entry { filename = "stun-info.nse", categories = { "discovery", "safe", } } Entry { filename = "stun-info.nse", categories = { "discovery", "safe", } }

349
scripts/ssl-poodle.nse Normal file
View File

@@ -0,0 +1,349 @@
local nmap = require "nmap"
local shortport = require "shortport"
local sslcert = require "sslcert"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local tls = require "tls"
local listop = require "listop"
local vulns = require "vulns"
description = [[
Checks whether SSLv3 CBC ciphers are allowed (POODLE)
Run with -sV to use Nmap's service scan to detect SSL/TLS on non-standard
ports. Otherwise, ssl-poodle will only run on ports that are commonly used for
SSL.
POODLE is CVE-2014-3566. All implementations of SSLv3 that accept CBC
ciphersuites are vulnerable. For speed of detection, this script will stop
after the first CBC ciphersuite is discovered. If you want to enumerate all CBC
ciphersuites, you can use Nmap's own ssl-enum-ciphers to do a full audit of
your TLS ciphersuites.
]]
---
-- @usage
-- nmap -sV --version-light --script ssl-poodle -p 443 <host>
--
-- @output
-- PORT STATE SERVICE REASON
-- 443/tcp open https syn-ack
-- | ssl-poodle:
-- | VULNERABLE:
-- | SSL POODLE information leak
-- | State: VULNERABLE
-- | IDs: CVE:CVE-2014-3566 OSVDB:113251
-- | The SSL protocol 3.0, as used in OpenSSL through 1.0.1i and
-- | other products, uses nondeterministic CBC padding, which makes it easier
-- | for man-in-the-middle attackers to obtain cleartext data via a
-- | padding-oracle attack, aka the "POODLE" issue.
-- | Disclosure date: 2014-10-14
-- | Check results:
-- | TLS_RSA_WITH_3DES_EDE_CBC_SHA
-- | References:
-- | https://www.imperialviolet.org/2014/10/14/poodle.html
-- | http://osvdb.org/113251
-- | http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3566
-- |_ https://www.openssl.org/~bodo/ssl-poodle.pdf
--
author = "Daniel Miller"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"vuln", "safe"}
-- Test this many ciphersuites at a time.
-- http://seclists.org/nmap-dev/2012/q3/156
-- http://seclists.org/nmap-dev/2010/q1/859
local CHUNK_SIZE = 64
local function keys(t)
local ret = {}
local k, v = next(t)
while k do
ret[#ret+1] = k
k, v = next(t, k)
end
return ret
end
-- Add additional context (protocol) to debug output
local function ctx_log(level, protocol, fmt, ...)
return stdnse.print_debug(level, "(%s) " .. fmt, protocol, ...)
end
local function try_params(host, port, t)
local buffer, err, i, record, req, resp, sock, status
local timeout = ((host.times and host.times.timeout) or 5) * 1000 + 5000
-- Create socket.
local specialized = sslcert.getPrepareTLSWithoutReconnect(port)
if specialized then
local status
status, sock = specialized(host, port)
if not status then
ctx_log(1, t.protocol, "Can't connect: %s", err)
return nil
end
else
sock = nmap.new_socket()
sock:set_timeout(timeout)
local status = sock:connect(host, port)
if not status then
ctx_log(1, t.protocol, "Can't connect: %s", err)
sock:close()
return nil
end
end
sock:set_timeout(timeout)
-- Send request.
req = tls.client_hello(t)
status, err = sock:send(req)
if not status then
ctx_log(1, t.protocol, "Can't send: %s", err)
sock:close()
return nil
end
-- Read response.
buffer = ""
record = nil
while true do
local status
status, buffer, err = tls.record_buffer(sock, buffer, 1)
if not status then
ctx_log(1, t.protocol, "Couldn't read a TLS record: %s", err)
return nil
end
-- Parse response.
i, record = tls.record_read(buffer, 1)
if record and record.type == "alert" and record.body[1].level == "warning" then
ctx_log(1, t.protocol, "Ignoring warning: %s", record.body[1].description)
-- Try again.
elseif record then
sock:close()
return record
end
buffer = buffer:sub(i+1)
end
end
local function sorted_keys(t)
local ret = {}
for k, _ in pairs(t) do
ret[#ret+1] = k
end
table.sort(ret)
return ret
end
local function in_chunks(t, size)
local ret = {}
for i = 1, #t, size do
local chunk = {}
for j = i, i + size - 1 do
chunk[#chunk+1] = t[j]
end
ret[#ret+1] = chunk
end
return ret
end
local function remove(t, e)
for i, v in ipairs(t) do
if v == e then
table.remove(t, i)
return i
end
end
return nil
end
-- https://bugzilla.mozilla.org/show_bug.cgi?id=946147
local function remove_high_byte_ciphers(t)
local output = {}
for i, v in ipairs(t) do
if tls.CIPHERS[v] <= 255 then
output[#output+1] = v
end
end
return output
end
-- Claim to support every elliptic curve and EC point format
local base_extensions = {
-- Claim to support every elliptic curve
["elliptic_curves"] = tls.EXTENSION_HELPERS["elliptic_curves"](sorted_keys(tls.ELLIPTIC_CURVES)),
-- Claim to support every EC point format
["ec_point_formats"] = tls.EXTENSION_HELPERS["ec_point_formats"](sorted_keys(tls.EC_POINT_FORMATS)),
}
-- Recursively copy a table.
-- Only recurs when a value is a table, other values are copied by assignment.
local function tcopy (t)
local tc = {};
for k,v in pairs(t) do
if type(v) == "table" then
tc[k] = tcopy(v);
else
tc[k] = v;
end
end
return tc;
end
-- Find which ciphers out of group are supported by the server.
local function find_ciphers_group(host, port, protocol, group)
local name, protocol_worked, record, results
results = {}
local t = {
["protocol"] = protocol,
["extensions"] = tcopy(base_extensions),
}
if host.targetname then
t["extensions"]["server_name"] = tls.EXTENSION_HELPERS["server_name"](host.targetname)
end
-- 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
-- 2. nil = The protocol is bad. Abandon thread.
-- 3. true = Protocol works, at least some cipher must be supported.
protocol_worked = false
while (next(group)) do
t["ciphers"] = group
record = try_params(host, port, t)
if record == nil then
if protocol_worked then
ctx_log(2, protocol, "%d ciphers rejected. (No handshake)", #group)
else
ctx_log(1, protocol, "%d ciphers and/or protocol rejected. (No handshake)", #group)
end
break
elseif record["protocol"] ~= protocol then
ctx_log(1, protocol, "Protocol rejected.")
protocol_worked = nil
break
elseif record["type"] == "alert" and record["body"][1]["description"] == "handshake_failure" then
protocol_worked = true
ctx_log(2, protocol, "%d ciphers rejected.", #group)
break
elseif record["type"] ~= "handshake" or record["body"][1]["type"] ~= "server_hello" then
ctx_log(2, protocol, "Unexpected record received.")
break
else
protocol_worked = true
name = record["body"][1]["cipher"]
ctx_log(1, protocol, "Cipher %s chosen.", name)
if not remove(group, name) then
ctx_log(1, protocol, "chose cipher %s that was not offered.", name)
ctx_log(1, protocol, "removing high-byte ciphers and trying again.")
local size_before = #group
group = remove_high_byte_ciphers(group)
ctx_log(1, protocol, "removed %d high-byte ciphers.", size_before - #group)
if #group == size_before then
-- No changes... Server just doesn't like our offered ciphers.
break
end
else
-- Add cipher to the list of accepted ciphers.
table.insert(results, name)
-- POODLE check doesn't care about the rest of the ciphers
break
end
end
end
return results, protocol_worked
end
-- POODLE only affects CBC ciphers
local cbc_ciphers = listop.filter(
function(x) return string.find(x, "_CBC_",1,true) end,
sorted_keys(tls.CIPHERS)
)
-- move these to the top, more likely to be supported
for _, c in ipairs({
"TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", --mandatory for TLSv1.0
"TLS_RSA_WITH_3DES_EDE_CBC_SHA", -- mandatory for TLSv1.1
"TLS_RSA_WITH_AES_128_CBC_SHA", -- mandatory fro TLSv1.2
}) do
remove(cbc_ciphers, c)
table.insert(cbc_ciphers, 1, c)
end
-- Break the cipher list into chunks of CHUNK_SIZE (for servers that can't
-- handle many client ciphers at once), and then call find_ciphers_group on
-- each chunk.
local function find_ciphers(host, port, protocol)
local name, protocol_worked, results, chunk
local ciphers = in_chunks(cbc_ciphers, CHUNK_SIZE)
results = {}
-- Try every cipher.
for _, group in ipairs(ciphers) do
chunk, protocol_worked = find_ciphers_group(host, port, protocol, group)
if protocol_worked == nil then return nil end
for _, name in ipairs(chunk) do
table.insert(results, name)
end
-- Another POODLE shortcut
if protocol_worked then return results end
end
return results
end
portrule = function (host, port)
return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
end
action = function(host, port)
local vuln_table = {
title = "SSL POODLE information leak",
description = [[
The SSL protocol 3.0, as used in OpenSSL through 1.0.1i and
other products, uses nondeterministic CBC padding, which makes it easier
for man-in-the-middle attackers to obtain cleartext data via a
padding-oracle attack, aka the "POODLE" issue.]],
state = vulns.STATE.NOT_VULN,
IDS = {
CVE = 'CVE-2014-3566',
OSVDB = '113251'
},
SCORES = {
CVSSv2 = '4.3'
},
dates = {
disclosure = {
year = 2014, month = 10, day = 14
}
},
references = {
"https://www.openssl.org/~bodo/ssl-poodle.pdf",
"https://www.imperialviolet.org/2014/10/14/poodle.html"
}
}
local report = vulns.Report:new(SCRIPT_NAME, host, port)
local ciphers = find_ciphers(host, port, 'SSLv3')
if ciphers == nil then
vuln_table.check_results = { "SSLv3 not supported" }
return report:make_output(vuln_table)
end
if #ciphers == 0 then
vuln_table.check_results = { "No CBC ciphersuites found" }
return report:make_output(vuln_table)
end
-- else
vuln_table.check_results = ciphers
vuln_table.state = vulns.STATE.VULN
return report:make_output(vuln_table)
end