description = [[ SLAAC-based host discovery. Do a very fast host discovery on link-local IPv6 network. ]] --- -- @usage -- ./nmap -6 --script=slaac_host_discovery.nse --script-args 'newtargets,interface=eth0,ipv6="2001:da8:215:3320:223:aeff:fe5d:3b10"' -sP author = "David and Weilin" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"discovery"} require 'nmap' require 'target' require 'packet' require "os" require "math" prerule = function() return nmap.is_privileged() and (stdnse.get_script_args("targets-ipv6-multicast.interface") or nmap.get_interface()) end catch = function() dnet:ethernet_close() pcap:pcap_close() end try = nmap.new_try(catch) local function get_ipv6_interface_info_by_name(if_name) local ifaces = nmap.list_interfaces() local iface local if_nfo for _,iface in pairs(ifaces) do if if_name == iface.device and iface.address and (#iface.address>15 or string.find(iface.address, "::")) then if_nfo = iface return if_nfo end end return nil 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_radom_ula_prefix(local_scope) local ula_prefix math.randomseed(os.time()) local global_id = string.char(math.random(256)-1,math.random(256)-1,math.random(256)-1,math.random(256)-1,math.random(256)-1) if local_scope then ula_prefix = packet.ip6tobin("fd00::") else ula_prefix = packet.ip6tobin("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.char(prefix_len, 0xc0) .. --flags: Onlink, Auto packet.set_u32("....",0,valid_time) .. packet.set_u32("....",0,preferred_time) .. string.char(0,0,0,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 action = function() local if_name = stdnse.get_script_args("targets-ipv6-multicast.interface") or nmap.get_interface() local if_nfo = get_ipv6_interface_info_by_name(if_name) if not if_nfo then stdnse.print_debug("Invalid interface: " .. if_name) return false end local src_mac = if_nfo.mac local src_ip6 = packet.ip6tobin(stdnse.get_script_args("ipv6.src")) or packet.ip6tobin(if_nfo.address) local dst_mac = packet.mactobin("33:33:00:00:00:01") local dst_ip6 = packet.ip6tobin("ff02::1") local id_set = {} ---------------------------------------------------------------------------- --SLAAC-based host discovery probe local dnet = nmap.new_dnet() local pcap = nmap.new_socket() try(dnet:ethernet_open(if_name)) pcap:pcap_open(if_name, 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_radom_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.ip6_src = packet.mac_to_lladdr(src_mac) probe.ip6_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 = packet.ip6tobin("::") local expected_ip6_dst_prefix = packet.ip6tobin("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() local found_targets = 0 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 reply = packet.Frame:new(layer2) if string.sub(reply.mac_dst, 1, 3) == string.sub(expected_mac_dst_prefix, 1, 3) then reply = packet.Packet:new(layer3) if reply.ip6_src == expected_ip6_src and string.sub(expected_ip6_dst_prefix,1,12) == string.sub(reply.ip6_dst,1,12) then local ula_target_addr_str = packet.toipv6(reply.ns_target) local identifier = get_identifier(reply.ns_target) found_targets = found_targets + 1 --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. if not id_set[identifier] then id_set[identifier] = true local actual_addr_str = packet.toipv6(actual_prefix .. identifier) target.add(actual_addr_str) end end end end until pcap_timeout_count >= 2 or cur_time - start_time >= nse_timeout stdnse.print_debug(0, "[SLAAC] Found %d targets.", found_targets) dnet:ethernet_close() pcap:pcap_close() return true end