diff options
Diffstat (limited to '')
-rw-r--r-- | scripts/ssl-ccs-injection.nse | 328 |
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 |