diff options
Diffstat (limited to '')
-rw-r--r-- | scripts/tls-alpn.nse | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/scripts/tls-alpn.nse b/scripts/tls-alpn.nse new file mode 100644 index 0000000..caf276d --- /dev/null +++ b/scripts/tls-alpn.nse @@ -0,0 +1,237 @@ +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 <targets> +-- +--@output +-- 443/tcp open https +-- | tls-alpn: +-- | h2 +-- | spdy/3 +-- |_ http/1.1 +-- +-- @xmloutput +-- <elem>h2</elem> +-- <elem>spdy/3</elem> +-- <elem>http/1.1</elem> + + +author = "Daniel Miller" + +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" + +categories = {"discovery", "safe", "default"} +dependencies = {"https-redirect"} + +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({ + -- TLSv1.3 does not send this extension plaintext. + -- TODO: implement key exchange crypto to retrieve encrypted extensions + protocol = "TLSv1.2", + ["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 did not return TLS ALPN extension.") + return nil + end + local results = {} + local alpndata = record.body[1].extensions[ALPN_NAME] + if alpndata == nil then + stdnse.debug1("Server did not return 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 = { + -- IANA-registered names + -- https://www.iana.org/assignments/tls-extensiontype-values/alpn-protocol-ids.csv + -- Last-Modified: Thu, 31 Oct 2019 22:30:11 GMT + "http/0.9", + "http/1.0", + "http/1.1", + "spdy/1", + "spdy/2", + "spdy/3", + "stun.turn", + "stun.nat-discovery", + "h2", + "h2c", -- should never be negotiated over TLS + "webrtc", + "c-webrtc", + "ftp", + "imap", + "pop3", + "managesieve", + "coap", + "xmpp-client", + "xmpp-server", + "acme-tls/1", + "mqtt", + "dot", + "ntske/1", + "sunrpc", + "h3", + "smb", + "irc", + "nntp", + "nnsp", + "doq", + -- Other sources + "grpc-exp", -- gRPC, see grpc.io + } + + local chosen = {} + local unique = {} + 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) + goto ALPN_DONE + end + for i, p in ipairs(result) do + if i > 1 then + stdnse.verbose1("Server violates RFC: sent additional protocol %s", p) + else + if not unique[p] then + chosen[#chosen+1] = p + end + unique[p] = true + if not find_and_remove(alpn_protos, p) then + stdnse.debug1("Chosen ALPN protocol %s was not offered", p) + -- Server is forcing this protocol, no need to continue offering. + goto ALPN_DONE + end + end + end + else + stdnse.debug1("Client hello failed with %d protocols", #alpn_protos) + goto ALPN_DONE + end + end + ::ALPN_DONE:: + if next(chosen) then + return chosen + end +end |