diff options
Diffstat (limited to 'scripts/ssl-date.nse')
-rw-r--r-- | scripts/ssl-date.nse | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/scripts/ssl-date.nse b/scripts/ssl-date.nse new file mode 100644 index 0000000..6666305 --- /dev/null +++ b/scripts/ssl-date.nse @@ -0,0 +1,215 @@ +local shortport = require "shortport" +local stdnse = require "stdnse" +local math = require "math" +local nmap = require "nmap" +local os = require "os" +local string = require "string" +local sslcert = require "sslcert" +local tls = require "tls" +local datetime = require "datetime" + +description = [[ +Retrieves a target host's time and date from its TLS ServerHello response. + + +In many TLS implementations, the first four bytes of server randomness +are a Unix timestamp. The script will test whether this is indeed true +and report the time only if it passes this test. + +Original idea by Jacob Appelbaum and his TeaTime and tlsdate tools: +* https://github.com/ioerror/TeaTime +* https://github.com/ioerror/tlsdate +]] + +--- +-- @usage +-- nmap <target> --script=ssl-date +-- +-- @output +-- PORT STATE SERVICE REASON +-- 5222/tcp open xmpp-client syn-ack +-- |_ssl-date: 2012-08-02T18:29:31Z; +4s from local time. +-- +-- @xmloutput +-- <elem key="date">2012-08-02T18:29:31+00:00</elem> +-- <elem key="delta">4</elem> + +author = {"Aleksandar Nikolic", "nnposter"} +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 + +-- Miscellaneous script-wide constants +local conn_timeout = 5 -- connection timeout (seconds) +local max_clock_skew = 90*60 -- maximum acceptable difference between target + -- and scanner clocks to avoid additional + -- testing (seconds) +local max_clock_jitter = 5 -- maximum acceptable target clock jitter + -- Logically should be 50-100% of conn_timeout + -- (seconds) +local detail_debug = 2 -- debug level for printing detailed steps + + +--- Function that sends a client hello packet +-- 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) + local sock, status, response, err, cli_h + + -- Craft Client Hello + cli_h = tls.client_hello() + + -- Connect to the target server + local specialized_function = sslcert.getPrepareTLSWithoutReconnect(port) + + if not specialized_function then + sock = nmap.new_socket() + sock:set_timeout(1000 * conn_timeout) + status, err = sock:connect(host, port) + if not status then + sock:close() + stdnse.debug("Can't connect: %s", err) + return false + end + else + status,sock = specialized_function(host,port) + if not status then + return false + end + end + + + repeat -- only once + -- Send Client Hello to the target server + status, err = sock:send(cli_h) + if not status then + stdnse.debug("Couldn't send: %s", err) + break + end + + -- Read response + status, response, err = tls.record_buffer(sock) + if not status then + stdnse.debug("Couldn't receive: %s", err) + break + end + until true + + sock:close() + return status, response +end + +-- extract time from ServerHello response +local extract_time = function(response) + local i, record = tls.record_read(response, 1) + if record == nil then + stdnse.debug("Unknown response from server") + return nil + end + + if record.type == "handshake" then + for _, body in ipairs(record.body) do + if body.type == "server_hello" then + return true, body.time + end + end + end + stdnse.debug("Server response was not server_hello") + return nil +end + + +--- +-- Retrieve a timestamp from a TLS port and compare it to the scanner clock +-- +-- @param host TLS host +-- @param port TLS port +-- @return Timestamp sample object or nil (if the operation failed) +local get_time_sample = function (host, port) + -- Send crafted client hello + local rstatus, response = client_hello(host, port) + local stm = os.time() + if not (rstatus and response) then return nil end + -- extract time from response + local tstatus, ttm = extract_time(response) + if not tstatus then return nil end + stdnse.debug(detail_debug, "TLS sample: %s", datetime.format_timestamp(ttm, 0)) + return {target=ttm, scanner=stm, delta=os.difftime(ttm, stm)} +end + + +local result = { STAGNANT = "stagnant", + ACCEPTED = "accepted", + REJECTED = "rejected" } + +--- +-- Obtain a new timestamp sample and validate it against a reference sample +-- +-- @param host TLS host +-- @param port TLS port +-- @param reftm Reference timestamp sample +-- @return Result code +-- @return New timestamp sample object or nil (if the operation failed) +local test_time_sample = function (host, port, reftm) + local tm = get_time_sample(host, port) + if not tm then return nil end + local tchange = os.difftime(tm.target, reftm.target) + local schange = os.difftime(tm.scanner, reftm.scanner) + local status = + -- clock cannot run backwards or drift rapidly + (tchange < 0 or math.abs(tchange - schange) > max_clock_jitter) + and result.REJECTED + -- the clock did not advance + or tchange == 0 + and result.STAGNANT + -- plausible enough + or result.ACCEPTED + stdnse.debug(detail_debug, "TLS sample verdict: %s", status) + return status, tm +end + + +action = function(host, port) + local tm = get_time_sample(host, port) + if not tm then + return stdnse.format_output(false, "Unable to obtain data from the target") + end + if math.abs(tm.delta) > max_clock_skew then + -- The target clock differs substantially from the scanner + -- Let's take another sample to eliminate cases where the TLS field + -- contains either random or fixed data instead of the timestamp + local reftm = tm + local status + status, tm = test_time_sample(host, port, reftm) + if status and status == result.STAGNANT then + -- The target clock did not advance between the two samples (reftm, tm) + -- Let's wait long enough for the target clock to advance + -- and then re-take the second sample + stdnse.sleep(1.1) + status, tm = test_time_sample(host, port, reftm) + end + if not status then + return nil + end + if status ~= result.ACCEPTED then + return {}, "TLS randomness does not represent time" + end + end + + datetime.record_skew(host, tm.target, tm.scanner) + local output = { + date = datetime.format_timestamp(tm.target, 0), + delta = tm.delta, + } + return output, + string.format("%s; %s from scanner time.", output.date, + datetime.format_difftime(os.date("!*t", tm.target), + os.date("!*t", tm.scanner))) +end |