mirror of
https://github.com/nmap/nmap.git
synced 2025-12-12 18:59:03 +00:00
Add sslv2-drown. Closes #365
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# Nmap Changelog ($Id$); -*-text-*-
|
||||
|
||||
o [NSE][GH#365] Added sslv2-drown for detecting vulnerability to the DROWN
|
||||
attack, including CVE-2016-0703 and CVE-2016-0704 that enable fast attacks on
|
||||
OpenSSL. [Bertrand Bonnefoy-Claudet]
|
||||
|
||||
o [NSE] Added http-mcmp for detecting mod_cluster Management Protocol (MCMP)
|
||||
and dumping its configuration. [Frank Spierings]
|
||||
|
||||
|
||||
@@ -483,6 +483,7 @@ Entry { filename = "ssl-google-cert-catalog.nse", categories = { "discovery", "e
|
||||
Entry { filename = "ssl-heartbleed.nse", categories = { "safe", "vuln", } }
|
||||
Entry { filename = "ssl-known-key.nse", categories = { "default", "discovery", "safe", "vuln", } }
|
||||
Entry { filename = "ssl-poodle.nse", categories = { "safe", "vuln", } }
|
||||
Entry { filename = "sslv2-drown.nse", categories = { "intrusive", "vuln", } }
|
||||
Entry { filename = "sslv2.nse", categories = { "default", "safe", } }
|
||||
Entry { filename = "sstp-discover.nse", categories = { "default", "discovery", } }
|
||||
Entry { filename = "stun-info.nse", categories = { "discovery", "safe", } }
|
||||
|
||||
334
scripts/sslv2-drown.nse
Normal file
334
scripts/sslv2-drown.nse
Normal file
@@ -0,0 +1,334 @@
|
||||
local nmap = require "nmap"
|
||||
local shortport = require "shortport"
|
||||
local table = require "table"
|
||||
local stdnse = require "stdnse"
|
||||
local string = require "string"
|
||||
local sslcert = require "sslcert"
|
||||
local sslv2 = require "sslv2"
|
||||
local vulns = require "vulns"
|
||||
|
||||
description = [[
|
||||
Determines whether the server supports SSLv2, what ciphers it supports and tests for
|
||||
CVE-2015-3197, CVE-2016-0703 and CVE-2016-0800 (DROWN)
|
||||
]]
|
||||
author = "Bertrand Bonnefoy-Claudet <bertrand@cryptosense.com>"
|
||||
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
|
||||
-- We can use the set of ciphers detected by sslv2.nse to avoid 1 handshake
|
||||
dependencies = {"sslv2"}
|
||||
categories = {"intrusive", "vuln"}
|
||||
|
||||
---
|
||||
-- @output
|
||||
-- 443/tcp open https
|
||||
-- | sslv2-drown:
|
||||
-- | ciphers:
|
||||
-- | SSL2_DES_192_EDE3_CBC_WITH_MD5
|
||||
-- | SSL2_IDEA_128_CBC_WITH_MD5
|
||||
-- | SSL2_RC2_128_CBC_WITH_MD5
|
||||
-- | SSL2_RC4_128_WITH_MD5
|
||||
-- | SSL2_DES_64_CBC_WITH_MD5
|
||||
-- | forced_ciphers:
|
||||
-- | SSL2_RC2_128_CBC_EXPORT40_WITH_MD5
|
||||
-- | SSL2_RC4_128_EXPORT40_WITH_MD5
|
||||
-- | vulns:
|
||||
-- | CVE-2016-0800:
|
||||
-- | title: OpenSSL: Cross-protocol attack on TLS using SSLv2 (DROWN)
|
||||
-- | state: VULNERABLE
|
||||
-- | ids:
|
||||
-- | CVE:CVE-2016-0800
|
||||
-- | description:
|
||||
-- | The SSLv2 protocol, as used in OpenSSL before 1.0.1s and 1.0.2 before 1.0.2g and
|
||||
-- | other products, requires a server to send a ServerVerify message before establishing
|
||||
-- | that a client possesses certain plaintext RSA data, which makes it easier for remote
|
||||
-- | attackers to decrypt TLS ciphertext data by leveraging a Bleichenbacher RSA padding
|
||||
-- | oracle, aka a "DROWN" attack.
|
||||
-- |
|
||||
-- | refs:
|
||||
-- | https://www.openssl.org/news/secadv/20160301.txt
|
||||
-- |_ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0800
|
||||
--
|
||||
-- @xmloutput
|
||||
-- <table key="ciphers">
|
||||
-- <elem>SSL2_DES_192_EDE3_CBC_WITH_MD5</elem>
|
||||
-- <elem>SSL2_IDEA_128_CBC_WITH_MD5</elem>
|
||||
-- <elem>SSL2_RC2_128_CBC_WITH_MD5</elem>
|
||||
-- <elem>SSL2_RC4_128_WITH_MD5</elem>
|
||||
-- <elem>SSL2_DES_64_CBC_WITH_MD5</elem>
|
||||
-- </table>
|
||||
-- <table key="forced_ciphers">
|
||||
-- <elem>SSL2_RC2_128_CBC_EXPORT40_WITH_MD5</elem>
|
||||
-- <elem>SSL2_RC4_128_EXPORT40_WITH_MD5</elem>
|
||||
-- </table>
|
||||
-- <table key="vulns">
|
||||
-- <table key="CVE-2016-0800">
|
||||
-- <elem key="title">OpenSSL: Cross-protocol attack on TLS using SSLv2 (DROWN)</elem>
|
||||
-- <elem key="state">VULNERABLE</elem>
|
||||
-- <table key="ids">
|
||||
-- <elem>CVE:CVE-2016-0800</elem>
|
||||
-- </table>
|
||||
-- <table key="description">
|
||||
-- <elem>
|
||||
-- The SSLv2 protocol, as used in OpenSSL before 1.0.1s and 1.0.2 before
|
||||
-- 1.0.2g and other products, requires a server to send a ServerVerify
|
||||
-- message before establishing that a client possesses certain plaintext
|
||||
-- RSA data, which makes it easier for remote attackers to decrypt TLS
|
||||
-- ciphertext data by leveraging a Bleichenbacher RSA padding oracle, aka
|
||||
-- a "DROWN" attack.
|
||||
-- </elem>
|
||||
-- </table>
|
||||
-- <table key="refs">
|
||||
-- <elem>https://www.openssl.org/news/secadv/20160301.txt</elem>
|
||||
-- <elem>https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0800</elem>
|
||||
-- </table>
|
||||
-- </table>
|
||||
-- </table>
|
||||
|
||||
|
||||
-- Those ciphers are weak enough to enable a "General DROWN" attack.
|
||||
local GENERAL_DROWN_CIPHERS = {}
|
||||
for k, v in pairs(sslv2.SSL_CIPHERS) do
|
||||
-- 40 bits or less, or single-DES (56 bits)
|
||||
if v.encrypted_key_length <= 5 or v.str:find("DES_64") then
|
||||
GENERAL_DROWN_CIPHERS[v.str] = true
|
||||
end
|
||||
end
|
||||
|
||||
portrule = function(host, port)
|
||||
return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
|
||||
end
|
||||
|
||||
-- Return whether all values of "t1" are also values in "t2".
|
||||
local function values_in(t1, t2)
|
||||
local set = {}
|
||||
for _, e in pairs(t2) do
|
||||
set[e] = true
|
||||
end
|
||||
for _, e in pairs(t1) do
|
||||
if not set[e] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Create a socket ready to begin an SSL negotiation and send client_hello
|
||||
local function do_setup(host, port)
|
||||
local timeout = stdnse.get_timeout(host, 10000, 5000)
|
||||
local status, socket, err
|
||||
local starttls = sslcert.getPrepareTLSWithoutReconnect(port)
|
||||
if starttls then
|
||||
status, socket = starttls(host, port)
|
||||
if not status then
|
||||
stdnse.debug(1, "Can't connect using STARTTLS: %s", socket)
|
||||
return nil
|
||||
end
|
||||
else
|
||||
socket = nmap.new_socket()
|
||||
socket:set_timeout(timeout)
|
||||
status, err = socket:connect(host, port)
|
||||
if not status then
|
||||
stdnse.debug(1, "Can't connect: %s", err)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
socket:set_timeout(timeout)
|
||||
socket:send(sslv2.client_hello(stdnse.keys(sslv2.SSL_CIPHER_CODES)))
|
||||
local status, buffer = sslv2.record_buffer(socket)
|
||||
if not status then
|
||||
socket:close()
|
||||
return false
|
||||
end
|
||||
return socket, buffer
|
||||
end
|
||||
|
||||
local function try_force_cipher(host, port, cipher)
|
||||
local socket, buffer = do_setup(host, port)
|
||||
if not socket then
|
||||
return false
|
||||
end
|
||||
|
||||
local i, server_hello = sslv2.record_read(buffer)
|
||||
|
||||
local code = sslv2.SSL_CIPHER_CODES[cipher]
|
||||
local key_length = sslv2.SSL_CIPHERS[code].key_length
|
||||
local encrypted_key_length = sslv2.SSL_CIPHERS[code].encrypted_key_length
|
||||
|
||||
local dummy_key = string.rep("\0", key_length)
|
||||
local clear_key = dummy_key:sub(1, key_length - encrypted_key_length)
|
||||
local encrypted_key = dummy_key:sub(key_length - encrypted_key_length + 1)
|
||||
|
||||
local dummy_client_master_key = sslv2.client_master_secret(cipher, clear_key, encrypted_key)
|
||||
socket:send(dummy_client_master_key)
|
||||
local status, buffer = sslv2.record_buffer(socket, buffer, i)
|
||||
socket:close()
|
||||
if not status then
|
||||
return false
|
||||
end
|
||||
local i, message = sslv2.record_read(buffer, i)
|
||||
|
||||
-- Treat an error as a failure to force the cipher.
|
||||
if not message or message.message_type == sslv2.SSL_MESSAGE_TYPES.ERROR then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function has_extra_clear_bug(host, port, cipher)
|
||||
local socket, buffer = do_setup(host, port)
|
||||
if not socket then
|
||||
return false
|
||||
end
|
||||
|
||||
local i, server_hello = sslv2.record_read(buffer)
|
||||
|
||||
local code = sslv2.SSL_CIPHER_CODES[cipher]
|
||||
local key_length = sslv2.SSL_CIPHERS[code].key_length
|
||||
local encrypted_key_length = sslv2.SSL_CIPHERS[code].encrypted_key_length
|
||||
|
||||
-- The length of clear_key is intentionally wrong to highlight the bug.
|
||||
local clear_key = string.rep("\0", key_length - encrypted_key_length + 1)
|
||||
local encrypted_key = string.rep("\0", encrypted_key_length)
|
||||
|
||||
local dummy_client_master_key = sslv2.client_master_secret(cipher, clear_key, encrypted_key)
|
||||
socket:send(dummy_client_master_key)
|
||||
local status, buffer, err = sslv2.record_buffer(socket, buffer, i)
|
||||
socket:close()
|
||||
if not status then
|
||||
return false
|
||||
end
|
||||
local i, message = sslv2.record_read(buffer, i)
|
||||
|
||||
-- Treat an error as a failure to force the cipher.
|
||||
if not message or message.message_type == sslv2.SSL_MESSAGE_TYPES.ERROR then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function registry_get(host, port)
|
||||
if host.registry.sslv2 then
|
||||
return host.registry.sslv2[port.number .. port.protocol]
|
||||
end
|
||||
end
|
||||
|
||||
local function unique (t)
|
||||
local tc = {};
|
||||
for k,v in ipairs(t) do
|
||||
tc[v] = true;
|
||||
end
|
||||
return tc;
|
||||
end
|
||||
|
||||
function action(host, port)
|
||||
local output = stdnse.output_table()
|
||||
local report = vulns.Report:new("sslv2-drown", host, port)
|
||||
local cve_2015_3197 = {
|
||||
title = "OpenSSL: SSLv2 doesn't block disabled ciphers",
|
||||
state = vulns.STATE.NOT_VULN,
|
||||
IDS = {
|
||||
CVE = 'CVE-2015-3197',
|
||||
},
|
||||
risk_factor = "Low",
|
||||
description = [[
|
||||
ssl/s2_srvr.c in OpenSSL 1.0.1 before 1.0.1r and 1.0.2 before 1.0.2f does not
|
||||
prevent use of disabled ciphers, which makes it easier for man-in-the-middle
|
||||
attackers to defeat cryptographic protection mechanisms by performing computations
|
||||
on SSLv2 traffic, related to the get_client_master_key and get_client_hello
|
||||
functions.
|
||||
]],
|
||||
references = {
|
||||
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-3197",
|
||||
"https://www.openssl.org/news/secadv/20160128.txt",
|
||||
},
|
||||
}
|
||||
local cve_2016_0703 = {
|
||||
title = "OpenSSL: Divide-and-conquer session key recovery in SSLv2",
|
||||
state = vulns.STATE.NOT_VULN,
|
||||
IDS = {
|
||||
CVE = 'CVE-2016-0703',
|
||||
},
|
||||
risk_factor = "High",
|
||||
description = [[
|
||||
The get_client_master_key function in s2_srvr.c in the SSLv2 implementation in
|
||||
OpenSSL before 0.9.8zf, 1.0.0 before 1.0.0r, 1.0.1 before 1.0.1m, and 1.0.2 before
|
||||
1.0.2a accepts a nonzero CLIENT-MASTER-KEY CLEAR-KEY-LENGTH value for an arbitrary
|
||||
cipher, which allows man-in-the-middle attackers to determine the MASTER-KEY value
|
||||
and decrypt TLS ciphertext data by leveraging a Bleichenbacher RSA padding oracle, a
|
||||
related issue to CVE-2016-0800.
|
||||
]],
|
||||
references = {
|
||||
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0703",
|
||||
"https://www.openssl.org/news/secadv/20160301.txt",
|
||||
},
|
||||
}
|
||||
local cve_2016_0800 = {
|
||||
title = "OpenSSL: Cross-protocol attack on TLS using SSLv2 (DROWN)",
|
||||
state = vulns.STATE.NOT_VULN,
|
||||
IDS = {
|
||||
CVE = 'CVE-2016-0800',
|
||||
},
|
||||
risk_factor = "High",
|
||||
description = [[
|
||||
The SSLv2 protocol, as used in OpenSSL before 1.0.1s and 1.0.2 before 1.0.2g and
|
||||
other products, requires a server to send a ServerVerify message before establishing
|
||||
that a client possesses certain plaintext RSA data, which makes it easier for remote
|
||||
attackers to decrypt TLS ciphertext data by leveraging a Bleichenbacher RSA padding
|
||||
oracle, aka a "DROWN" attack.
|
||||
]],
|
||||
references = {
|
||||
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-0800",
|
||||
"https://www.openssl.org/news/secadv/20160301.txt",
|
||||
},
|
||||
}
|
||||
local offered_ciphers = registry_get(host, port) or sslv2.test_sslv2(host, port)
|
||||
if not offered_ciphers then
|
||||
output.vulns = report:make_output()
|
||||
return output
|
||||
end
|
||||
if next(offered_ciphers) then
|
||||
output.ciphers = offered_ciphers
|
||||
end
|
||||
|
||||
-- CVE-2015-3197
|
||||
local forced_ciphers = {}
|
||||
local all_ciphers = unique(offered_ciphers)
|
||||
for cipher, code in pairs(sslv2.SSL_CIPHER_CODES) do
|
||||
if not all_ciphers[cipher] and try_force_cipher(host, port, cipher) then
|
||||
all_ciphers[cipher] = true
|
||||
table.insert(forced_ciphers, cipher)
|
||||
end
|
||||
end
|
||||
if next(forced_ciphers) then
|
||||
output.forced_ciphers = forced_ciphers
|
||||
cve_2015_3197.state = vulns.STATE.VULN
|
||||
end
|
||||
|
||||
-- CVE-2016-0703
|
||||
local cipher, _ = next(all_ciphers)
|
||||
local result = has_extra_clear_bug(host, port, cipher)
|
||||
if result then
|
||||
cve_2016_0703.state = vulns.STATE.VULN
|
||||
end
|
||||
|
||||
|
||||
-- CVE-2016-0800
|
||||
local has_weak_ciphers = false
|
||||
for cipher, _ in pairs(all_ciphers) do
|
||||
if GENERAL_DROWN_CIPHERS[cipher] then
|
||||
has_weak_ciphers = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if has_weak_ciphers or cve_2016_0703.state == vulns.STATE.VULN then
|
||||
cve_2016_0800.state = vulns.STATE.VULN
|
||||
end
|
||||
|
||||
report:add_vulns(cve_2015_3197)
|
||||
report:add_vulns(cve_2016_0703)
|
||||
report:add_vulns(cve_2016_0800)
|
||||
|
||||
output.vulns = report:make_output()
|
||||
return output
|
||||
end
|
||||
Reference in New Issue
Block a user