-
-
Save markandrewj/515ce77ba6f7b744f907 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local coroutine = require "coroutine" | |
local io = require "io" | |
local math = require "math" | |
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" | |
description = [[ | |
Stripped-down version of ssl-enum-ciphers that just 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 pass <code>--script-args poodle-all</code>, but you would | |
be better off using 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> | |
-- | |
-- @args poodle-all Enumerate all SSLv3 CBC ciphers instead of just the first. | |
-- | |
-- @output | |
-- PORT STATE SERVICE REASON | |
-- 443/tcp open https syn-ack | |
-- | ssl-poodle: | |
-- | SSLv3: | |
-- | ciphers: | |
-- |_ TLS_RSA_WITH_3DES_EDE_CBC_SHA | |
-- 443/tcp open https syn-ack | |
-- | ssl-poodle: | |
-- |_ SSLv3: No CBC ciphers found | |
author = "Daniel Miller <[email protected]>, Mak Kolybabi, Gabriel Lawrence" | |
license = "Same as Nmap--See http://nmap.org/book/man-legal.html" | |
categories = {"discovery"} | |
local poodle_all = stdnse.get_script_args("poodle-all") | |
-- 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 | |
-- Use Nmap's own discovered timeout, doubled for safety | |
-- Default to 10 seconds. | |
local timeout = ((host.times and host.times.timeout) or 5) * 1000 * 2 | |
-- 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 | |
if not poodle_all then break end | |
end | |
end | |
end | |
return results, protocol_worked | |
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( | |
-- POODLE only affects CBC ciphers | |
listop.filter(function(x) return string.find(x, "_CBC_",1,true) end, sorted_keys(tls.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 and not poodle_all then return results end | |
end | |
if not next(results) then return nil end | |
return results | |
end | |
local function try_protocol(host, port, protocol, upresults) | |
local ciphers, compressors, results | |
local condvar = nmap.condvar(upresults) | |
results = stdnse.output_table() | |
-- Find all valid ciphers. | |
ciphers = find_ciphers(host, port, protocol) | |
if ciphers == nil then | |
condvar "signal" | |
return nil | |
end | |
if #ciphers == 0 then | |
results = {ciphers={},compressors={}} | |
setmetatable(results,{ | |
__tostring=function(t) return "No CBC ciphers found" end | |
}) | |
upresults[protocol] = results | |
condvar "signal" | |
return nil | |
end | |
results["ciphers"] = ciphers | |
upresults[protocol] = results | |
condvar "signal" | |
return nil | |
end | |
portrule = function (host, port) | |
return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port) | |
end | |
--- Return a table that yields elements sorted by key when iterated over with pairs() | |
-- Should probably put this in a formatting library later. | |
-- Depends on keys() function defined above. | |
--@param t The table whose data should be used | |
--@return out A table that can be passed to pairs() to get sorted results | |
function sorted_by_key(t) | |
local out = {} | |
setmetatable(out, { | |
__pairs = function(_) | |
local order = sorted_keys(t) | |
return coroutine.wrap(function() | |
for i,k in ipairs(order) do | |
coroutine.yield(k, t[k]) | |
end | |
end) | |
end | |
}) | |
return out | |
end | |
action = function(host, port) | |
local name, result, results | |
results = {} | |
local condvar = nmap.condvar(results) | |
local threads = {} | |
-- POODLE shortcut: only one protocol matters | |
local co = stdnse.new_thread(try_protocol, host, port, 'SSLv3', results) | |
threads[co] = true | |
repeat | |
for thread in pairs(threads) do | |
if coroutine.status(thread) == "dead" then threads[thread] = nil end | |
end | |
if ( next(threads) ) then | |
condvar "wait" | |
end | |
until next(threads) == nil | |
if #( keys(results) ) == 0 then | |
return nil | |
end | |
return results | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment