Skip to content

Instantly share code, notes, and snippets.

@markandrewj
Forked from bonsaiviking/ssl-poodle.md
Last active August 29, 2015 14:07
Show Gist options
  • Save markandrewj/515ce77ba6f7b744f907 to your computer and use it in GitHub Desktop.
Save markandrewj/515ce77ba6f7b744f907 to your computer and use it in GitHub Desktop.
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 <bonsaiviking@gmail.com>, 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