diff --git a/CHANGELOG b/CHANGELOG index 07cb5676b..f402cdcb1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added ipv6-node-info, which gets hostnames and IP addresses + with IPv6 Node Information Queries. [David] + o [NSE] Added the script irc-brute that performs password guessing against password protected IRC servers. [Patrik] diff --git a/scripts/ipv6-node-info.nse b/scripts/ipv6-node-info.nse new file mode 100644 index 000000000..f1a8dcadc --- /dev/null +++ b/scripts/ipv6-node-info.nse @@ -0,0 +1,327 @@ +description = [[ +Gets 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: (actually hostnames) mac-mini.local + +categories = {"default", "discovery", "safe"} + +author = "David Fifield" + +require("bin") +require("bit") +require("dns") +require("openssl") +require("packet") + +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() + p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODEADDRESSES) + dnet:ip_send(p) + p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODENAME) + dnet:ip_send(p) + p = build_ni_query(host.bin_ip_src, host.bin_ip, QTYPE_NODEIPV4ADDRESSES) + dnet:ip_send(p) + 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 + +-- RFC 4620, section 6.3. +local function stringify_nodename(flags, data) + local status, names + local text + + status, names = try_decode_nodenames(data) + if empty(names) then + return + end + text = stdnse.strjoin(", ", names) + if not status then + text = text .. " (parsing error)" + end + + return text +end + +-- RFC 4620, section 6.3. +local function stringify_nodeaddresses(flags, data) + local ttl, binaddr + local text + 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] = packet.toipv6(binaddr) + end + if empty(addrs) then + return + end + + text = stdnse.strjoin(", ", addrs) + if bit.band(flags, 0x01) ~= 0 then + text = text .. " (more omitted for space reasons)" + end + + return text +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 text + local addrs = {} + local pos = nil + + -- Check for DNS names. + status, names = try_decode_nodenames(data .. "\0\0") + if status then + return "(actually hostnames) " .. stdnse.strjoin(", ", 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] = packet.toip(binaddr) + end + if empty(addrs) then + return + end + + text = stdnse.strjoin(", ", addrs) + if bit.band(flags, 0x01) ~= 0 then + text = text .. " (more omitted for space reasons)" + end + + return text +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.print_debug(1, "Got NI reply with unknown qtype %d from %s", qtype, packet.toipv6(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) + local QTYPE_ORDER = { + QTYPE_NOOP, + QTYPE_NODENAME, + QTYPE_NODEADDRESSES, + QTYPE_NODEIPV4ADDRESSES, + } + local output + + output = {} + for _, qtype in ipairs(QTYPE_ORDER) do + if results[qtype] then + output[#output + 1] = QTYPE_STRINGS[qtype] .. ": " .. results[qtype] + end + end + + return stdnse.format_output(true, 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 diff --git a/scripts/script.db b/scripts/script.db index 18a12ce14..53d0c550d 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -120,6 +120,7 @@ Entry { filename = "ip-geolocation-geoplugin.nse", categories = { "discovery", " Entry { filename = "ip-geolocation-ipinfodb.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "ip-geolocation-maxmind.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "ipidseq.nse", categories = { "discovery", "safe", } } +Entry { filename = "ipv6-node-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "irc-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "irc-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "irc-unrealircd-backdoor.nse", categories = { "exploit", "intrusive", "malware", "vuln", } }