diff options
Diffstat (limited to 'scripts/ntp-monlist.nse')
-rw-r--r-- | scripts/ntp-monlist.nse | 1036 |
1 files changed, 1036 insertions, 0 deletions
diff --git a/scripts/ntp-monlist.nse b/scripts/ntp-monlist.nse new file mode 100644 index 0000000..3ebe193 --- /dev/null +++ b/scripts/ntp-monlist.nse @@ -0,0 +1,1036 @@ +local ipOps = require "ipOps" +local math = require "math" +local nmap = require "nmap" +local packet = require "packet" +local shortport = require "shortport" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" + +author = "jah" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"discovery", "intrusive"} +description = [[ +Obtains and prints an NTP server's monitor data. + +Monitor data is a list of the most recently used (MRU) having NTP associations +with the target. Each record contains information about the most recent NTP +packet sent by a host to the target including the source and destination +addresses and the NTP version and mode of the packet. With this information it +is possible to classify associated hosts as Servers, Peers, and Clients. + +A Peers command is also sent to the target and the peers list in the response +allows differentiation between configured Mode 1 Peers and clients which act +like Peers (such as the Windows W32Time service). + +Associated hosts are further classified as either public or private. +Private hosts are those +having IP addresses which are not routable on the public Internet and thus can +help to form a picture about the topology of the private network on which the +target resides. + +Other information revealed by the monlist and peers commands are the host with +which the target clock is synchronized and hosts which send Control Mode (6) +and Private Mode (7) commands to the target and which may be used by admins for +the NTP service. + +It should be noted that the very nature of the NTP monitor data means that the +Mode 7 commands sent by this script are recorded by the target (and will often +appear in these results). Since the monitor data is a MRU list, it is probable +that you can overwrite the record of the Mode 7 command by sending an innocuous +looking Client Mode request. This can be achieved easily using Nmap: +<code>nmap -sU -pU:123 -Pn -n --max-retries=0 <target></code> + +Notes: +* The monitor list in response to the monlist command is limited to 600 associations. +* The monitor capability may not be enabled on the target in which case you may receive an error number 4 (No Data Available). +* There may be a restriction on who can perform Mode 7 commands (e.g. "restrict noquery" in <code>ntp.conf</code>) in which case you may not receive a reply. +* This script does not handle authenticating and targets expecting auth info may respond with error number 3 (Format Error). +]] + +--- +-- @usage +-- nmap -sU -pU:123 -Pn -n --script=ntp-monlist <target> +-- +-- @output +-- PORT STATE SERVICE REASON +-- 123/udp open ntp udp-response +-- | ntp-monlist: +-- | Target is synchronised with 127.127.38.0 (reference clock) +-- | Alternative Target Interfaces: +-- | 10.17.4.20 +-- | Private Servers (0) +-- | Public Servers (0) +-- | Private Peers (0) +-- | Public Peers (0) +-- | Private Clients (2) +-- | 10.20.8.69 169.254.138.63 +-- | Public Clients (597) +-- | 4.79.17.248 68.70.72.194 74.247.37.194 99.190.119.152 +-- | ... +-- | 12.10.160.20 68.80.36.133 75.1.39.42 108.7.58.118 +-- | 68.56.205.98 +-- | 2001:1400:0:0:0:0:0:1 2001:16d8:dd00:38:0:0:0:2 +-- | 2002:db5a:bccd:1:21d:e0ff:feb7:b96f 2002:b6ef:81c4:0:0:1145:59c5:3682 +-- | Other Associations (1) +-- |_ 127.0.0.1 seen 1949869 times. last tx was unicast v2 mode 7 + +-- This script uses the NTP sequence numbers and the 'more' bit found in +-- response packets in order to determine when to stop the reception loop. It +-- would be possible for a malicious target to tie-up this script by sending +-- a continuous stream of UDP datagrams. +-- Therefore MAXIMUM_EVIL has been defined to limit the number of malformed or +-- duplicate packets that will be processed before a target is rejected and +-- MAX_RECORDS simply limits the storage of valid looking NTP data to a sane +-- level. +local MAXIMUM_EVIL = 25 +local MAX_RECORDS = 1200 + +local TIMEOUT = 5000 -- ms + + +--- +-- ntp-monlist will run against the ntp service which only runs on UDP 123 +-- +portrule = shortport.port_or_service(123, 'ntp', {'udp'}) + +--- +-- Send an NTPv2 Mode 7 'monlist' command to the target, receive any responses +-- and parse records from those responses. If the target responds favourably +-- then send a 'peers' command and parse the responses. Finally, categorise the +-- discovered NTP associations (hosts) and output the interpreted results. +-- +action = function(host, port) + + -- Define the request code and implementation numbers of the NTP request to + -- send to the target. + local REQ_MON_GETLIST_1 = 42 + local REQ_PEER_LIST = 0 + local IMPL_XNTPD = 3 + + -- parsed records will be stored in this table. + local records = {['peerlist'] = {}} + + -- send monlist command and fill the records table with the responses. + local inum, rcode = IMPL_XNTPD, REQ_MON_GETLIST_1 + local sock = doquery(nil, host, port, inum, rcode, records) + + -- end here if there are zero records. + if #records == 0 then + if sock then sock:close() end + return nil + end + + -- send peers command and add the responses to records.peerlist. + rcode = REQ_PEER_LIST + sock = doquery(sock, host, port, inum, rcode, records) + if sock then sock:close() end + + -- now we can interpret the collected records. + local interpreted = interpret(records, host.ip) + + -- output. + return summary(interpreted) + +end + + +--- +-- Sends NTPv2 Mode 7 requests to the target, receives any responses and parses +-- records from those responses. +-- +-- @param sock Socket object which must be supplied in a connected state. +-- nil may be supplied instead and a socket will be created. +-- @param host Nmap host table for the target. +-- @param port Nmap port table for the target. +-- @param inum NTP implementation number (i.e. 0, 2 or 3). +-- @param rcode NTP Mode 7 request code (e.g. 42 for 'monlist'). +-- @param records Table in which to store records parsed from responses. +-- @return sock Socket object connected to the target. +-- +function doquery(sock, host, port, inum, rcode, records) + + local target = ('%s%s%d'):format( + host.ip, host.ip:match(':') and '.' or ':', port.number + ) + records.badpkts = records.badpkts or 0 + records.peerlist = records.peerlist or {} + + if #records + #records.peerlist >= MAX_RECORDS then + stdnse.debug1('MAX_RECORDS has been reached for target %s - only processing what we have already!', target) + if sock then sock:close() end + return nil + end + + -- connect a new socket if one wasn't supplied + if not sock then + sock = nmap.new_socket() + sock:set_timeout(TIMEOUT) + local constatus, conerr = sock:connect(host, port) + if not constatus then + stdnse.debug1('Error establishing a UDP connection for %s - %s', target, conerr) + return nil + end + end + + -- send + stdnse.debug2('Sending NTPv2 Mode 7 Request %d Implementation %d to %s.', rcode, inum, target) + local ntpData = getPrivateMode(inum, rcode) + local sendstatus, senderr = sock:send(ntpData) + if not sendstatus then + stdnse.debug1('Error sending NTP request to %s:%d - %s', host.ip, port.number, senderr) + sock:close() + return nil + end + + local track = { + ['evil_pkts'] = records.badpkts, -- a count of bad packets + ['hseq'] = -1, -- highest received seq number + ['mseq'] = '|', -- missing seq numbers + ['errcond'] = false, -- true if sock, ntp or sane response error exists + ['rcv_again'] = false, -- true if we should receive_bytes again (more bit is set or missing seq). + ['target'] = target, -- target ip and port + ['v'] = 2, -- ntp version + ['m'] = 7, -- ntp mode + ['c'] = rcode, -- ntp request code + ['i'] = inum -- ntp request implementation number + } + + -- receive and parse + repeat + -- receive any response + local rcvstatus, response = sock:receive_bytes(1) + -- check the response + local packet_to_parse = check(rcvstatus, response, track) + -- parse the response + if not track.errcond then + local remain = parse_v2m7(packet_to_parse, records) + if remain > 0 then + stdnse.debug1('MAX_RECORDS has been reached while parsing NTPv2 Mode 7 Code %d responses from the target %s.', rcode, target) + track.rcv_again = false + elseif remain == -1 then + stdnse.debug1('Parsing of NTPv2 Mode 7 implementation number %d request code %d response from %s has not been implemented.', inum, rcode, target) + track.rcv_again = false + end + end + records.badpkts = records.badpkts + track.evil_pkts + if records.badpkts >= MAXIMUM_EVIL then + stdnse.debug1('Had %d bad packets from %s - Not continuing with this host!', target, records.badpkts) + sock:close() + return nil + end + + until not track.rcv_again + + return sock + +end + + +--- +-- Generates an NTP Private Mode (7) request with the supplied implementation +-- number and request code. +-- +-- @param impl number - valid values are 0, 2 and 3 - defaults to 3 +-- @param requestCode number - defaults to 42 +-- @return String request. +-- +function getPrivateMode(impl, requestCode) + local pay + -- udp payload is 48 bytes. + -- For a description of Mode 7 packets see NTP source file: + -- include/ntp_request.h + -- + -- Flags 8bits: 0x17 + -- (Response Bit: 0, More Bit: 0, Version Number 3bits: 2, Mode 3bits: 7) + -- Authenticated Bit: 0, Sequence Number 7bits: 0 + -- Implementation Number 8bits: e.g. 0x03 (IMPL_XNTPD) + -- Request Code 8bits: e.g. 0x2a (MON_GETLIST_1) + -- Err 4bits: 0, Number of Data Items 12bits: 0 + -- MBZ 4bits: 0, Size of Data Items 12bits: 0 + return string.char( + 0x17, 0x00, impl or 0x03, requestCode or 0x2a, + 0x00, 0x00, 0x00, 0x00 + ) + -- Data 40 Octets: 0 + .. ("\x00"):rep(40) + -- The following are optional if the Authenticated bit is set: + -- Encryption Keyid 4 Octets: 0 + -- Message Authentication Code 16 Octets (MD5): 0 +end + + +--- +-- Checks that the response from the target is a valid NTP response. +-- +-- Starts by checking that the socket read was successful and then creates a +-- packet object from the response (with dummy IP and UDP headers). Then +-- perform checks that ensure that the response is of the expected type and +-- length, that the records in the response are of the correct size and that +-- the response is part of a sequence of 1 or more responses and is not a +-- duplicate. +-- +-- @param status boolean returned from a socket read operation. +-- @param response string response returned from a socket operation. +-- @param track table used for tracking a sequence of NTP responses. +-- @return A Packet object ready for parsing or +-- nil if the response does not pass all checks. +-- +function check(status, response, track) + + -- check for socket error + if not status then + track.errcond = true + track.rcv_again = false + if track.rcv_again then -- we were expecting more responses + stdnse.debug1('Socket error while reading from %s - %s', track.target, response) + end + return nil + end + + -- reset flags + track.errcond = false + track.rcv_again = false + + -- create a packet object + local pkt = make_udp_packet(response) + if pkt == nil then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1('Failed to create a Packet object with response from %s', track.target) + return nil + end + + -- off is the start of udp payload i.e. NTP + local off = 28 + + -- NTP sanity checks + + local val + + -- NTP data must be at least 8 bytes + val = response:len() + if val < 8 then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1('Expected a response of at least 8 bytes from %s, got %d bytes.', track.target, val) + return nil + end + + -- response bit set + if (pkt:u8(off) >> 7) ~= 1 then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1('Bad response from %s - did not have response bit set.', track.target) + return nil + end + -- version is as expected + val = (pkt:u8(off) >> 3) & 0x07 + if val ~= track.v then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1('Bad response from %s - expected NTP version %d, got %d', track.target, track.v, val) + return nil + end + -- mode is as expected + val = pkt:u8(off) & 0x07 + if val ~= track.m then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1('Bad response from %s - expected NTP mode %d, got %d', track.target, track.m, val) + return nil + end + -- implementation number is as expected + val = pkt:u8(off+2) + if val ~= track.i then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1('Bad response from %s - expected NTP implementation number %d, got %d', track.target, track.i, val) + return nil + end + -- request code is as expected + val = pkt:u8(off+3) + if val ~= track.c then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1('Bad response from %s - expected NTP request code %d got %d.', track.target, track.c, val) + return nil + end + -- NTP error conditions - defined codes are not evil (bogus codes are). + local fail, msg = false + local err = (pkt:u8(off+4) >> 4) & 0x0f + if err == 0 then + -- NoOp + elseif err == 1 then + fail = true + msg = 'Incompatible Implementation Number' + elseif err == 2 then + fail = true + msg = 'Unimplemented Request Code' + elseif err == 3 then + fail = true + msg = 'Format Error' -- could be that auth is required - we didn't provide it. + elseif err == 4 then + fail = true + msg = 'No Data Available' -- monitor not enabled or nothing in mru list. + elseif err == 5 or err == 6 then + fail = true + msg = 'I don\'t know' + elseif err == 7 then + fail = true + msg = 'Authentication Failure' + elseif err > 7 then + fail = true + track.evil_pkts = track.evil_pkts+1 + msg = 'Bogus Error Code!' -- should not happen... + end + if fail then + track.errcond = true + stdnse.debug1('Response from %s was NTP Error Code %d - "%s"', track.target, err, msg) + return nil + end + + -- length checks - the data (number of items * size of an item) should be + -- 8 <= data <= 500 and each data item should be of correct length for the + -- implementation and request type. + + -- Err 4 bits, Number of Data Items 12 bits + local icount = pkt:u16(off+4) & 0xFFF + -- MBZ 4 bits, Size of Data Items: 12 bits + local isize = pkt:u16(off+6) & 0xFFF + if icount < 1 then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1('Expected at least one record from %s.', track.target) + return nil + elseif icount*isize + 8 > response:len() then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1('NTP Mode 7 response from %s has invalid count (%d) and/or size (%d) values.', track.target, icount, isize) + return nil + elseif icount*isize > 500 then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1('NTP Mode 7 data section is larger than 500 bytes (%d) in response from %s.', icount*isize, track.target) + return nil + end + + if track.c == 42 and track.i == 3 and isize ~= 72 then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1( + 'Expected item size of 72 bytes (got %d) for request code 42 implementation number 3 in response from %s.', + isize, track.target + ) + return nil + elseif track.c == 0 and track.i == 3 and isize ~= 32 then + track.errcond = true + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1( + 'Expected item size of 32 bytes (got %d) for request code 0 implementation number 3 in response from %s.', + isize, track.target + ) + return nil + end + + -- is the response out of sequence, a duplicate or is it peachy + local seq = pkt:u8(off+1) & 0x7f + if seq == track.hseq+1 then -- all good + track.hseq = track.hseq+1 + elseif track.mseq:match(('|%d|'):format(seq)) then -- one of our missing seq# + track.mseq:gsub(('|%d|'):format(seq), '|', 1) + stdnse.debug3('Response from %s with sequence number %s was previously missing.', -- this never seems to happen! + track.target, seq + ) + elseif seq > track.hseq then -- some seq# have gone missing + for i=track.hseq+1, seq-1 do + track.mseq = ('%s%d|'):format(track.mseq, i) + end + stdnse.debug3( + 'Response from %s was out of sequence - expected #%d but got #%d (missing:%s)', + track.target, track.hseq+1, seq, track.mseq + ) + track.hseq = seq + else -- seq <= hseq !duplicate! + track.evil_pkts = track.evil_pkts+1 + stdnse.debug1( + 'Response from %s had a duplicate sequence number - dropping it.', + track.target + ) + return nil + end + + -- if the more bit is set or if we have missing sequence numbers then we'll + -- want to receive more packets after parsing this one. + local more = (pkt:u8(off) >> 6) & 0x01 + if more == 1 then + track.rcv_again = true + elseif track.mseq:len() > 1 then + track.rcv_again = true + end + + return pkt + +end + + +--- +-- Returns a Packet Object generated with dummy IP and UDP headers and the +-- supplied UDP payload so that the payload may be conveniently parsed using +-- packet library methods. The dummy headers contain the barest information +-- needed to appear valid to packet.lua +-- +-- @param response String UDP payload. +-- @return Packet object or nil in case of an error. +-- +function make_udp_packet(response) + + -- udp len + local udplen = 8 + response:len() + -- ip len + local iplen = 20 + udplen + + -- dummy headers + -- ip + local dh = "\x45\x00" -- IPv4, 20-byte header, no DSCP, no ECN + .. string.pack('>I2', iplen) -- total length + .. "\x00\x00" -- IPID 0 + .. "\x40\x00" -- DF + .. "\x40\x11" -- TTL 0x40, UDP (proto 17) + .. "\x00\x00" -- checksum 0 + .. "\x00\x00\x00\x00\x00\x00\x00\x00" -- Source, destination 0.0.0.0 + .. "\x00\x00\x00\x00" -- UDP source, dest port 0 + .. string.pack('>I2', udplen) -- UDP length + .. "\x00\x00" -- UDP checksum 0 + + return packet.Packet:new(dh .. response, iplen) + +end + + +--- +-- Invokes parsing routines for NTPv2 Mode 7 response packets based on the +-- implementation number and request code defined in the response. +-- +-- @param pkt Packet Object to be parsed. +-- @param recs Table to hold the accumulated records parsed from supplied +-- packet objects. +-- @return Number of records not parsed from the packet (usually zero) or +-- -1 if the response does not have an associated parsing routine. +-- +function parse_v2m7(pkt, recs) + local off = pkt.udp_offset + 8 + local impl = pkt:u8(off+2) + local code = pkt:u8(off+3) + if (impl == 3 or impl == 2) and code == 42 then + return parse_monlist_1(pkt, recs) + elseif (impl == 3 or impl == 2) and code == 0 then + return parse_peerlist(pkt, recs) + else + return -1 + end +end + + +--- +-- Parsed records from the supplied monitor list packet into the supplied table +-- of accumulated records. +-- +-- The supplied response packets should be NTPv2 Mode 7 implementation number 2 +-- or 3 and request code 42. +-- The fields parsed are the source and destination IP addresses, the count of +-- times the target has seen the host, the method of transmission (uni|broad| +-- multicast), NTP Version and Mode of the last packet received by the target +-- from the host. +-- +-- @param pkt Packet object to extract monitor records from. +-- @param recs A table of accumulated monitor records for storage of parsed +-- records. +-- @return Number of records not parsed from the packet which will be zero +-- except when MAX_RECORDS is reached. +-- +function parse_monlist_1(pkt, recs) + + local off = pkt.udp_offset + 8 -- beginning of NTP + local icount = pkt:u16(off+4) & 0xFFF + local isize = pkt:u16(off+6) & 0xFFF + local remaining = icount + + off = off+8 -- beginning of data section + + for i=1, icount, 1 do + if #recs + #recs.peerlist >= MAX_RECORDS then + return remaining + end + local pos = off + isize * (i-1) -- beginning of item + local t = {} + + -- src and dst addresses + -- IPv4 if impl == 2 or v6 flag is not set + if isize == 32 or pkt:u8(pos+32) ~= 1 then -- IPv4 + local saddr = ipOps.str_to_ip(pkt:raw(pos+16, 4)) + local daddr = ipOps.str_to_ip(pkt:raw(pos+20, 4)) + t.saddr = saddr + t.daddr = daddr + else -- IPv6 + local saddr = {} + for j=40, 54, 2 do + saddr[#saddr+1] = stdnse.tohex(pkt:u16(pos+j)) + end + t.saddr = table.concat(saddr, ':') + local daddr = {} + for j=56, 70, 2 do + daddr[#daddr+1] = stdnse.tohex(pkt:u16(pos+j)) + end + t.daddr = table.concat(daddr, ':') + end + + t.count = pkt:u32(pos+12) + t.flags = pkt:u32(pos+24) + -- I've seen flags be wrong-endian just once. why? I really don't know. + -- Some implementations are not doing htonl for this field? + if t.flags > 0xFFFFFF then + -- only concerned with the high order byte + t.flags = t.flags >> 24 + end + t.mode = pkt:u8(pos+30) + t.version = pkt:u8(pos+31) + recs[#recs+1] = t + remaining = remaining -1 + end + + return remaining +end + + +--- +-- Parsed records from the supplied peer list packet into the supplied table of +-- accumulated records. +-- +-- The supplied response packets should be NTPv2 Mode 7 implementation number 2 +-- or 3 and request code 0. +-- The fields parsed are the source IP address and the peer information flag. +-- +-- @param pkt Packet object to extract peer records from. +-- @param recs A table of accumulated monitor and peer records for storage of +-- parsed records. +-- @return Number of records not parsed from the packet which will be zero +-- except when MAX_RECORDS is reached. +-- +function parse_peerlist(pkt, recs) + + local off = pkt.udp_offset + 8 -- beginning of NTP + local icount = pkt:u16(off+4) & 0xFFF + local isize = pkt:u16(off+6) & 0xFFF + local remaining = icount + + off = off+8 -- beginning of data section + + for i=0, icount-1, 1 do + if #recs + #recs.peerlist >= MAX_RECORDS then + return remaining + end + local pos = off + (i * isize) -- beginning of item + local t = {} + + -- src address + -- IPv4 if impl == 2 or v6 flag is not set + if isize == 8 or pkt:u8(pos+8) ~= 1 then + local saddr = ipOps.str_to_ip(pkt:raw(pos, 4)) + t.saddr = saddr + else -- IPv6 + local saddr = {} + for j=16, 30, 2 do + saddr[#saddr+1] = stdnse.tohex(pkt:u16(pos+j)) + end + t.saddr = table.concat(saddr, ':') + end + + t.flags = pkt:u8(pos+7) + table.insert(recs.peerlist, t) + remaining = remaining -1 + end + + return remaining +end + + +--- +-- Interprets the supplied records to discover information about the target +-- NTP associations. +-- +-- Associations are categorised as NTP Servers, Peers and Clients based on the +-- Mode of packets sent to the target. Alternative target interfaces are +-- recorded as well as the transmission mode of packets sent to the target (i.e. +-- unicast, broadcast or multicast). +-- +-- @param recs A table of accumulated monitor and peer records for storage +-- of parsed records. +-- @param targetip String target IP address (e.g. host.ip) +-- @return Table of interpreted results with fields such as servs, clients, +-- peers, ifaces etc. +-- +function interpret(recs, targetip) + local txtyp = { + ['1'] = 'unicast', + ['2'] = 'broadcast', + ['4'] = 'multicast' + } + local t = {} + t.servs = {['pub']={['4']={},['6']={}}, ['prv']={['4']={},['6']={}}} + t.peers = {['pub']={['4']={},['6']={}}, ['prv']={['4']={},['6']={}}} + t.porc = {['pub']={['4']={},['6']={}}, ['prv']={['4']={},['6']={}}} + t.clients = {['pub']={['4']={},['6']={}}, ['prv']={['4']={},['6']={}}} + t.casts = {['b']={['4']={},['6']={}}, ['m']={['4']={},['6']={}}} + t.ifaces = {['4']={},['6']={}} + t.other = {} + t.sync = '' + if #recs.peerlist > 0 then + t.have_peerlist = true + recs.peerhash = {} + for _, peer in ipairs(recs.peerlist) do + recs.peerhash[peer.saddr] = peer + end + else + t.have_peerlist = false + end + + for _, r in ipairs(recs) do + local vis = ipOps.isPrivate(r.saddr) and 'prv' or 'pub' + local af = r.saddr:match(':') and '6' or '4' + + -- is the host a client, peer, server or other? + if r.mode == 3 then + table.insert(t.clients[vis][af], r.saddr) + elseif r.mode == 4 then + table.insert(t.servs[vis][af], r.saddr) + elseif r.mode == 2 then + table.insert(t.peers[vis][af], r.saddr) + elseif r.mode == 1 then + + -- if we have a list of peers we can distinguish between mode 1 peers and + -- mode 1 peers that are really clients (i.e. not configured as peers). + if t.have_peerlist then + if recs.peerhash[r.saddr] then + table.insert(t.peers[vis][af], r.saddr) + else + table.insert(t.clients[vis][af], r.saddr) + end + else + table.insert(t.porc[vis][af], r.saddr) + end + + elseif r.mode == 5 then + table.insert(t.servs[vis][af], r.saddr) + else + local tx = tostring(r.flags) + table.insert( + t.other, + ('%s%s seen %d time%s. last tx was %s v%d mode %d'):format( + r.saddr, _ == 1 and ' (You?)' or '', r.count, + r.count > 1 and 's' or '', + txtyp[tx] or tx, r.version, r.mode + ) + ) + end + + local function isLoopback(addr) + if addr:match(':') then + if ipOps.compare_ip(addr, 'eq', '::1') then return true end + elseif addr:match('^127') then + return true + end + return false + end + + -- destination addresses are target interfaces or broad/multicast addresses. + if not isLoopback(r.daddr) then + if r.flags == 1 then + t.ifaces[af][r.daddr] = r.daddr + elseif r.flags == 2 then + t.casts.b[af][r.daddr] = r.daddr + elseif r.flags == 4 then + t.casts.m[af][r.daddr] = r.daddr + else -- shouldn't happen + stdnse.debug1( + 'Host associated with %s had transmission flag value %d - Strange!', + targetip, r.flags + ) + end + end + + end -- for + + local function isTarget(addr, target) + local targ_af = target:match(':') and 6 or 4 + local test_af = addr:match(':') and 6 or 4 + if test_af ~= targ_af then return false end + if targ_af == 4 and addr == target then return true end + if targ_af == 6 + and (ipOps.compare_ip(addr, 'eq', target)) then return true end + return false + end + + -- reorganise ifaces and casts tables + local _ = {} + for k, v in pairs(t.ifaces['4']) do + if not isTarget(v, targetip) then + _[#_+1] = v + end + end + t.ifaces['4'] = _ + _ = {} + for k, v in pairs(t.ifaces['6']) do + if not isTarget(v, targetip) then + _[#_+1] = v + end + end + t.ifaces['6'] = _ + _ = {} + for k, v in pairs(t.casts.b['4']) do + _[#_+1] = v + end + t.casts.b['4'] = _ + _ = {} + for k, v in pairs(t.casts.b['6']) do + _[#_+1] = v + end + t.casts.b['6'] = _ + _ = {} + for k, v in pairs(t.casts.m['4']) do + _[#_+1] = v + end + t.casts.m['4'] = _ + _ = {} + for k, v in pairs(t.casts.m['6']) do + _[#_+1] = v + end + t.casts.m['6'] = _ + + -- Single out the server to which the target is synched. + -- Note that this server may not even appear in the monlist - it depends how + -- busy the server is. + if t.have_peerlist then + for _, peer in ipairs(recs.peerlist) do + if (peer.flags & 0x2) == 0x2 then + t.sync = peer.saddr + if peer.saddr:match('^127') then -- always IPv4, never IPv6! + t.sync = t.sync .. ' (reference clock)' + end + break + end + end + end + + return t + +end + + +--- +-- Outputs the supplied table of interpreted records. +-- +-- @param t Table of interpreted records as returned from interpret(). +-- @return String script output. +-- +function summary(t) + + local o = {} + local count = 0 + local vbs = nmap.verbosity() + + -- Target is Synchronised with: + if t.sync ~= '' then + table.insert(o, ('Target is synchronised with %s'):format(t.sync)) + end + + -- Alternative Target Interfaces + if #t.ifaces['4'] > 0 or #t.ifaces['6'] > 0 then + table.insert(o, + { + ['name'] = 'Alternative Target Interfaces:', + output_ips(t.ifaces) + } + ) + end + + -- Target listens to Broadcast Addresses + if #t.casts.b['4'] > 0 or #t.casts.b['6'] > 0 then + table.insert(o, + { + ['name'] = 'Target listens to Broadcast Addresses:', + output_ips(t.casts.b) + } + ) + end + + -- Target listens to Multicast Addresses + if #t.casts.m['4'] > 0 or #t.casts.m['6'] > 0 then + table.insert(o, + { + ['name'] = 'Target listens to Multicast Addresses:', + output_ips(t.casts.m) + } + ) + end + + -- Private Servers + count = #t.servs.prv['4']+#t.servs.prv['6'] + if count > 0 or vbs > 1 then + table.insert(o, + { + ['name'] = ('Private Servers (%d)'):format(count), + output_ips(t.servs.prv) + } + ) + end + -- Public Servers + count = #t.servs.pub['4']+#t.servs.pub['6'] + if count > 0 or vbs > 1 then + table.insert(o, + { + ['name'] = ('Public Servers (%d)'):format(count), + output_ips(t.servs.pub) + } + ) + end + + -- Private Peers + count = #t.peers.prv['4']+#t.peers.prv['6'] + if count > 0 or vbs > 1 then + table.insert(o, + { + ['name'] = ('Private Peers (%d)'):format(count), + output_ips(t.peers.prv) + } + ) + end + -- Public Peers + count = #t.peers.pub['4']+#t.peers.pub['6'] + if count > 0 or vbs > 1 then + table.insert(o, + { + ['name'] = ('Public Peers (%d)'):format(count), + output_ips(t.peers.pub) + } + ) + end + + -- Private Peers or Clients + count = #t.porc.prv['4']+#t.porc.prv['6'] + if not t.have_peerlist and (count > 0 or vbs > 1) then + table.insert(o, + { + ['name'] = ('Private Peers or Clients (%d)'):format(count), + output_ips(t.porc.prv) + } + ) + end + -- Public Peers or Clients + count = #t.porc.pub['4']+#t.porc.pub['6'] + if not t.have_peerlist and (count > 0 or vbs > 1) then + table.insert(o, + { + ['name'] = ('Public Peers or Clients (%d)'):format(count), + output_ips(t.porc.pub) + } + ) + end + + -- Private Clients + count = #t.clients.prv['4']+#t.clients.prv['6'] + if count > 0 or vbs > 1 then + table.insert(o, + { + ['name'] = ('Private Clients (%d)'):format(count), + output_ips(t.clients.prv) + } + ) + end + -- Public Clients + count = #t.clients.pub['4']+#t.clients.pub['6'] + if count > 0 or vbs > 1 then + table.insert(o, + { + ['name'] = ('Public Clients (%d)'):format(count), + output_ips(t.clients.pub) + } + ) + end + + -- Other + count = #t.other + if count > 0 then + table.insert(o, + { + ['name'] = ('Other Associations (%d)'):format(count), + t.other + } + ) + end + + return stdnse.format_output(true, o) + +end + + +--- +-- Sorts and combines a set of IPv4 and IPv6 addresses into a table of rows. +-- IPv4 addresses are ascending-sorted numerically and arranged in four columns +-- and IPv6 appear in subsequent rows, sorted and arranged to fit as many +-- addresses into a row as possible without the need for wrapping. +-- +-- @param t Table containing two subtables indexed as '4' and '6' containing +-- a list of IPv4 and IPv6 addresses respectively. +-- @return Table where each entry is a row of sorted and arranged IP addresses. +-- +function output_ips(t) + + if #t['4'] < 1 and #t['6'] < 1 then return nil end + + local o = {} + + -- sort and tabulate IPv4 addresses + table.sort(t['4'], function(a,b) return ipOps.compare_ip(a, "lt", b) end) + local limit = #t['4'] + local cols = 4 + local rows = math.ceil(limit/cols) + local numlast = limit - cols*rows + cols + local pad4 = (' '):rep(15) + local index = 0 + for c=1, cols, 1 do + for r=1, rows, 1 do + if r == rows and c > numlast then break end + index = index+1 + o[r] = o[r] or '' + local padlen = pad4:len() - t['4'][index]:len() + o[r] = ('%s%s%s '):format(o[r], t['4'][index], pad4:sub(1, padlen)) + end + end + + -- IPv6 + -- Rows are allowed to be 71 chars wide + table.sort(t['6'], function(a,b) return ipOps.compare_ip(a, "lt", b) end) + local i = 1 + local limit = #t['6'] + while i <= limit do + local work = {} + local len = 0 + local j = i + repeat + if not t['6'][j] then j = j-1; break end + len = len + t['6'][j]:len() + 1 + if len > 71 then + j = j-1 + else + j = j+1 + end + until len > 71 + for n = i, j, 1 do + work[#work+1] = t['6'][n] + end + o[#o+1] = table.concat(work, ' ') + i = j+1 + end + return o +end |