summaryrefslogtreecommitdiffstats
path: root/scripts/ipv6-node-info.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/ipv6-node-info.nse')
-rw-r--r--scripts/ipv6-node-info.nse337
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