summaryrefslogtreecommitdiffstats
path: root/scripts/clock-skew.nse
blob: 14f80d12eec820e6e03cea0d093fff7b69458e74 (plain)
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