summaryrefslogtreecommitdiffstats
path: root/scripts/ipv6-node-info.nse
blob: 56f280aa06269b1689db6e59a892a2f8e0b1ec23 (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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
local dns = require "dns"
local ipOps = require "ipOps"
local nmap = require "nmap"
local outlib = require "outlib"
local packet = require "packet"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local rand = require "rand"

description = [[
Obtains hostnames, IPv4 and IPv6 addresses through IPv6 Node Information Queries.

IPv6 Node Information Queries are defined in RFC 4620. There are three
useful types of queries:
* qtype=2: Node Name
* qtype=3: Node Addresses
* qtype=4: IPv4 Addresses

Some operating systems (Mac OS X and OpenBSD) return hostnames in
response to qtype=4, IPv4 Addresses. In this case, the hostnames are still
shown in the "IPv4 addresses" output row, but are prefixed by "(actually
hostnames)".
]]

---
-- @usage nmap -6 <target>
--
-- @output
-- | ipv6-node-info:
-- |   Hostnames: mac-mini.local
-- |   IPv6 addresses: fe80::a8bb:ccff:fedd:eeff, 2001:db8:1234:1234::3
-- |_  IPv4 addresses: mac-mini.local
--
-- @xmloutput
-- <elem key="Hostnames">mac-mini.local</elem>
-- <table key="IPv6 addresses">
--   <elem>fe80::a8bb:ccff:fedd:eeff</elem>
--   <elem>2001:db8:1234:1234::3</elem>
-- </table>
-- <table key="IPv4 addresses">
--   <elem>mac-mini.local</elem>
-- </table>

categories = {"default", "discovery", "safe"}

author = "David Fifield"


local ICMPv6_NODEINFOQUERY = 139
local   ICMPv6_NODEINFOQUERY_IPv6ADDR = 0
local   ICMPv6_NODEINFOQUERY_NAME = 1
local   ICMPv6_NODEINFOQUERY_IPv4ADDR = 1
local ICMPv6_NODEINFORESP = 140
local   ICMPv6_NODEINFORESP_SUCCESS = 0
local   ICMPv6_NODEINFORESP_REFUSED = 1
local   ICMPv6_NODEINFORESP_UNKNOWN = 2

local QTYPE_NOOP = 0
local QTYPE_NODENAME = 2
local QTYPE_NODEADDRESSES = 3
local QTYPE_NODEIPV4ADDRESSES = 4

local QTYPE_STRINGS = {
  [QTYPE_NOOP] = "NOOP",
  [QTYPE_NODENAME] = "Hostnames",
  [QTYPE_NODEADDRESSES] = "IPv6 addresses",
  [QTYPE_NODEIPV4ADDRESSES] = "IPv4 addresses",
}

local function build_ni_query(src, dst, qtype)
  local flags
  local nonce = rand.random_string(8)
  if qtype == QTYPE_NODENAME then
    flags = 0x0000
  elseif qtype == QTYPE_NODEADDRESSES then
    -- Set all the flags GSLCA (see RFC 4620, Figure 3).
    flags = 0x003E
  elseif qtype == QTYPE_NODEIPV4ADDRESSES then
    -- Set the A flag (see RFC 4620, Figure 4).
    flags = 0x0002
  else
    error("Unknown qtype " .. qtype)
  end
  local payload = string.pack(">I2 I2", qtype, flags) .. nonce .. dst
  local p = packet.Packet:new()
  p:build_icmpv6_header(ICMPv6_NODEINFOQUERY, ICMPv6_NODEINFOQUERY_IPv6ADDR, payload, src, dst)
  p:build_ipv6_packet(src, dst, packet.IPPROTO_ICMPV6)

  return p.buf
end

function hostrule(host)
  return nmap.is_privileged() and #host.bin_ip == 16 and host.interface
end

local function open_sniffer(host)
  local bpf
  local s

  s = nmap.new_socket()
  bpf = string.format("ip6 and src host %s", host.ip)
  s:pcap_open(host.interface, 1500, false, bpf)

  return s
end

local function send_queries(host)
  local dnet

  dnet = nmap.new_dnet()
  dnet:ip_open()
  local p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODEADDRESSES)
  dnet:ip_send(p, host)
  p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODENAME)
  dnet:ip_send(p, host)
  p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODEIPV4ADDRESSES)
  dnet:ip_send(p, host)
  dnet:ip_close()
end

local function empty(t)
  return not next(t)
end

-- Try to decode a Node Name reply data field. If successful, returns true and
-- a list of DNS names. In case of a parsing error, returns false and the
-- partial list of names that were parsed prior to the error.
local function try_decode_nodenames(data)
  local names = {}

  local ttl, pos = string.unpack(">I4", data)
  if not ttl then
    return false, names
  end
  while pos <= #data do
    local name

    pos, name = dns.decStr(data, pos)
    if not name then
      return false, names
    end
    -- Ignore empty names, such as those at the end.
    if name ~= "" then
      names[#names + 1] = name
    end
  end

  return true, names
end

local function stringify_noop(flags, data)
  return "replied"
end

-- RFC 4620, section 6.3.
local function stringify_nodename(flags, data)
  local status, names

  status, names = try_decode_nodenames(data)
  if empty(names) then
    return
  end
  if not status then
    names[#names+1] = "(parsing error)"
  end

  outlib.list_sep(names)
  return names
end

-- RFC 4620, section 6.3.
local function stringify_nodeaddresses(flags, data)
  local ttl, binaddr
  local addrs = {}
  local pos = nil

  while true do
    ttl, binaddr, pos = string.unpack(">I4 c16", data, pos)
    if not ttl then
      break
    end
    addrs[#addrs + 1] = ipOps.str_to_ip(binaddr)
  end
  if empty(addrs) then
    return
  end

  if (flags & 0x01) ~= 0 then
    addrs[#addrs+1] = "(more omitted for space reasons)"
  end

  outlib.list_sep(addrs)
  return addrs
end

-- RFC 4620, section 6.4.
-- But Mac OS X puts DNS names in here instead of IPv4 addresses, but it
-- doesn't include the two empty labels at the end as it does with a Node Name
-- response. For example, here is a Node Name reply:
-- 00 00 00 00 0e 6d 61 63  2d 6d 69 6e 69 2e 6c 6f    .....mac -mini.lo
-- 63 61 6c 00 00                                      cal..
-- And here is a Node Addresses reply:
-- 00 00 00 00 0e 6d 61 63  2d 6d 69 6e 69 2e 6c 6f    .....mac -mini.lo
-- 63 61 6c                                            cal
local function stringify_nodeipv4addresses(flags, data)
  local status, names
  local ttl, binaddr
  local addrs = {}
  local pos = nil

  -- Check for DNS names.
  status, names = try_decode_nodenames(data .. "\0\0")
  if status then
    outlib.list_sep(names)
    return names
  end

  -- Okay, looks like it's really IP addresses.
  while true do
    ttl, binaddr, pos = string.unpack(">I4 c4", data, pos)
    if not ttl then
      break
    end
    addrs[#addrs + 1] = ipOps.str_to_ip(binaddr)
  end
  if empty(addrs) then
    return
  end

  if (flags & 0x01) ~= 0 then
    addrs[#addrs+1] = "(more omitted for space reasons)"
  end

  outlib.list_sep(addrs)
  return addrs
end

local STRINGIFY = {
  [QTYPE_NOOP] = stringify_noop,
  [QTYPE_NODENAME] = stringify_nodename,
  [QTYPE_NODEADDRESSES] = stringify_nodeaddresses,
  [QTYPE_NODEIPV4ADDRESSES] = stringify_nodeipv4addresses,
}

local function handle_received_packet(buf)
  local text

  local p = packet.Packet:new(buf)
  if p.icmpv6_type ~= ICMPv6_NODEINFORESP then
    return
  end
  local qtype, flags, pos = string.unpack(">I2I2", p.buf, p.icmpv6_offset + 4)
  local data = string.sub(p.buf, pos + 8)

  if not STRINGIFY[qtype] then
    -- This is a not a qtype we sent or know about.
    stdnse.debug1("Got NI reply with unknown qtype %d from %s", qtype, p.ip6_src)
    return
  end

  if p.icmpv6_code == ICMPv6_NODEINFORESP_SUCCESS then
    text = STRINGIFY[qtype](flags, data)
  elseif p.icmpv6_code == ICMPv6_NODEINFORESP_REFUSED then
    text = "refused"
  elseif p.icmpv6_code == ICMPv6_NODEINFORESP_UNKNOWN then
    text = string.format("target said: qtype %d is unknown", qtype)
  else
    text = string.format("unknown ICMPv6 code %d for qtype %d", p.icmpv6_code, qtype)
  end

  return qtype, text
end

local function format_results(results)
  if empty(results) then
    return nil
  end
  local QTYPE_ORDER = {
    QTYPE_NOOP,
    QTYPE_NODENAME,
    QTYPE_NODEADDRESSES,
    QTYPE_NODEIPV4ADDRESSES,
  }
  local output

  output = stdnse.output_table()
  for _, qtype in ipairs(QTYPE_ORDER) do
    if results[qtype] then
      output[QTYPE_STRINGS[qtype]] = results[qtype]
    end
  end

  return output
end

function action(host)
  local s
  local timeout, end_time, now
  local pending, results

  timeout = host.times.timeout * 10

  s = open_sniffer(host)

  send_queries(host)

  pending = {
    [QTYPE_NODENAME] = true,
    [QTYPE_NODEADDRESSES] = true,
    [QTYPE_NODEIPV4ADDRESSES] = true,
  }
  results = {}

  now = nmap.clock_ms()
  end_time = now + timeout
  repeat
    local _, status, buf

    s:set_timeout((end_time - now) * 1000)

    status, _, _, buf = s:pcap_receive()
    if status then
      local qtype, text = handle_received_packet(buf)
      if qtype then
        results[qtype] = text
        pending[qtype] = nil
      end
    end

    now = nmap.clock_ms()
  until empty(pending) or now > end_time

  s:pcap_close()

  return format_results(results)
end