1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 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
|