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
|
local coroutine = require "coroutine"
local ipOps = require "ipOps"
local nmap = require "nmap"
local packet = require "packet"
local stdnse = require "stdnse"
local string = require "string"
local tab = require "tab"
local table = require "table"
local target = require "target"
local rand = require "rand"
description = [[
Performs IPv6 host discovery by triggering stateless address auto-configuration
(SLAAC).
This script works by sending an ICMPv6 Router Advertisement with a random
address prefix, which causes hosts to begin SLAAC and send a solicitation for
their newly configured address, as part of duplicate address detection. The
script then guesses the remote addresses by combining the link-local prefix of
the interface with the interface identifier in each of the received
solicitations. This should be followed up with ordinary ND host discovery to
verify that the guessed addresses are correct.
The router advertisement has a router lifetime of zero and a short prefix
lifetime (a few seconds)
See also:
* RFC 4862, IPv6 Stateless Address Autoconfiguration, especially section 5.5.3.
* https://github.com/rapid7/metasploit-framework/blob/master/modules/auxiliary/scanner/discovery/ipv6_neighbor_router_advertisement.rb
]]
---
-- @usage
-- nmap -6 --script targets-ipv6-multicast-slaac --script-args 'newtargets,interface=eth0' -sP
-- @output
-- Pre-scan script results:
-- | targets-ipv6-multicast-slaac:
-- | IP: fe80:0000:0000:0000:1322:33ff:fe44:5566 MAC: 11:22:33:44:55:66 IFACE: eth0
-- |_ Use --script-args=newtargets to add the results as targets
-- @args targets-ipv6-multicast-slaac.interface The interface to use for host discovery.
author = {"David Fifield", "Xu Weilin"}
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery","broadcast"}
prerule = function()
return nmap.is_privileged()
end
local function get_identifier(ip6_addr)
return string.sub(ip6_addr, 9, 16)
end
--- Get a Unique-local Address with random global ID.
-- @param local_scope The scope of the address, local or reserved.
-- @return A 16-byte string of IPv6 address, and the length of the prefix.
local function get_random_ula_prefix(local_scope)
local ula_prefix
local global_id = rand.random_string(5)
if local_scope then
ula_prefix = ipOps.ip_to_str("fd00::")
else
ula_prefix = ipOps.ip_to_str("fc00::")
end
ula_prefix = string.sub(ula_prefix,1,1) .. global_id .. string.sub(ula_prefix,7,-1)
return ula_prefix,64
end
--- Build an ICMPv6 payload of Router Advertisement.
-- @param mac_src six-byte string of the source MAC address.
-- @param prefix 16-byte string of IPv6 address.
-- @param prefix_len integer that represents the length of the prefix.
-- @param valid_time integer that represents the valid time of the prefix.
-- @param preferred_time integer that represents the preferred time of the prefix.
local function build_router_advert(mac_src,prefix,prefix_len,valid_time,preferred_time)
local ra_msg = string.char(0x0, --cur hop limit
0x08, --flags
0x00,0x00, --router lifetime
0x00,0x00,0x00,0x00, --reachable time
0x00,0x00,0x00,0x00) --retrans timer
local prefix_option_msg = string.pack(">BB I4 I4 I4",
prefix_len,
0xc0, --flags: Onlink, Auto
valid_time, -- valid lifetime
preferred_time, -- preferred lifetime
0 -- unknown
) .. prefix
local icmpv6_prefix_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_PREFIX_INFORMATION,prefix_option_msg)
local icmpv6_src_link_option = packet.Packet:set_icmpv6_option(packet.ND_OPT_SOURCE_LINKADDR,mac_src)
local icmpv6_payload = ra_msg .. icmpv6_prefix_option .. icmpv6_src_link_option
return icmpv6_payload
end
local function get_interfaces()
local interface_name = stdnse.get_script_args(SCRIPT_NAME .. ".interface")
or nmap.get_interface()
-- interfaces list (decide which interfaces to broadcast on)
local interfaces = {}
if interface_name then
-- single interface defined
local if_table = nmap.get_interface_info(interface_name)
if if_table and ipOps.ip_to_str(if_table.address) and if_table.link == "ethernet" then
interfaces[#interfaces + 1] = if_table
else
stdnse.debug1("Interface not supported or not properly configured.")
end
else
for _, if_table in ipairs(nmap.list_interfaces()) do
if ipOps.ip_to_str(if_table.address) and if_table.link == "ethernet" then
table.insert(interfaces, if_table)
end
end
end
return interfaces
end
local function single_interface_broadcast(if_nfo, results)
stdnse.debug1("Starting " .. SCRIPT_NAME .. " on " .. if_nfo.device)
local condvar = nmap.condvar(results)
local src_mac = if_nfo.mac
local src_ip6 = ipOps.ip_to_str(if_nfo.address)
local dst_mac = packet.mactobin("33:33:00:00:00:01")
local dst_ip6 = ipOps.ip_to_str("ff02::1")
----------------------------------------------------------------------------
--SLAAC-based host discovery probe
local dnet = nmap.new_dnet()
local pcap = nmap.new_socket()
local function catch ()
dnet:ethernet_close()
pcap:pcap_close()
end
local try = nmap.new_try(catch)
try(dnet:ethernet_open(if_nfo.device))
pcap:pcap_open(if_nfo.device, 128, true, "src ::0/128 and dst net ff02::1:0:0/96 and icmp6 and ip6[6:1] = 58 and ip6[40:1] = 135")
local actual_prefix = string.sub(src_ip6,1,8)
local ula_prefix, prefix_len = get_random_ula_prefix()
-- preferred_lifetime <= valid_lifetime.
-- Nmap will get the whole IPv6 addresses of each host if the two parameters are both longer than 5 seconds.
-- Sometimes it makes sense to regard the several addresses of a host as
-- different hosts, as the host's administrator may apply different firewall
-- configurations on them.
local valid_lifetime = 6
local preferred_lifetime = 6
local probe = packet.Frame:new()
probe.ip_bin_src = packet.mac_to_lladdr(src_mac)
probe.ip_bin_dst = dst_ip6
probe.mac_src = src_mac
probe.mac_dst = packet.mactobin("33:33:00:00:00:01")
local icmpv6_payload = build_router_advert(src_mac,ula_prefix,prefix_len,valid_lifetime,preferred_lifetime)
probe:build_icmpv6_header(packet.ND_ROUTER_ADVERT, 0, icmpv6_payload)
probe:build_ipv6_packet()
probe:build_ether_frame()
try(dnet:ethernet_send(probe.frame_buf))
local expected_mac_dst_prefix = packet.mactobin("33:33:ff:00:00:00")
local expected_ip6_src = ipOps.ip_to_str("::")
local expected_ip6_dst_prefix = ipOps.ip_to_str("ff02::1:0:0")
pcap:set_timeout(1000)
local pcap_timeout_count = 0
local nse_timeout = 5
local start_time = nmap:clock()
local cur_time = nmap:clock()
repeat
local status, length, layer2, layer3 = pcap:pcap_receive()
cur_time = nmap:clock()
if not status then
pcap_timeout_count = pcap_timeout_count + 1
else
local l2reply = packet.Frame:new(layer2)
if string.sub(l2reply.mac_dst, 1, 3) == string.sub(expected_mac_dst_prefix, 1, 3) then
local reply = packet.Packet:new(layer3)
if reply.ip_bin_src == expected_ip6_src and
string.sub(expected_ip6_dst_prefix,1,12) == string.sub(reply.ip_bin_dst,1,12) then
local ula_target_addr_str = ipOps.str_to_ip(reply.ns_target)
local identifier = get_identifier(reply.ns_target)
--Filter out the reduplicative identifiers.
--A host will send several NS packets with the same interface
--identifier if it receives several RA packets with different prefix
--during the discovery phase.
local actual_addr_str = ipOps.str_to_ip(actual_prefix .. identifier)
if not results[actual_addr_str] then
if target.ALLOW_NEW_TARGETS then
target.add(actual_addr_str)
end
results[#results + 1] = { address = actual_addr_str, mac = stdnse.format_mac(l2reply.mac_src), iface = if_nfo.device }
results[actual_addr_str] = true
end
end
end
end
until pcap_timeout_count >= 2 or cur_time - start_time >= nse_timeout
dnet:ethernet_close()
pcap:pcap_close()
condvar("signal")
end
local function format_output(results)
local output = tab.new()
for _, record in ipairs(results) do
tab.addrow(output, "IP: " .. record.address, "MAC: " .. record.mac, "IFACE: " .. record.iface)
end
if #results > 0 then
output = { tab.dump(output) }
if not target.ALLOW_NEW_TARGETS then
output[#output + 1] = "Use --script-args=newtargets to add the results as targets"
end
return stdnse.format_output(true, output)
end
end
action = function()
local threads = {}
local results = {}
local condvar = nmap.condvar(results)
for _, if_nfo in ipairs(get_interfaces()) do
-- create a thread for each interface
if ipOps.ip_in_range(if_nfo.address, "fe80::/10") then
local co = stdnse.new_thread(single_interface_broadcast, if_nfo, results)
threads[co] = true
end
end
repeat
for thread in pairs(threads) do
if coroutine.status(thread) == "dead" then threads[thread] = nil end
end
if ( next(threads) ) then
condvar "wait"
end
until next(threads) == nil
return format_output(results)
end
|