coolbins/system/usr/share/nmap/scripts/ssl-ccs-injection.nse

329 lines
9.9 KiB
Lua

local nmap = require('nmap')
local shortport = require('shortport')
local sslcert = require('sslcert')
local stdnse = require('stdnse')
local vulns = require('vulns')
local tls = require 'tls'
local tableaux = require "tableaux"
description = [[
Detects whether a server is vulnerable to the SSL/TLS "CCS Injection"
vulnerability (CVE-2014-0224), first discovered by Masashi Kikuchi.
The script is based on the ccsinjection.c code authored by Ramon de C Valle
(https://gist.github.com/rcvalle/71f4b027d61a78c42607)
In order to exploit the vulnerablity, a MITM attacker would effectively
do the following:
o Wait for a new TLS connection, followed by the ClientHello
ServerHello handshake messages.
o Issue a CCS packet in both the directions, which causes the OpenSSL
code to use a zero length pre master secret key. The packet is sent
to both ends of the connection. Session Keys are derived using a
zero length pre master secret key, and future session keys also
share this weakness.
o Renegotiate the handshake parameters.
o The attacker is now able to decrypt or even modify the packets
in transit.
The script works by sending a 'ChangeCipherSpec' message out of order and
checking whether the server returns an 'UNEXPECTED_MESSAGE' alert record
or not. Since a non-patched server would simply accept this message, the
CCS packet is sent twice, in order to force an alert from the server. If
the alert type is different than 'UNEXPECTED_MESSAGE', we can conclude
the server is vulnerable.
]]
---
-- @usage
-- nmap -p 443 --script ssl-ccs-injection <target>
--
-- @output
-- PORT STATE SERVICE
-- 443/tcp open https
-- | ssl-ccs-injection:
-- | VULNERABLE:
-- | SSL/TLS MITM vulnerability (CCS Injection)
-- | State: VULNERABLE
-- | Risk factor: High
-- | Description:
-- | OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before
-- | 1.0.1h does not properly restrict processing of ChangeCipherSpec
-- | messages, which allows man-in-the-middle attackers to trigger use
-- | of a zero-length master key in certain OpenSSL-to-OpenSSL
-- | communications, and consequently hijack sessions or obtain
-- | sensitive information, via a crafted TLS handshake, aka the
-- | "CCS Injection" vulnerability.
-- |
-- | References:
-- | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0224
-- | http://www.cvedetails.com/cve/2014-0224
-- |_ http://www.openssl.org/news/secadv_20140605.txt
author = "Claudiu Perta <claudiu.perta@gmail.com>"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = { "vuln", "safe" }
dependencies = {"https-redirect"}
portrule = function(host, port)
return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
end
local Error = {
NOT_VULNERABLE = 0,
CONNECT = 1,
PROTOCOL_MISMATCH = 2,
SSL_HANDSHAKE = 3,
TIMEOUT = 4
}
---
-- Reads an SSL/TLS record and returns true if it's any fatal
-- alert and false otherwise.
local function fatal_alert(s)
local status, buffer = tls.record_buffer(s)
if not status then
return false
end
local position, record = tls.record_read(buffer, 1)
if record == nil then
return false
end
if record.type ~= "alert" then
return false
end
for _, body in ipairs(record.body) do
if body.level == "fatal" then
return true
end
end
return false
end
---
-- Reads an SSL/TLS record and returns true if it's a fatal,
-- 'unexpected_message' alert and false otherwise.
local function alert_unexpected_message(s)
local status, buffer
status, buffer = tls.record_buffer(s, buffer, 1)
if not status then
return false
end
local position, record = tls.record_read(buffer, 1)
if record == nil then
return false
end
if record.type ~= "alert" then
-- Mark this as VULNERABLE, we expect an alert record
return true,true
end
for _, body in ipairs(record.body) do
if body.level == "fatal" and body.description == "unexpected_message" then
return true,false
end
end
return true,true
end
local function test_ccs_injection(host, port, version)
local hello = tls.client_hello({
["protocol"] = version,
-- Only negotiate SSLv3 on its own;
-- TLS implementations may refuse to answer if SSLv3 is mentioned.
["record_protocol"] = (version == "SSLv3") and "SSLv3" or "TLSv1.0",
-- Claim to support every cipher
-- Doesn't work with IIS, but IIS isn't vulnerable
["ciphers"] = tableaux.keys(tls.CIPHERS),
["compressors"] = {"NULL"},
["extensions"] = {
-- Claim to support common elliptic curves
["elliptic_curves"] = tls.EXTENSION_HELPERS["elliptic_curves"](
tls.DEFAULT_ELLIPTIC_CURVES),
},
})
local status, err
local s
local specialized = sslcert.getPrepareTLSWithoutReconnect(port)
if specialized then
status, s = specialized(host, port)
if not status then
stdnse.debug3("Connection to server failed: %s", s)
return false, Error.CONNECT
end
else
s = nmap.new_socket()
status, err = s:connect(host, port)
if not status then
stdnse.debug3("Connection to server failed: %s", err)
return false, Error.CONNECT
end
end
-- Set a sufficiently large timeout
s:set_timeout(10000)
-- Send Client Hello to the target server
status, err = s:send(hello)
if not status then
stdnse.debug1("Couldn't send Client Hello: %s", err)
s:close()
return false, Error.CONNECT
end
-- Read response
local done = false
local i = 1
local response
repeat
status, response, err = tls.record_buffer(s, response, i)
if err == "TIMEOUT" or not status then
stdnse.verbose1("No response from server: %s", err)
s:close()
return false, Error.TIMEOUT
end
local record
i, record = tls.record_read(response, i)
if record == nil then
stdnse.debug1("Unknown response from server")
s:close()
return false, Error.NOT_VULNERABLE
elseif record.protocol ~= version then
stdnse.debug1("Protocol version mismatch (%s)", version)
s:close()
return false, Error.PROTOCOL_MISMATCH
elseif record.type == "alert" then
for _, body in ipairs(record.body) do
if body.level == "fatal" then
stdnse.debug1("Fatal alert: %s", body.description)
-- Could be something else, but this lets us retry
return false, Error.PROTOCOL_MISMATCH
end
end
end
if record.type == "handshake" then
for _, body in ipairs(record.body) do
if body.type == "server_hello_done" then
stdnse.debug1("Handshake completed (%s)", version)
done = true
end
end
end
until done
-- Send the change_cipher_spec message twice to
-- force an alert in the case the server is not
-- patched.
-- change_cipher_spec message
local ccs = tls.record_write(
"change_cipher_spec", version, "\x01")
-- Send the first ccs message
status, err = s:send(ccs)
if not status then
stdnse.debug1("Couldn't send first ccs message: %s", err)
s:close()
return false, Error.SSL_HANDSHAKE
end
-- Optimistically read the first alert message
-- Shorter timeout: we expect most servers will bail at this point.
s:set_timeout(stdnse.get_timeout(host))
-- If we got an alert right away, we can stop right away: it's not vulnerable.
if fatal_alert(s) then
s:close()
return false, Error.NOT_VULNERABLE
end
-- Restore our slow timeout
s:set_timeout(10000)
-- Send the second ccs message
status, err = s:send(ccs)
if not status then
stdnse.debug1("Couldn't send second ccs message: %s", err)
s:close()
return false, Error.SSL_HANDSHAKE
end
-- Read the alert message
local vulnerable
status,vulnerable = alert_unexpected_message(s)
-- Leave the target not vulnerable in case of an error. This could occur
-- when running against a different TLS/SSL implementations (e.g., GnuTLS)
if not status then
stdnse.debug1("Couldn't get reply from the server (probably not OpenSSL)")
s:close()
return false, Error.SSL_HANDSHAKE
end
if not vulnerable then
stdnse.debug1("Server returned UNEXPECTED_MESSAGE alert, not vulnerable")
s:close()
return false, Error.NOT_VULNERABLE
else
stdnse.debug1("Vulnerable - alert is not UNEXPECTED_MESSAGE")
s:close()
return true
end
end
action = function(host, port)
local vuln_table = {
title = "SSL/TLS MITM vulnerability (CCS Injection)",
state = vulns.STATE.NOT_VULN,
risk_factor = "High",
description = [[
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h
does not properly restrict processing of ChangeCipherSpec messages,
which allows man-in-the-middle attackers to trigger use of a zero
length master key in certain OpenSSL-to-OpenSSL communications, and
consequently hijack sessions or obtain sensitive information, via
a crafted TLS handshake, aka the "CCS Injection" vulnerability.
]],
references = {
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-0224',
'http://www.cvedetails.com/cve/2014-0224',
'http://www.openssl.org/news/secadv_20140605.txt'
}
}
local report = vulns.Report:new(SCRIPT_NAME, host, port)
-- client hello will support multiple versions of TLS. We only retry to fall
-- back to SSLv3, which some implementations won't allow in combination with
-- newer versions.
for _, tls_version in ipairs({"TLSv1.2", "SSLv3"}) do
local vulnerable, err = test_ccs_injection(host, port, tls_version)
-- Return an explicit message in case of a TIMEOUT,
-- to avoid considering this as not vulnerable.
if err == Error.TIMEOUT then
return "No reply from server (TIMEOUT)"
end
if err ~= Error.PROTOCOL_MISMATCH then
if vulnerable then
vuln_table.state = vulns.STATE.VULN
end
break
end
end
return report:make_output(vuln_table)
end