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