local bin = require "bin" local dns = require "dns" local ipOps = require "ipOps" local nmap = require "nmap" local packet = require "packet" local stdnse = require "stdnse" local string = require "string" local openssl = stdnse.silent_require "openssl" 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 -- -- @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 -- mac-mini.local -- -- fe80::a8bb:ccff:fedd:eeff -- 2001:db8:1234:1234::3 --
-- -- mac-mini.local --
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 payload, p, flags local nonce nonce = openssl.rand_pseudo_bytes(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 payload = bin.pack(">SSAA", qtype, flags, nonce, dst) 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 ttl local names = {} local pos = nil pos, ttl = bin.unpack(">I", data, pos) 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 local commasep = { __tostring = function (t) return stdnse.strjoin(", ", t) 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 setmetatable(names, commasep) 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 pos, ttl, binaddr = bin.unpack(">IA16", 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 setmetatable(addrs, commasep) 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 setmetatable(names, commasep) return names end -- Okay, looks like it's really IP addresses. while true do pos, ttl, binaddr = bin.unpack(">IA4", 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 setmetatable(addrs, commasep) 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 p, qtype, flags, data local text p = packet.Packet:new(buf) if p.icmpv6_type ~= ICMPv6_NODEINFORESP then return end qtype = packet.u16(p.buf, p.icmpv6_offset + 4) flags = packet.u16(p.buf, p.icmpv6_offset + 6) data = string.sub(p.buf, p.icmpv6_offset + 16 + 1) 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