diff options
Diffstat (limited to '')
-rw-r--r-- | scripts/ipv6-node-info.nse | 337 |
1 files changed, 337 insertions, 0 deletions
diff --git a/scripts/ipv6-node-info.nse b/scripts/ipv6-node-info.nse new file mode 100644 index 0000000..56f280a --- /dev/null +++ b/scripts/ipv6-node-info.nse @@ -0,0 +1,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 |