1037 lines
32 KiB
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
|
|
}
|
|
|
|
// GetPort returns the port that is being scanned
|
|
func (scanner *Scanner) GetPort() uint {
|
|
return scanner.config.Port
|
|
}
|
|
|
|
// 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
|
|
}
|