summaryrefslogtreecommitdiffstats
path: root/scripts/dns-cache-snoop.nse
blob: 14fcb58ed9868ede303fdde430593b1e24c7389e (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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
local dns = require "dns"
local formulas = require "formulas"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"

description = [[
Performs DNS cache snooping against a DNS server.

There are two modes of operation, controlled by the
<code>dns-cache-snoop.mode</code> script argument. In
<code>nonrecursive</code> mode (the default), queries are sent to the
server with the RD (recursion desired) flag set to 0. The server should
respond positively to these only if it has the domain cached. In
<code>timed</code> mode, the mean and standard deviation response times
for a cached domain are calculated by sampling the resolution of a name
(www.google.com) several times. Then, each domain is resolved and the
time taken compared to the mean. If it is less than one standard
deviation over the mean, it is considered cached. The <code>timed</code>
mode inserts entries in the cache and can only be used reliably once.

The default list of domains to check consists of the top 50 most popular
sites, each site being listed twice, once with "www." and once without.
Use the <code>dns-cache-snoop.domains</code> script argument to use a
different list.
]]

---
-- @args dns-cache-snoop.mode which of two supported snooping methods to
-- use. <code>nonrecursive</code>, the default, checks if the server
-- returns results for non-recursive queries. Some servers may disable
-- this. <code>timed</code> measures the difference in time taken to
-- resolve cached and non-cached hosts. This mode will pollute the DNS
-- cache and can only be used once reliably.
-- @args dns-cache-snoop.domains an array of domain to check in place of
-- the default list.
--
-- @usage
-- nmap -sU -p 53 --script dns-cache-snoop.nse --script-args 'dns-cache-snoop.mode=timed,dns-cache-snoop.domains={host1,host2,host3}' <target>
--
-- @output
-- PORT   STATE SERVICE REASON
-- 53/udp open  domain  udp-response
-- | dns-cache-snoop: 10 of 100 tested domains are cached.
-- | www.google.com
-- | facebook.com
-- | www.facebook.com
-- | www.youtube.com
-- | yahoo.com
-- | twitter.com
-- | www.twitter.com
-- | www.google.com.hk
-- | www.google.co.uk
-- |_www.linkedin.com


author = "Eugene V. Alexeev"

license = "Same as Nmap--See https://nmap.org/book/man-legal.html"

categories = {"intrusive", "discovery"}

portrule = shortport.port_or_service(53, "domain", "udp")

local DOMAINS = {}
local MODE = "nonrecursive"
-- This domain is used as a default cached entry in timed mode.
local TIMED_DUMMY_DOMAIN = "www.google.com"
-- How many samples to collect for the time taken to resolve the dummy domain.
local TIMED_NUM_SAMPLES = 25
-- In timed mode, times below mean + TIMED_MULTIPLIER * stddev are
-- accepted as cached. Using one standard deviation gives us a roughly
-- 84% chance that a domain with the same response time as the reference
-- domain will be detected as cached.
local TIMED_MULTIPLIER = 1.0

-- This list is the first 50 entries of
-- http://s3.amazonaws.com/alexa-static/top-1m.csv.zip on 2013-08-08.
local ALEXA_DOMAINS = {
  "google.com",
  "facebook.com",
  "youtube.com",
  "yahoo.com",
  "baidu.com",
  "wikipedia.org",
  "amazon.com",
  "qq.com",
  "live.com",
  "linkedin.com",
  "twitter.com",
  "blogspot.com",
  "taobao.com",
  "google.co.in",
  "bing.com",
  "yahoo.co.jp",
  "yandex.ru",
  "wordpress.com",
  "sina.com.cn",
  "vk.com",
  "ebay.com",
  "google.de",
  "tumblr.com",
  "msn.com",
  "google.co.uk",
  "googleusercontent.com",
  "ask.com",
  "mail.ru",
  "google.com.br",
  "163.com",
  "google.fr",
  "pinterest.com",
  "google.com.hk",
  "hao123.com",
  "microsoft.com",
  "google.co.jp",
  "xvideos.com",
  "google.ru",
  "weibo.com",
  "craigslist.org",
  "paypal.com",
  "instagram.com",
  "amazon.co.jp",
  "google.it",
  "imdb.com",
  "blogger.com",
  "google.es",
  "apple.com",
  "conduit.com",
  "sohu.com",
}

-- Construct the default list of domains.
for _, domain in ipairs(ALEXA_DOMAINS) do
  DOMAINS[#DOMAINS + 1] = domain
  if not string.match(domain, "^www%.") then
    DOMAINS[#DOMAINS + 1] = "www." .. domain
  end
end

local function nonrecursive_mode(host, port, domains)
  local cached = {}

  for _,domain in ipairs(domains) do
    if dns.query(domain, {host = host.ip, port = port.number, tries = 0, norecurse=true}) then
      cached[#cached + 1] = domain
    end
  end

  return cached
end

-- Return the time taken (in seconds) to resolve the given domain, or nil if
-- it could not be resolved.
local function timed_query(host, port, domain)
  local start, stop

  start = nmap.clock_ms()
  if dns.query(domain, {host = host.ip, port = port.number, tries = 0, norecurse = false}) then
    stop = nmap.clock_ms()
    return (stop - start) / 1000
  else
    return nil
  end
end

local function timed_mode(host, port, domains)
  local cached = {}
  local i, t

  -- Insert in the cache.
  timed_query(host, port, TIMED_DUMMY_DOMAIN)

  -- Measure how long it takes to resolve on average.
  local times = {}
  for i = 1, TIMED_NUM_SAMPLES do
    t = timed_query(host, port, TIMED_DUMMY_DOMAIN)
    if t then
      times[#times + 1] = t
    end
  end
  local mean, stddev = formulas.mean_stddev(times)
  local cutoff = mean + stddev * TIMED_MULTIPLIER
  stdnse.debug1("reference %s: mean %g  stddev %g  cutoff %g", TIMED_DUMMY_DOMAIN, mean, stddev, cutoff)

  -- Now try all domains one by one.
  for _, domain in ipairs(domains) do
    t = timed_query(host, port, domain)
    if t then
      if t < cutoff then
        stdnse.debug1("%s: %g is cached (cutoff %g)", domain, t, cutoff)
        cached[#cached + 1] = domain
      else
        stdnse.debug1("%s: %g not cached (cutoff %g)", domain, t, cutoff)
      end
    end
  end

  return cached
end

action = function(host, port)
  local domains = DOMAINS
  local mode = MODE

  local args = nmap.registry.args
  if args then
    if args["dns-cache-snoop.mode"] then
      mode = args["dns-cache-snoop.mode"]
    end
    if args["dns-cache-snoop.domains"] then
      domains = args["dns-cache-snoop.domains"]
    end
  end

  local cached

  mode = string.lower(mode)
  if mode == "nonrecursive" then
    cached = nonrecursive_mode(host, port, domains)
  elseif mode == "timed" then
    cached = timed_mode(host, port, domains)
  else
    return string.format("Error: \"%s\" is not a known mode. Use \"nonrecursive\" or \"timed\".")
  end

  if #cached > 0 then
    nmap.set_port_state(host, port, "open")
  end

  return string.format("%d of %d tested domains are cached.\n", #cached, #domains) ..  table.concat(cached, "\n")
end