// 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 }