summaryrefslogtreecommitdiffstats
path: root/scripts/ssl-ccs-injection.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/ssl-ccs-injection.nse')
-rw-r--r--scripts/ssl-ccs-injection.nse328
1 files changed, 328 insertions, 0 deletions
diff --git a/scripts/ssl-ccs-injection.nse b/scripts/ssl-ccs-injection.nse
new file mode 100644
index 0000000..fd34bb1
--- /dev/null
+++ b/scripts/ssl-ccs-injection.nse
@@ -0,0 +1,328 @@
+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