329 lines
9.9 KiB
Lua
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
|