zgrab2/modules/ntp/scanner.go

1037 lines
32 KiB
Go

// Package ntp provides a zgrab2 module that probes for the NTP service.
// NOTE: unlike most modules, this scans on UDP.
//
// The default scan does a standard get time request.
//
// Passing the monlist flag will check for the DDoS-amplifying MONLIST command.
//
// The results of the scan are the version number and the time returned by the
// server, and if verbose results are enabled, the entire parsed response
// packet(s).
//
// For more details on NTP, see https://tools.ietf.org/html/rfc5905.
package ntp
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"strconv"
"time"
log "github.com/sirupsen/logrus"
"github.com/zmap/zgrab2"
)
var (
// ErrInvalidLeapIndicator is returned if an invalid LeapIndicator is found
ErrInvalidLeapIndicator = errors.New("leap indicator not valid")
// ErrInvalidVersion is returned if an invalid version number is found
ErrInvalidVersion = errors.New("version number not valid")
// ErrInvalidMode is returned if an invalid mode identifier is found
ErrInvalidMode = errors.New("mode not valid")
// ErrInvalidStratum is returned if an invalid stratum identifier is found
ErrInvalidStratum = errors.New("stratum invalid")
// ErrInvalidReferenceID is returned if an invalid reference ID is found (i.e. it contains non-ASCII characters)
ErrInvalidReferenceID = errors.New("reference ID contained non-ASCII characters")
// ErrBufferTooSmall is returned if a buffer is not large enough to contain the input
ErrBufferTooSmall = errors.New("buffer too small")
// ErrInvalidHeader is returned if the header cannot be interpreted as a valid NTP header
ErrInvalidHeader = errors.New("invalid header data")
// ErrInvalidResponse is returned if the response cannot be interpreted as a valid NTP response
ErrInvalidResponse = errors.New("invalid response")
// ErrInvalidRequestCode is returned if an invalid RequestCode is found
ErrInvalidRequestCode = errors.New("request code invalid")
)
// Section 6 of https://tools.ietf.org/html/rfc5905: times are relative to 1/1/1900 UTC
var ntpEpoch = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
var unixEpoch = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
// LeapIndicator is a two-bit field, whose values are defined in figure 9 of https://tools.ietf.org/html/rfc5905
type LeapIndicator uint8
const (
// NoWarning is a LeapIndicator that indicates that there is no problem
NoWarning LeapIndicator = 0
// ExtraSecond is a LeapIndicator that indicates that the last minute has 61 seconds
ExtraSecond = 1
// MissingSecond is a LeapIndicator that indicates that the last minute has 59 seconds
MissingSecond = 2
// Unknown is a LeapIndicator that indicates an unknown alarm condition
Unknown = 3
)
// AssociationMode is a three-bit value, whose values are defined in figure 9 of https://tools.ietf.org/html/rfc5905
type AssociationMode uint8
const (
// Reserved is a reserved AssociationMode
Reserved AssociationMode = 0
// SymmetricActive is an AssociationMode indicating that the service is in the active symmetric mode
SymmetricActive = 1
// SymmetricPassive is an AssociationMode indicating that the service is in the passive symmetric mode
SymmetricPassive = 2
// Client is an AssociationMode indicating that the caller is a client
Client = 3
// Server is an AssociationMode indicating that the packet is to be interpreted as a server
Server = 4
// Broadcast is an AssociationMode indicating that the this is a broadcast packet
Broadcast = 5
// Control is an AssociationMode reserved for NTP control messages
Control = 6
// Private is an AssociationMode reserved for private use
Private = 7
)
// ImplNumber is an 8-bit value used in Private packets
type ImplNumber uint8
// Constants from ntp/include/ntp_request.h
const (
// ImplUniv corresponds to the IMPL_UNIV constant
ImplUniv ImplNumber = 0
// ImplXNTPDOld corresponds to the IMPL_XNTPD_OLD constant
ImplXNTPDOld = 2
// ImplXNTPD corresponds to the IMPL_XNTPD constant
ImplXNTPD = 3
)
// These match the #define values in ntp_request.h
var implNumberMap = map[ImplNumber]string{
ImplUniv: "IMPL_UNIV",
ImplXNTPDOld: "IMPL_XNTPD_OLD",
ImplXNTPD: "IMPL_XNTPD",
}
// MarshalJSON gives the #define name, or "UNKNOWN (0x##)"
func (num ImplNumber) MarshalJSON() ([]byte, error) {
ret, ok := implNumberMap[num]
if !ok {
ret = fmt.Sprintf("UNKNOWN (0x%02x)", num)
}
return json.Marshal(ret)
}
// RequestCode is an 8-bit value used in Private packets, from ntp/include/ntp_request.h
type RequestCode uint8
const (
// ReqPeerList corresponds to the REQ_PEER_LIST constant
ReqPeerList RequestCode = 0
// ReqPeerListSum corresponds to the REQ_PEER_LIST_SUM constant
ReqPeerListSum = 1
// ReqPeerInfo corresponds to the REQ_PEER_INFO constant
ReqPeerInfo = 2
// ReqPeerStats corresponds to the REQ_PEER_STATS constant
ReqPeerStats = 3
// ReqSysInfo corresponds to the REQ_SYS_INFO constant
ReqSysInfo = 4
// ReqSysStats corresponds to the REQ_SYS_STATS constant
ReqSysStats = 5
// ReqIOStats corresponds to the REQ_IO_STATS constant
ReqIOStats = 6
// ReqMemStats corresponds to the REQ_MEM_STATS constant
ReqMemStats = 7
// ReqLoopInfo corresponds to the REQ_LOOP_INFO constant
ReqLoopInfo = 8
// ReqTimerStats corresponds to the REQ_TIMER_STATS constant
ReqTimerStats = 9
// ReqConfig corresponds to the REQ_CONFIG constant
ReqConfig = 10
// ReqUnconfig corresponds to the REQ_UNCONFIG constant
ReqUnconfig = 11
// ReqSetSysFlag corresponds to the REQ_SET_SYS_FLAG constant
ReqSetSysFlag = 12
// ReqClrSysFlag corresponds to the REQ_CLR_SYS_FLAG constant
ReqClrSysFlag = 13
// ReqMonitor corresponds to the REQ_MONITOR constant
ReqMonitor = 14
// ReqNoMonitor corresponds to the REQ_NOMONITOR constant
ReqNoMonitor = 15
// ReqGetRestrict corresponds to the REQ_GET_RESTRICT constant
ReqGetRestrict = 16
// ReqResAddFlags corresponds to the REQ_RES_ADD_FLAGS constant
ReqResAddFlags = 17
// ReqResSubFlags corresponds to the REQ_RES_SUB_FLAGS constant
ReqResSubFlags = 18
// ReqUnrestrict corresponds to the REQ_UNRESTRICT constant
ReqUnrestrict = 19
// ReqMonGetList corresponds to the REQ_MON_GETLIST constant
ReqMonGetList = 20
// ReqResetStats corresponds to the REQ_RESET_STATS constant
ReqResetStats = 21
// ReqResetPeer corresponds to the REQ_RESET_PEER constant
ReqResetPeer = 22
// ReqRereadKeys corresponds to the REQ_REREAD_KEYS constant
ReqRereadKeys = 23
// ReqDoDirtyHack corresponds to the REQ_DO_DIRTY_HACK constant
ReqDoDirtyHack = 24
// ReqDontDirtyHack corresponds to the REQ_DONT_DIRTY_HACK constant
ReqDontDirtyHack = 25
// ReqTrustKey corresponds to the REQ_TRUST_KEY constant
ReqTrustKey = 26
// ReqUntrustKey corresponds to the REQ_UNTRUST_KEY constant
ReqUntrustKey = 27
// ReqAuthInfo corresponds to the REQ_AUTH_INFO constant
ReqAuthInfo = 28
// ReqTraps corresponds to the REQ_TRAPS constant
ReqTraps = 29
// ReqAddTrap corresponds to the REQ_ADD_TRAP constant
ReqAddTrap = 30
// ReqClrTrap corresponds to the REQ_CLR_TRAP constant
ReqClrTrap = 31
// ReqRequestKey corresponds to the REQ_REQUEST_KEY constant
ReqRequestKey = 32
// ReqControlKey corresponds to the REQ_CONTROL_KEY constant
ReqControlKey = 33
// ReqGetCtlStats corresponds to the REQ_GET_CTLSTATS constant
ReqGetCtlStats = 34
// ReqGetLeapInfo corresponds to the REQ_GET_LEAPINFO constant
ReqGetLeapInfo = 35
// ReqGetClockInfo corresponds to the REQ_GET_CLOCKINFO constant
ReqGetClockInfo = 36
// ReqSetClkFudge corresponds to the REQ_SET_CLKFUDGE constant
ReqSetClkFudge = 37
// ReqGetKernel corresponds to the REQ_GET_KERNEL constant
ReqGetKernel = 38
// ReqGetClkBugInfo corresponds to the REQ_GET_CLKBUGINFO constant
ReqGetClkBugInfo = 39
// ReqSetPrecision corresponds to the REQ_SET_PRECISION constant
ReqSetPrecision = 41
// ReqMonGetList1 corresponds to the REQ_MON_GETLIST_1 constant
ReqMonGetList1 = 42
// ReqHostnameAssocID corresponds to the REQ_HOSTNAME_ASSOCID constant
ReqHostnameAssocID = 43
// ReqIfStats corresponds to the REQ_IF_STATS constant
ReqIfStats = 44
// ReqIfReload corresponds to the REQ_IF_RELOAD constant
ReqIfReload = 45
)
// These match the #defines in ntp-request.h
var requestCodeMap = map[string]RequestCode{
"REQ_PEER_LIST": ReqPeerList,
"REQ_PEER_LIST_SUM": ReqPeerListSum,
"REQ_PEER_INFO": ReqPeerInfo,
"REQ_PEER_STATS": ReqPeerStats,
"REQ_SYS_INFO": ReqSysInfo,
"REQ_SYS_STATS": ReqSysStats,
"REQ_IO_STATS": ReqIOStats,
"REQ_MEM_STATS": ReqMemStats,
"REQ_LOOP_INFO": ReqLoopInfo,
"REQ_TIMER_STATS": ReqTimerStats,
"REQ_CONFIG": ReqConfig,
"REQ_UNCONFIG": ReqUnconfig,
"REQ_SET_SYS_FLAG": ReqSetSysFlag,
"REQ_CLR_SYS_FLAG": ReqClrSysFlag,
"REQ_MONITOR": ReqMonitor,
"REQ_NOMONITOR": ReqNoMonitor,
"REQ_GET_RESTRICT": ReqGetRestrict,
"REQ_RESADDFLAGS": ReqResAddFlags,
"REQ_RESSUBFLAGS": ReqResSubFlags,
"REQ_UNRESTRICT": ReqUnrestrict,
"REQ_MON_GETLIST": ReqMonGetList,
"REQ_RESET_STATS": ReqResetStats,
"REQ_RESET_PEER": ReqResetPeer,
"REQ_REREAD_KEYS": ReqRereadKeys,
"REQ_DO_DIRTY_HACK": ReqDoDirtyHack,
"REQ_DONT_DIRTY_HACK": ReqDontDirtyHack,
"REQ_TRUSTKEY": ReqTrustKey,
"REQ_UNTRUSTKEY": ReqUntrustKey,
"REQ_AUTHINFO": ReqAuthInfo,
"REQ_TRAPS": ReqTraps,
"REQ_ADD_TRAP": ReqAddTrap,
"REQ_CLR_TRAP": ReqClrTrap,
"REQ_REQUEST_KEY": ReqRequestKey,
"REQ_CONTROL_KEY": ReqControlKey,
"REQ_GET_CTLSTATS": ReqGetCtlStats,
"REQ_GET_LEAPINFO": ReqGetLeapInfo,
"REQ_GET_CLOCKINFO": ReqGetClockInfo,
"REQ_SET_CLKFUDGE": ReqSetClkFudge,
"REQ_GET_KERNEL": ReqGetKernel,
"REQ_GET_CLKBUGINFO": ReqGetClkBugInfo,
"REQ_SET_PRECISION": ReqSetPrecision,
"REQ_MON_GETLIST_1": ReqMonGetList1,
"REQ_HOSTNAME_ASSOCID": ReqHostnameAssocID,
"REQ_IF_STATS": ReqIfStats,
"REQ_IF_RELOAD": ReqIfReload,
}
var reverseRequestCodeMap map[RequestCode]string
// MarshalJSON gives the #define name, or "UNKNOWN (0x##)"
func (code RequestCode) MarshalJSON() ([]byte, error) {
if reverseRequestCodeMap == nil {
reverseRequestCodeMap = make(map[RequestCode]string)
for k, v := range requestCodeMap {
reverseRequestCodeMap[v] = k
}
}
ret, ok := reverseRequestCodeMap[code]
if !ok {
ret = fmt.Sprintf("UNKNOWN (0x%02x)", code)
}
return json.Marshal(ret)
}
// getRequestCode() returns the numeric value for the input string
// The input can either be a #define name from ntp_request.h or an integer
func getRequestCode(enum string) (RequestCode, error) {
ret, ok := requestCodeMap[enum]
if ok {
return ret, nil
}
v, err := strconv.ParseInt(enum, 0, 8)
if err != nil {
return 0, err
}
if v < 0 || v >= 0xff {
return 0, ErrInvalidRequestCode
}
return RequestCode(v), nil
}
// InfoError is a 3-bit integer, values taken from ntp_request.h
type InfoError uint8
const (
// InfoErrorOkay corresponds to the INFO_OKAY constant
InfoErrorOkay InfoError = 0
// InfoErrorImpl corresponds to the INFO_ERR_IMPL constant
InfoErrorImpl = 1
// InfoErrorReq corresponds to the INFO_ERR_REQ constant
InfoErrorReq = 2
// InfoErrorFmt corresponds to the INFO_ERR_FMT constant
InfoErrorFmt = 3
// InfoErrorNoData corresponds to the INFO_ERR_NODATA constant
InfoErrorNoData = 4
// InfoErrorUnknown5 has no corresponding constant (it is the unused value 5)
InfoErrorUnknown5 = 5
// InfoErrorUnknown6 has no corresponding constant (it is the unused value 6)
InfoErrorUnknown6 = 6
// InfoErrorAuth corresponds to the INFO_ERR_AUTH constant
InfoErrorAuth = 7
)
// These match the #define names in ntp_rqeuest.h
var infoErrorMap = map[InfoError]string{
InfoErrorOkay: "INFO_OKAY",
InfoErrorImpl: "INFO_ERR_IMPL",
InfoErrorReq: "INFO_ERR_REQ",
InfoErrorFmt: "INFO_ERR_FMT",
InfoErrorNoData: "INFO_ERR_NODATA",
InfoErrorAuth: "INFO_ERR_AUTH",
}
// isInfoError checks if err is an instance of InfoError
func isInfoError(err error) bool {
_, ok := err.(InfoError)
return ok
}
// Error implements the error interface (returns the #define name, or "UNKNOWN (0x##)")
func (err InfoError) Error() string {
ret, ok := infoErrorMap[err]
if !ok {
return fmt.Sprintf("UNKNOWN (0x%02x)", uint8(err))
}
return ret
}
// MarshalJSON gives the #define name, or "UNKNOWN (0x##)"
func (err InfoError) MarshalJSON() ([]byte, error) {
return json.Marshal(err.Error())
}
// NTPShort a 32-bit struct defined in figure 3 of RFC5905.
type NTPShort struct {
Seconds uint16 `json:"seconds"`
Fraction uint16 `json:"fraction"`
}
// Decode populates the values of this NTPShort with the first 4 bytes of buf
func (when *NTPShort) Decode(buf []byte) error {
if len(buf) < 4 {
return ErrBufferTooSmall
}
when.Seconds = binary.BigEndian.Uint16(buf[0:2])
when.Fraction = binary.BigEndian.Uint16(buf[2:4])
return nil
}
// decodeNTPShort decodes an NTPShort from the first 4 bytes of buf
func decodeNTPShort(buf []byte) (*NTPShort, error) {
if len(buf) < 4 {
return nil, ErrBufferTooSmall
}
ret := NTPShort{}
err := ret.Decode(buf)
return &ret, err
}
// Encode encodes the NTPShort according to RFC5905 -- upper 16 bits the seconds, lower 16 bits the fractional seconds (big endian)
func (when *NTPShort) Encode() []byte {
ret := make([]byte, 4)
binary.BigEndian.PutUint16(ret[0:2], when.Seconds)
binary.BigEndian.PutUint16(ret[2:4], when.Fraction)
return ret
}
// Conversion constants for going from binary fractional seconds to nanoseconds
// fraction/(1 << bits) = nanos/1e9
// nanos = fraction * 1e9 / (1 << bits)
// fraction = nanos * (1 << bits) / 1e9
const (
uint16FracToNanos float32 = float32(1e9) / float32(1<<16)
uint32FracToNanos float64 = float64(1e9) / float64(1<<32)
nanosToUint16Frac float32 = float32(1<<16) / float32(1e9)
nanosToUint32Frac float64 = float64(1<<32) / float64(1e9)
)
// GetNanos gets the number of nanoseconds represented by when.Fraction
func (when *NTPShort) GetNanos() uint32 {
return uint32(uint16FracToNanos * float32(when.Fraction))
}
// SetNanos sets when.Fraction to the binary fractional value corresponding to nanos nanoseconds
func (when *NTPShort) SetNanos(nanos int) {
when.Fraction = uint16(nanosToUint16Frac * float32(nanos))
}
// GetDuration gets the time.Duration corresponding to when
func (when *NTPShort) GetDuration() time.Duration {
return time.Duration(when.Seconds)*time.Second + time.Duration(when.GetNanos())*time.Nanosecond
}
// SetDuration sets the Seconds and Fraction to match the given duration
func (when *NTPShort) SetDuration(d time.Duration) {
ns := d.Nanoseconds()
when.Seconds = uint16(ns / 1e9)
when.SetNanos(int(ns % 1e9))
}
// NTPLong is a 64-bit fixed-length number defined in figure 3 of RFC5905
type NTPLong struct {
Seconds uint32 `json:"seconds"`
Fraction uint32 `json:"fraction"`
}
// GetNanos gets the number of nanoseconds represented by when.Fraction
func (when *NTPLong) GetNanos() uint64 {
return uint64(uint32FracToNanos * float64(when.Fraction))
}
// SetNanos sets when.Fraction to the binary fractional value corresponding to nanos nanoseconds
func (when *NTPLong) SetNanos(nanos int) {
when.Fraction = uint32(nanosToUint32Frac * float64(nanos))
}
// GetTime gets the absolute time.Time corresponding to when
func (when *NTPLong) GetTime() time.Time {
return ntpEpoch.Add(time.Duration(when.Seconds)*time.Second + time.Duration(when.GetNanos())*time.Nanosecond)
}
// SetTime sets the absolute time.Time
func (when *NTPLong) SetTime(t time.Time) {
ntpTime := t.Add(unixEpoch.Sub(ntpEpoch))
// whole seconds
s := ntpTime.Unix()
// fractional nanoseconds
ns := ntpTime.UnixNano() - s*1e9
when.Seconds = uint32(s)
when.SetNanos(int(ns))
}
// Decode populates the values of this NTPShort with the first 8 bytes of buf
func (when *NTPLong) Decode(buf []byte) error {
if len(buf) < 8 {
return ErrBufferTooSmall
}
when.Seconds = binary.BigEndian.Uint32(buf[0:4])
when.Fraction = binary.BigEndian.Uint32(buf[4:8])
return nil
}
// decodeNTPLong decodes an NTPShort from the first 8 bytes of buf
func decodeNTPLong(buf []byte) (*NTPLong, error) {
if len(buf) < 8 {
return nil, ErrBufferTooSmall
}
ret := NTPLong{}
err := ret.Decode(buf)
return &ret, err
}
// Encode encodes the NTPShort according to RFC5905 -- upper 32 bits the seconds, lower 32 bits the fractional seconds (big endian)
func (when *NTPLong) Encode() []byte {
ret := make([]byte, 8)
binary.BigEndian.PutUint32(ret[0:4], when.Seconds)
binary.BigEndian.PutUint32(ret[4:8], when.Fraction)
return ret
}
// ReferenceID is defined in RFC5905 as a 32-bit code whose interpretation depends on the stratum field
type ReferenceID [4]byte
// MarshalJSON ensures that it is marshalled like a slice, not an array
func (id ReferenceID) MarshalJSON() ([]byte, error) {
return json.Marshal(id[:])
}
// NTPHeader is defined in figure 8 of RFC5905
type NTPHeader struct {
// LeapIndicator is the the top two bits of the first byte
LeapIndicator LeapIndicator `json:"leap_indicator"`
// Version is bits 5..3 of the first byte
Version uint8 `json:"version"`
// The mode is the lowest three bits of the first byte
Mode AssociationMode `json:"mode"`
// Stratum is defined in figure 11: values > 16 are Reserved
Stratum uint8 `json:"stratum"`
// Poll: 8-bit signed integer representing the maximum interval between
// successive messages, in log2 seconds.
Poll int8 `json:"poll"`
// Precision: 8-bit signed integer representing the precision of the system clock, in log2 seconds.
Precision int8 `json:"precision"`
// Root Delay: Total round-trip delay to the reference clock
RootDelay NTPShort `json:"root_delay"`
// Root Dispersion: Total dispersion to the reference clock
RootDispersion NTPShort `json:"root_dispersion"`
// Reference ID (refid): 32-bit code identifying the particular Server or reference clock.
ReferenceID ReferenceID `json:"reference_id,omitempty"`
// Reference Timestamp: Time when the system clock was last set or corrected
ReferenceTimestamp NTPLong `json:"reference_timestamp,omitempty"`
// Origin Timestamp (org): Time at the Client when the request departed for the Server
OriginTimestamp NTPLong `json:"origin_timestamp,omitempty"`
// Receive Timestamp (rec): Time at the Server when the request arrived from the Client
ReceiveTimestamp NTPLong `json:"receive_timestamp,omitempty"`
// Transmit Timestamp (xmt): Time at the Server when the response left for the Client
TransmitTimestamp NTPLong `json:"transmit_timestamp,omitempty"`
}
// decodeNTPHeader decodes an NTP header from the first 48 bytes of buf
func decodeNTPHeader(buf []byte) (*NTPHeader, error) {
if len(buf) < 48 {
return nil, ErrBufferTooSmall
}
ret := NTPHeader{}
ret.LeapIndicator = LeapIndicator(buf[0] >> 6)
ret.Version = uint8(buf[0] >> 3 & 0x07)
ret.Mode = AssociationMode(buf[0] & 0x07)
ret.Stratum = uint8(buf[1])
ret.Poll = int8(buf[2])
ret.Precision = int8(buf[3])
if err := ret.RootDelay.Decode(buf[4:8]); err != nil {
return nil, err
}
if err := ret.RootDispersion.Decode(buf[8:12]); err != nil {
return nil, err
}
copy(ret.ReferenceID[:], buf[12:16])
if err := ret.ReferenceTimestamp.Decode(buf[16:24]); err != nil {
return nil, err
}
if err := ret.OriginTimestamp.Decode(buf[24:32]); err != nil {
return nil, err
}
if err := ret.ReceiveTimestamp.Decode(buf[32:40]); err != nil {
return nil, err
}
if err := ret.TransmitTimestamp.Decode(buf[40:48]); err != nil {
return nil, err
}
return &ret, nil
}
// readNTPHeader reads 48 bytes from conn and interprets it as an NTPHeader
func readNTPHeader(conn net.Conn) (*NTPHeader, error) {
buf := make([]byte, 48)
_, err := io.ReadFull(conn, buf)
if err != nil {
return nil, err
}
return decodeNTPHeader(buf)
}
// Encode returns the encoding of the header according to RFC5905
func (header *NTPHeader) Encode() ([]byte, error) {
ret := make([]byte, 48)
if (header.Version >> 3) != 0 {
return nil, ErrInvalidVersion
}
if (header.Mode >> 3) != 0 {
return nil, ErrInvalidMode
}
if (header.LeapIndicator >> 2) != 0 {
return nil, ErrInvalidLeapIndicator
}
ret[0] = byte((uint8(header.LeapIndicator) << 6) | (uint8(header.Mode) << 3) | uint8(header.Version))
ret[1] = byte(header.Stratum)
ret[2] = byte(header.Poll)
ret[3] = byte(header.Precision)
copy(ret[4:8], header.RootDelay.Encode())
copy(ret[8:12], header.RootDispersion.Encode())
copy(ret[12:16], header.ReferenceID[:])
copy(ret[16:24], header.ReferenceTimestamp.Encode())
copy(ret[24:32], header.OriginTimestamp.Encode())
copy(ret[32:40], header.ReceiveTimestamp.Encode())
copy(ret[40:48], header.TransmitTimestamp.Encode())
return ret[:], nil
}
// ValidateSyntax checks that the header's values are within range and make semantic sense
func (header *NTPHeader) ValidateSyntax() error {
if header.Version < 1 || header.Version > 4 {
return ErrInvalidVersion
}
if header.Mode == 0 {
return ErrInvalidMode
}
if header.Stratum > 16 {
return ErrInvalidStratum
}
if header.Stratum < 2 {
// For packet stratum 0 [the reference ID] is a four-character ASCII string
// called the "kiss code"... For stratum 1 (reference clock), this is a
// four-octet, left-justified, zero-padded ASCII string
for _, v := range header.ReferenceID {
if v >= 0x7f {
return ErrInvalidReferenceID
}
}
}
return nil
}
// PrivatePacketHeader represents a header for a mode-7 packet, roughly corresponding to struct resp_pkt in ntp_request.h
type PrivatePacketHeader struct {
IsResponse bool `json:"is_response"`
HasMore bool `json:"has_more"`
Version uint8 `json:"version"`
Mode AssociationMode `json:"mode"`
IsAuthenticated bool `json:"is_authenticated"`
SequenceNumber uint8 `json:"sequence_number"`
ImplementationNumber ImplNumber `json:"implementation_number"`
RequestCode RequestCode `json:"request_code"`
Error InfoError `json:"error"`
NumItems uint16 `json:"num_items"`
MBZ uint8 `json:"mbz"`
ItemSize uint16 `json:"item_size"`
}
// Encode encodes the packet header as a struct resp_pkt
func (header *PrivatePacketHeader) Encode() ([]byte, error) {
ret := [8]byte{}
if (header.Mode>>3) != 0 || (header.Version>>3) != 0 {
return nil, ErrInvalidHeader
}
ret[0] = uint8(header.Mode) | (header.Version << 3)
if header.IsResponse {
ret[0] = ret[0] | 0x80
}
if header.HasMore {
ret[0] = ret[0] | 0x40
}
if header.SequenceNumber&0x80 != 0 {
return nil, ErrInvalidHeader
}
ret[1] = header.SequenceNumber
if header.IsAuthenticated {
ret[1] = ret[1] | 0x80
}
ret[2] = uint8(header.ImplementationNumber)
ret[3] = uint8(header.RequestCode)
if (header.Error>>4) != 0 || (header.NumItems>>12) != 0 {
return nil, ErrInvalidHeader
}
ret[4] = (uint8(header.Error) << 4) | uint8(header.NumItems>>8)
ret[5] = byte(header.NumItems & 0xFF)
if (header.MBZ>>4) != 0 || (header.ItemSize>>12) != 0 {
return nil, ErrInvalidHeader
}
ret[6] = (header.MBZ << 4) | uint8(header.ItemSize>>8)
ret[7] = byte(header.ItemSize & 0xFF)
return ret[:], nil
}
// Decode a Private packet header from the first 8 bytes of buf
func decodePrivatePacketHeader(buf []byte) (*PrivatePacketHeader, error) {
ret := PrivatePacketHeader{}
if len(buf) < 8 {
return nil, ErrInvalidHeader
}
ret.Mode = AssociationMode(buf[0] & 0x07)
ret.Version = buf[0] >> 3 & 0x07
ret.HasMore = (buf[0]>>6)&1 == 1
ret.IsResponse = (buf[0]>>7)&1 == 1
ret.SequenceNumber = buf[1] & 0x7F
ret.IsAuthenticated = (buf[1]>>7)&1 == 1
ret.ImplementationNumber = ImplNumber(buf[2])
ret.RequestCode = RequestCode(buf[3])
ret.Error = InfoError(buf[4] >> 4)
ret.NumItems = uint16(buf[4]&0x0F)<<4 | uint16(buf[5])
ret.MBZ = buf[6] >> 4
ret.ItemSize = uint16(buf[6]&0x0f)<<4 | uint16(buf[7])
return &ret, nil
}
// Results is the struct that is returned to the zgrab2 framework from Scan()
type Results struct {
// Version is the version number returned in the get time response header.
// Absent if --skip-get-time is set.
Version *uint8 `json:"version,omitempty"`
// Time is the time returned by the server (specifically, the
// ReceiveTimestamp) in response to the get time call. Converted into a
// standard golang time.
// Absent if --skip-get-time is set.
Time *time.Time `json:"time,omitempty"`
// TimeResponse is the full header returned by the get time call.
// Absent if --skip-get-time is set. Debug only.
TimeResponse *NTPHeader `json:"time_response,omitempty" zgrab:"debug"`
// MonListResponse is the raw data returned by the call to monlist.
// Only present if --monlist is set.
MonListResponse []byte `json:"monlist_response,omitempty"`
// MonListHeader is the header returned by the call to monlist.
// Only present if --monlist is set. Debug only.
MonListHeader *PrivatePacketHeader `json:"monlist_header,omitempty" zgrab:"debug"`
}
// Flags holds the command-line flags for the scanner.
type Flags struct {
zgrab2.BaseFlags
zgrab2.UDPFlags
Verbose bool `long:"verbose" description:"More verbose logging, include debug fields in the scan results"`
Version uint8 `long:"version" description:"The version number to pass to the Server." default:"3"`
LeapIndicator uint8 `long:"leap-indicator" description:"The LI value to pass to the Server. Default 3 (Unknown)"`
SkipGetTime bool `long:"skip-get-time" description:"If set, don't request the Server time"`
MonList bool `long:"monlist" description:"Perform a ReqMonGetList request"`
RequestCode string `long:"request-code" description:"Specify a request code for MonList other than ReqMonGetList" default:"REQ_MON_GETLIST"`
}
// Module is the zgrab2 module implementation
type Module struct {
}
// Scanner holds the state for a single scan
type Scanner struct {
config *Flags
}
// RegisterModule registers the module with zgrab2
func RegisterModule() {
var module Module
_, err := zgrab2.AddCommand("ntp", "NTP", "Scan for NTP", 123, &module)
if err != nil {
log.Fatal(err)
}
}
// NewFlags returns a flags instant to be populated with the command line args
func (module *Module) NewFlags() interface{} {
return new(Flags)
}
// NewScanner returns a new NTP scanner instance
func (module *Module) NewScanner() zgrab2.Scanner {
return new(Scanner)
}
// Validate checks that the flags are valid
func (cfg *Flags) Validate(args []string) error {
return nil
}
// Help returns the module's help string
func (cfg *Flags) Help() string {
return ""
}
// Init initialized the scanner
func (scanner *Scanner) Init(flags zgrab2.ScanFlags) error {
f, _ := flags.(*Flags)
scanner.config = f
return nil
}
// InitPerSender initializes the scanner for a given sender
func (scanner *Scanner) InitPerSender(senderID int) error {
return nil
}
// Protocol returns the protocol identifer for the scanner.
func (s *Scanner) Protocol() string {
return "ntp"
}
// GetName returns the module's name
func (scanner *Scanner) GetName() string {
return scanner.config.Name
}
// GetTrigger returns the Trigger defined in the Flags.
func (scanner *Scanner) GetTrigger() string {
return scanner.config.Trigger
}
// SendAndReceive is a rough version of ntpdc.c's doquery(), except it only supports a single packet response
func (scanner *Scanner) SendAndReceive(impl ImplNumber, req RequestCode, body []byte, sock net.Conn) (*PrivatePacketHeader, []byte, error) {
outHeader, err := (&PrivatePacketHeader{
Version: scanner.config.Version,
Mode: Private,
SequenceNumber: 0x00,
ImplementationNumber: impl,
RequestCode: req,
Error: 0x00,
}).Encode()
if err != nil {
return nil, nil, err
}
outPacket := append(outHeader, body...)
n, err := sock.Write(outPacket)
if err != nil {
return nil, nil, err
}
if n != len(outPacket) {
return nil, nil, err
}
buf := make([]byte, 512)
n, err = sock.Read(buf)
if err != nil || n == 0 {
return nil, nil, err
}
if n < 8 {
log.Debugf("Returned data too small (%d bytes)", n)
return nil, nil, err
}
response := buf[0:n]
inPacket, err := decodePrivatePacketHeader(response)
if err != nil {
return inPacket, nil, err
}
// Validation logic taken from getresponse@ntpdc/ntpdc.c
// check if version is in bounds
if inPacket.Mode != Private {
log.Debugf("Received non Private-mode packet (mode=0x%02x), packet=%v", inPacket.Mode, inPacket)
return inPacket, nil, err
}
if !inPacket.IsResponse {
log.Debugf("Received non response packet (mode=0x%02x), packet=%v", inPacket.Mode, inPacket)
return inPacket, nil, err
}
if inPacket.MBZ != 0 {
log.Debugf("Received nonzero MBZ in response packet (mbz=0x%02x), packet=%v", inPacket.MBZ, inPacket)
// TODO: continue?
return inPacket, nil, err
}
if inPacket.ImplementationNumber != impl {
log.Debugf("Received mismatched implementation number in response packe (expected 0x%02x, got 0x%02x), packet=%v", impl, inPacket.ImplementationNumber, inPacket)
// TODO: continue?
return inPacket, nil, err
}
if inPacket.Error != InfoErrorOkay {
log.Debugf("Got error in non-final response packet (error=0x%02x), packet=%v", inPacket.Error, inPacket)
return inPacket, nil, inPacket.Error
}
ret := response[8:]
if len(ret) != int(inPacket.ItemSize*inPacket.NumItems) {
log.Debugf("Body length (%d) does not match record size (%d) * num records (%d)", len(ret), inPacket.ItemSize, inPacket.NumItems)
return inPacket, ret, ErrInvalidResponse
}
return inPacket, ret, nil
}
// MonList does a ReqMonGetList call to the Server and populates result with the output
func (scanner *Scanner) MonList(sock net.Conn, result *Results) (zgrab2.ScanStatus, error) {
ReqCode, err := getRequestCode(scanner.config.RequestCode)
if err != nil {
panic(err)
}
body := make([]byte, 40)
header, ret, err := scanner.SendAndReceive(ImplXNTPD, ReqCode, body, sock)
if ret != nil {
result.MonListResponse = ret
}
if header != nil {
result.MonListHeader = header
}
if err != nil {
switch {
case err == ErrInvalidResponse:
// Response packet had invalid syntax or semantics
return zgrab2.SCAN_PROTOCOL_ERROR, err
case isInfoError(err):
return zgrab2.SCAN_APPLICATION_ERROR, err
default:
return zgrab2.TryGetScanStatus(err), err
}
}
return zgrab2.SCAN_SUCCESS, err
}
// GetTime sends a "Client" packet to the Server and reads / returns the response
func (scanner *Scanner) GetTime(sock net.Conn) (*NTPHeader, error) {
outPacket := NTPHeader{}
outPacket.Mode = Client
outPacket.Version = scanner.config.Version
outPacket.LeapIndicator = LeapIndicator(scanner.config.LeapIndicator)
outPacket.Stratum = 0
encoded, err := outPacket.Encode()
if err != nil {
return nil, err
}
_, err = sock.Write(encoded)
if err != nil {
return nil, err
}
inPacket, err := readNTPHeader(sock)
if err != nil {
return nil, err
}
err = inPacket.ValidateSyntax()
if err != nil {
return inPacket, err
}
return inPacket, nil
}
// Scan scans the configured server with the settings provided by the command
// line arguments as follows:
// 1. If SkipGetTime is not set, send a GetTime packet to the server and read
// the response packet into the result.
// 2. If MonList is set, send a MONLIST packet to the server and read the
// response packet into the result.
// The presence of an NTP service at the target can be inferred by a non-nil
// result -- if the service does not return any data or if the response is not
// a valid NTP packet, then the result will be nil.
// The presence of a DDoS-amplifying target can be inferred by
// result.MonListReponse being present.
func (scanner *Scanner) Scan(t zgrab2.ScanTarget) (zgrab2.ScanStatus, interface{}, error) {
sock, err := t.OpenUDP(&scanner.config.BaseFlags, &scanner.config.UDPFlags)
if err != nil {
return zgrab2.TryGetScanStatus(err), nil, err
}
defer sock.Close()
result := &Results{}
if !scanner.config.SkipGetTime {
inPacket, err := scanner.GetTime(sock)
if err != nil {
// even if an inPacket is returned, it failed the syntax check, so indicate a failed detection via result == nil.
return zgrab2.TryGetScanStatus(err), nil, err
}
temp := inPacket.ReceiveTimestamp.GetTime()
result.TimeResponse = inPacket
result.Time = &temp
result.Version = &inPacket.Version
}
if scanner.config.MonList {
status, err := scanner.MonList(sock, result)
if err != nil {
if scanner.config.SkipGetTime {
// TODO: Currently, returning a non-nil result means that the service was positively detected.
// It may be safer to add an explicit flag for this (status == success is not sufficient, since e.g. you can get a timeout after positively identifying the service)
// This also means that partial TLS handshakes cannot be returned
return status, nil, err
}
return status, result, err
}
}
return zgrab2.SCAN_SUCCESS, result, nil
}