summaryrefslogtreecommitdiffstats
path: root/scripts/clock-skew.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/clock-skew.nse')
-rw-r--r--scripts/clock-skew.nse179
1 files changed, 179 insertions, 0 deletions
diff --git a/scripts/clock-skew.nse b/scripts/clock-skew.nse
new file mode 100644
index 0000000..14f80d1
--- /dev/null
+++ b/scripts/clock-skew.nse
@@ -0,0 +1,179 @@
+local datetime = require "datetime"
+local formulas = require "formulas"
+local math = require "math"
+local nmap = require "nmap"
+local outlib = require "outlib"
+local stdnse = require "stdnse"
+local table = require "table"
+
+-- These scripts contribute clock skews, so we need them to run first.
+-- portrule scripts do not always run before hostrule scripts, and certainly
+-- not before the hostrule is evaluated.
+dependencies = {
+ "bitcoin-info",
+ "http-date",
+ "http-ntlm-info",
+ "imap-ntlm-info",
+ "memcached-info",
+ "ms-sql-ntlm-info",
+ "nntp-ntlm-info",
+ "ntp-info",
+ "openwebnet-discovery",
+ "pop3-ntlm-info",
+ "rfc868-time",
+ "smb-os-discovery",
+ "smb-security-mode",
+ "smb2-time",
+ "smb2-vuln-uptime",
+ "smtp-ntlm-info",
+ "ssl-date",
+ "telnet-ntlm-info",
+}
+
+description = [[
+Analyzes the clock skew between the scanner and various services that report timestamps.
+
+At the end of the scan, it will show groups of systems that have similar median
+clock skew among their services. This can be used to identify targets with
+similar configurations, such as those that share a common time server.
+
+You must run at least 1 of the following scripts to collect clock data:
+* ]] .. table.concat(dependencies, "\n* ") .. "\n"
+
+---
+-- @output
+-- Host script results:
+-- |_clock-skew: mean: -13s, deviation: 12s, median: -6s
+--
+-- Post-scan script results:
+-- | clock-skew:
+-- | -6s: Majority of systems scanned
+-- | 3s:
+-- | 192.0.2.5
+-- |_ 192.0.2.7 (example.com)
+--
+-- @xmloutput
+-- <elem key="stddev">12.124355652982</elem>
+-- <elem key="mean">-13.0204495</elem>
+-- <elem key="median">-6.0204495</elem>
+
+author = "Daniel Miller"
+
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+
+categories = {"default", "safe"}
+
+hostrule = function(host)
+ return host.registry.datetime_skew and #host.registry.datetime_skew > 0
+end
+
+postrule = function()
+ return nmap.registry.clock_skews and #nmap.registry.clock_skews > 0
+end
+
+local function format_host (host)
+ local name = stdnse.get_hostname(host)
+ if name == host.ip then
+ return name
+ else
+ return ("%s (%s)"):format(host.ip, name)
+ end
+end
+
+local function record_stats(host, mean, stddev, median)
+ local reg = nmap.registry.clock_skews or {}
+ reg[#reg+1] = {
+ ip = format_host(host),
+ mean = mean,
+ stddev = stddev,
+ median = median,
+ -- Allowable variance to regard this a match.
+ variance = host.times.rttvar * 2
+ }
+ nmap.registry.clock_skews = reg
+end
+
+hostaction = function(host)
+ local skews = host.registry.datetime_skew
+ if not skews or #skews < 1 then
+ return nil
+ end
+ local mean, stddev = formulas.mean_stddev(skews)
+ local median = formulas.median(skews)
+ -- truncate to integers; we don't care about fractional seconds)
+ mean = math.modf(mean)
+ stddev = math.modf(stddev)
+ median = math.modf(median)
+ record_stats(host, mean, stddev, median)
+ if mean ~= 0 or stddev ~= 0 or nmap.verbosity() > 1 then
+ local out = {count = #skews, mean = mean, stddev = stddev, median = median}
+ return out, (#skews == 1 and datetime.format_time(mean)
+ or ("mean: %s, deviation: %s, median: %s"):format(
+ datetime.format_time(mean),
+ datetime.format_time(stddev),
+ datetime.format_time(median)
+ )
+ )
+ end
+end
+
+local function sorted_keys(t)
+ local ret = {}
+ for k, _ in pairs(t) do
+ ret[#ret+1] = k
+ end
+ table.sort(ret)
+ return ret
+end
+
+postaction = function()
+ local skews = nmap.registry.clock_skews
+
+ local host_count = #skews
+ local groups = {}
+ for i=1, host_count do
+ local current = skews[i]
+ -- skip if we already grouped this one
+ if not current.grouped then
+ current.grouped = true
+ local group = {current.ip}
+ groups[current.mean] = group
+ for j=i+1, #skews do
+ local check = skews[j]
+ if not check.grouped then
+ -- Consider it a match if it's within a the average variance of the 2 targets.
+ -- Use the median to rule out influence of outliers, since these ought to be discrete.
+ if math.abs(check.median - current.median) < (check.variance + current.variance) / 2 then
+ check.grouped = true
+ group[#group+1] = check.ip
+ end
+ end
+ end
+ end
+ end
+
+ local out = {}
+ for mean, group in pairs(groups) do
+ -- Collapse the biggest group
+ if #groups > 1 and #group > host_count // 2 then
+ out[datetime.format_time(mean)] = "Majority of systems scanned"
+ elseif #group > 1 then
+ -- Only record groups of more than one system together
+ out[datetime.format_time(mean)] = group
+ end
+ end
+
+ if next(out) then
+ return outlib.sorted_by_key(out)
+ end
+end
+
+local ActionsTable = {
+ -- hostrule: Get the average clock skew and put it in the registry
+ hostrule = hostaction,
+ -- postrule: compare clock skews and report similar ones
+ postrule = postaction
+}
+
+-- execute the action function corresponding to the current rule
+action = function(...) return ActionsTable[SCRIPT_TYPE](...) end