Created
January 15, 2014 10:03
-
-
Save abdallah/8433742 to your computer and use it in GitHub Desktop.
Inspect range of IPs for NTP DDOS vulnerability
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
IPRANGE=10.10.10.0/24 | |
nmap -sU -pU:123 -Pn -n --script=ntp-monlist $IPRANGE | grep -B 4 ntp-monlist | grep report | awk '{ print $5 }' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- downloaded from http://nmap.org/svn/scripts/ntp-monlist.nse | |
local bin = require "bin" | |
local bit = require "bit" | |
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 http://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 innoccuous | |
looking Client Mode request. This can be acheived 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.print_debug(1, | |
'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.print_debug(1, | |
'Error establishing a UDP connection for %s - %s', target, conerr | |
) | |
return nil | |
end | |
end | |
-- send | |
stdnse.print_debug(2, | |
'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.print_debug(1, | |
'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.print_debug(1, | |
'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.print_debug(1, | |
'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.print_debug(1, | |
'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 | |
pay = string.char( | |
0x17, 0x00, impl or 0x03, requestCode or 0x2a, | |
0x00, 0x00, 0x00, 0x00 | |
) | |
-- Data 40 Octets: 0 | |
pay = pay .. string.char(0x00):rep(40) | |
-- The following are optional if the Authenticated bit is set: | |
-- Encryption Keyid 4 Octets: 0 | |
-- Message Authentication Code 16 Octets (MD5): 0 | |
return pay | |
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.print_debug(1, | |
'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.print_debug(1, | |
'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.print_debug(1, | |
'Expected a response of at least 8 bytes from %s, got %d bytes.', | |
track.target, val | |
) | |
return nil | |
end | |
-- response bit set | |
if bit.rshift(pkt:u8(off), 7) ~= 1 then | |
track.errcond = true | |
track.evil_pkts = track.evil_pkts+1 | |
stdnse.print_debug(1, | |
'Bad response from %s - did not have response bit set.', track.target | |
) | |
return nil | |
end | |
-- version is as expected | |
val = bit.band(bit.rshift(pkt:u8(off), 3), 0x07) | |
if val ~= track.v then | |
track.errcond = true | |
track.evil_pkts = track.evil_pkts+1 | |
stdnse.print_debug(1, | |
'Bad response from %s - expected NTP version %d, got %d', track.target, | |
track.v, val | |
) | |
return nil | |
end | |
-- mode is as expected | |
val = bit.band(pkt:u8(off), 0x07) | |
if val ~= track.m then | |
track.errcond = true | |
track.evil_pkts = track.evil_pkts+1 | |
stdnse.print_debug(1, | |
'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.print_debug(1, | |
'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.print_debug(1, | |
'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 = bit.band(bit.rshift(pkt:u8(off+4), 4), 0x0f) | |
if err == 0 then | |
-- NoOp | |
elseif err == 1 then | |
fail = true | |
msg = 'Incompatable 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.print_debug(1, | |
'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 = bit.band(pkt:u16(off+4), 0xFFF) | |
-- MBZ 4 bits, Size of Data Items: 12 bits | |
local isize = bit.band(pkt:u16(off+6), 0xFFF) | |
if icount < 1 then | |
track.errcond = true | |
track.evil_pkts = track.evil_pkts+1 | |
stdnse.print_debug(1, | |
'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.print_debug(1, | |
'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.print_debug(1, | |
'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.print_debug(1, | |
'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.print_debug(1, | |
'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 = bit.band(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.print_debug(3, | |
'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.print_debug(3, | |
'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.print_debug(1, | |
'Response from %s had a duplcate 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 = bit.band(bit.rshift(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 = string.char(0x45, 0x00) | |
dh = dh .. bin.pack('S', iplen) | |
dh = dh .. string.char( | |
0x00, 0x00, 0x40, 0x00, 0x40, 0x11, 0x00, 0x00, | |
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | |
-- udp | |
0x00, 0x00, 0x00, 0x00 | |
) | |
dh = dh .. bin.pack('S', udplen) | |
dh = dh .. string.char(0x00, 0x00) | |
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 = bit.band(pkt:u16(off+4), 0xFFF) | |
local isize = bit.band(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 = packet.toip(pkt:raw(pos+16, 4)) | |
local daddr = packet.toip(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 = bit.rshift(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 = bit.band(pkt:u16(off+4), 0xFFF) | |
local isize = bit.band(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 = packet.toip(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.print_debug(1, | |
'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 bit.band(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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment