diff --git a/CHANGELOG b/CHANGELOG index 88bbd2888..3252b52da 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ #Nmap Changelog ($Id$); -*-text-*- +o [NSE] tls-alpn checks TLS servers for Application Layer Protocol Negotiation + (ALPN) support and reports supported protocols. ALPN largely replaces NPN, + which tls-nextprotoneg was written for. [Daniel Miller] + o [NSE][GH#1038][GH#1037] Script qscan was not observing interpacket delays (parameter qscan.delay). [nnposter] diff --git a/nselib/tls.lua b/nselib/tls.lua index 3858520f0..1b678f698 100644 --- a/nselib/tls.lua +++ b/nselib/tls.lua @@ -265,6 +265,13 @@ EXTENSION_HELPERS = { end return pack(">s2", table.concat(list)) end, + ["application_layer_protocol_negotiation"] = function(protocols) + local list = {} + for _, proto in ipairs(protocols) do + list[#list+1] = pack(">s1", proto) + end + return pack(">s2", table.concat(list)) + end, ["next_protocol_negotiation"] = tostring, } diff --git a/scripts/script.db b/scripts/script.db index 3c9e5d99b..d3f6e6298 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -552,6 +552,7 @@ Entry { filename = "telnet-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "telnet-encryption.nse", categories = { "discovery", "safe", } } Entry { filename = "telnet-ntlm-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "tftp-enum.nse", categories = { "discovery", "intrusive", } } +Entry { filename = "tls-alpn.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "tls-nextprotoneg.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "tls-ticketbleed.nse", categories = { "safe", "vuln", } } Entry { filename = "tn3270-screen.nse", categories = { "discovery", "safe", } } diff --git a/scripts/tls-alpn.nse b/scripts/tls-alpn.nse new file mode 100644 index 000000000..98ec771fe --- /dev/null +++ b/scripts/tls-alpn.nse @@ -0,0 +1,208 @@ +local nmap = require "nmap" +local shortport = require "shortport" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" +local sslcert = require "sslcert" +local tls = require "tls" + +description = [[ +Enumerates a TLS server's supported application-layer protocols using the ALPN protocol. + +Repeated queries are sent to determine which of the registered protocols are supported. + +For more information, see: +* https://tools.ietf.org/html/rfc7301 +]] + +--- +-- @usage +-- nmap --script=tls-alpn +-- +--@output +-- 443/tcp open https +-- | tls-alpn: +-- | h2 +-- | spdy/3 +-- |_ http/1.1 +-- +-- @xmloutput +-- h2 +-- spdy/3 +-- http/1.1 + + +author = "Daniel Miller" + +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" + +categories = {"discovery", "safe", "default"} + +portrule = function(host, port) + return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port) +end + + +local ALPN_NAME = "application_layer_protocol_negotiation" + +--- Function that sends a client hello packet with the TLS ALPN extension to the +-- target host and returns the response +--@args host The target host table. +--@args port The target port table. +--@return status true if response, false else. +--@return response if status is true. +local client_hello = function(host, port, protos) + local sock, status, response, err, cli_h + + cli_h = tls.client_hello({ + ["extensions"] = { + [ALPN_NAME] = tls.EXTENSION_HELPERS[ALPN_NAME](protos) + }, + }) + + -- Connect to the target server + local status, err + local sock + local specialized = sslcert.getPrepareTLSWithoutReconnect(port) + if specialized then + status, sock = specialized(host, port) + if not status then + stdnse.debug1("Connection to server failed: %s", sock) + return false + end + else + sock = nmap.new_socket() + status, err = sock:connect(host, port) + if not status then + stdnse.debug1("Connection to server failed: %s", err) + return false + end + end + + sock:set_timeout(5000) + + -- Send Client Hello to the target server + status, err = sock:send(cli_h) + if not status then + stdnse.debug1("Couldn't send: %s", err) + sock:close() + return false + end + + -- Read response + status, response, err = tls.record_buffer(sock) + if not status then + stdnse.debug1("Couldn't receive: %s", err) + sock:close() + return false + end + + return true, response +end + +--- Function that checks for the returned protocols to a ALPN extension request. +--@args response Response to parse. +--@return results List of found protocols. +local check_alpn = function(response) + local i, record = tls.record_read(response, 1) + if record == nil then + stdnse.debug1("Unknown response from server") + return nil + end + + if record.type == "handshake" and record.body[1].type == "server_hello" then + if record.body[1].extensions == nil then + stdnse.debug1("Server does not support TLS ALPN extension.") + return nil + end + local results = {} + local alpndata = record.body[1].extensions[ALPN_NAME] + if alpndata == nil then + stdnse.debug1("Server does not support TLS ALPN extension.") + return nil + end + -- Parse data + alpndata = string.unpack(">s2", alpndata, 1) + i = 1 + while i <= #alpndata do + if i > 1 then + stdnse.debug1("Server sent multiple protocols but RFC only permits 1") + end + local protocol + protocol, i = string.unpack(">s1", alpndata, i) + table.insert(results, protocol) + end + + if next(results) then + return results + else + stdnse.debug1("Server supports TLS ALPN extension, but no protocols were offered.") + return nil + end + else + stdnse.debug1("Server response was not server_hello") + return nil + end +end + +local function find_and_remove(t, value) + for i, v in ipairs(t) do + if v == value then + table.remove(t, i) + return true + end + end + return false +end + +action = function(host, port) + local alpn_protos = { + "http/1.1", + "spdy/1", + "spdy/2", + "spdy/3", + "stun.turn", + "stun.nat-discovery", + "h2", + "h2c", + "webrtc", + "c-webrtc", + "ftp", + "imap", + "pop3", + "managesieve", + } + + local chosen = {} + while next(alpn_protos) do + -- Send crafted client hello + local status, response = client_hello(host, port, alpn_protos) + if status and response then + -- Analyze response + local result = check_alpn(response) + if not result then + stdnse.debug1("None of %d protocols chosen", #alpn_protos) + break + end + for i, p in ipairs(result) do + if i > 1 then + stdnse.verbose1("Server violates RFC: sent additional protocol %s", p) + end + chosen[#chosen+1] = p + if not find_and_remove(alpn_protos, p) then + stdnse.verbose1("Chosen ALPN protocol %s was not offered", p) + if stdnse.contains(chosen, p) then + stdnse.debug1("Server is forcing %s", p) + break + end + end + end + else + stdnse.debug1("Client hello failed with %d protocols", #alpn_protos) + break + end + end + if next(chosen) then + return chosen + end +end