diff --git a/lib/smb/smb/encoder/unicode.go b/lib/smb/smb/encoder/unicode.go index f425cb3b..dd2eba1e 100644 --- a/lib/smb/smb/encoder/unicode.go +++ b/lib/smb/smb/encoder/unicode.go @@ -27,3 +27,21 @@ func ToUnicode(s string) []byte { binary.Write(&b, binary.LittleEndian, &uints) return b.Bytes() } + +func ToSmbString(s string) []byte { + res := ToUnicode(s) + res = append(res, 0x0, 0x0) + return res +} + +func FromSmbString(d []byte) (string, error) { + res, err := FromUnicode(d) + if err != nil { + return "", err + } + if len(res) == 0 { + return "", nil + } + // Trim null terminator + return res[:len(res)-1], nil +} diff --git a/lib/smb/smb/smb.go b/lib/smb/smb/smb.go index 7100a2bb..1d536518 100644 --- a/lib/smb/smb/smb.go +++ b/lib/smb/smb/smb.go @@ -95,6 +95,10 @@ const ( ShareCapAsymmetric uint32 = 0x00000080 ) +const ( + SmbHeaderV1Length = 32 +) + type HeaderV1 struct { ProtocolID []byte `smb:"fixed:4"` Command uint8 @@ -133,6 +137,24 @@ type NegotiateReqV1 struct { Dialects []uint8 `smb:"fixed:12"` } +type SessionSetupV1Req struct { + HeaderV1 + WordCount uint8 + AndCommand uint8 + Reserved1 uint8 + AndOffset uint16 + MaxBuffer uint16 + MaxMPXCount uint16 + VCNumber uint16 + SessionKey uint32 + OEMPasswordLength uint16 + UnicodePasswordLength uint16 + Reserved2 uint32 + Capabilities uint32 + ByteCount uint16 + VarData []byte +} + type NegotiateResV1 struct { HeaderV1 WordCount uint8 @@ -147,8 +169,8 @@ type NegotiateResV1 struct { SystemTime uint64 ServerTimezon uint16 ChallengeLength uint8 - ByteCount uint16 - // variable data afterwords that we don't care about + ByteCount uint16 `smb:"len:VarData"` + VarData []byte } type NegotiateReq struct { @@ -260,6 +282,17 @@ type TreeDisconnectRes struct { func newHeaderV1() HeaderV1 { return HeaderV1{ ProtocolID: []byte(ProtocolSmb), + Status: 0, + Flags: 0x18, + Flags2: 0xc843, + PIDHigh: 0, + // These bytes must be explicit here + SecurityFeatures: []byte{0, 0, 0, 0, 0, 0, 0, 0}, + Reserved: 0, + TID: 0xffff, + PIDLow: 0xfeff, + UID: 0, + MID: 0, } } @@ -287,11 +320,24 @@ func (s *Session) NewNegotiateReqV1() NegotiateReqV1 { return NegotiateReqV1{ HeaderV1: header, WordCount: 0, - ByteCount: 14, + ByteCount: 12, Dialects: []uint8(DialectSmb_1_0), } } +func (s *Session) NewSessionSetupV1Req() SessionSetupV1Req { + header := newHeaderV1() + header.Command = 0x73 // SMB1 Session Setup + return SessionSetupV1Req{ + HeaderV1: header, + WordCount: 0xd, + AndCommand: 0xff, + MaxBuffer: 0x1111, + MaxMPXCount: 0xa, + VarData: []byte{}, + } +} + func (s *Session) NewNegotiateReq() NegotiateReq { header := newHeader() header.Command = CommandNegotiate diff --git a/lib/smb/smb/zgrab.go b/lib/smb/smb/zgrab.go index ce05add3..e28291b7 100644 --- a/lib/smb/smb/zgrab.go +++ b/lib/smb/smb/zgrab.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net" + "strings" "unicode/utf16" @@ -130,6 +131,12 @@ type SMBLog struct { Version *SMBVersions `json:"smb_version,omitempty"` + // If present, represent the NativeOS, NTLM, and GroupName fields of SMBv1 Session Setup Negotiation + // An empty string for these values indicate the data was not available + NativeOs string `json:"native_os"` + NTLM string `json:"ntlm"` + GroupName string `json:"group_name"` + // While the NegotiationLogs and SessionSetupLog each have their own // Capabilties field, we are ignoring the SessionsSetupLog capability // when decoding, and only representing the server capabilties based @@ -208,7 +215,10 @@ func GetSMBLog(conn net.Conn, session bool, v1 bool, debug bool) (smbLog *SMBLog } if v1 { - err = s.LoggedNegotiateProtocolv1(session) + err := s.LoggedNegotiateProtocolv1(session) + if err == nil && session { + s.LoggedSessionSetupV1() + } } else { err = s.LoggedNegotiateProtocol(session) } @@ -269,6 +279,63 @@ func (ls *LoggedSession) LoggedNegotiateProtocolv1(setup bool) error { return nil } +func (ls *LoggedSession) LoggedSessionSetupV1() (err error) { + s := &ls.Session + var buf []byte + + req := s.NewSessionSetupV1Req() + s.Debug("Sending LoggedSessionSetupV1 Request", nil) + buf, err = s.send(req) + if err != nil { + s.Debug("No response to SMBv1 cleartext SessionSetup", nil) + return nil + } + + // Safely trim down everything except the payload + if len(buf) < SmbHeaderV1Length { + return nil + } + // When using unicode, a padding byte will exist after the header + paddingLength := int((buf[11] >> 7) & 1) + // Skip header + buf = buf[SmbHeaderV1Length:] + // The byte after the header holds the number of words remaining in uint16s + // words + 3 bytes for wordlength & bytecount + potential unicode padding + claimedRemainingSize := int(buf[0])*2 + 3 + paddingLength + if len(buf) < claimedRemainingSize { + return nil + } + buf = buf[claimedRemainingSize:] + + var decoded string + if paddingLength == 1 { + // Unicode string + decoded, err = encoder.FromSmbString(buf) + if err != nil { + s.Debug("Error encountered while decoding SMB string", err) + return nil + } + } else { + // ASCII string + decoded = string(buf) + } + + // We expect 3 null-terminated strings in this order; + // These fields are technically all optional, but guaranteed to be in this order + fields := strings.Split(decoded, "\000") + if len(fields) > 0 { + ls.Log.NativeOs = fields[0] + } + if len(fields) > 1 { + ls.Log.NTLM = fields[1] + } + if len(fields) > 2 { + ls.Log.GroupName = fields[2] + } + + return nil +} + // LoggedNegotiateProtocol performs the same operations as // Session.NegotiateProtocol() up to the point where user credentials would be // required, and logs the server's responses. diff --git a/modules/fox/scanner.go b/modules/fox/scanner.go index 910aa40c..a2ac69cb 100644 --- a/modules/fox/scanner.go +++ b/modules/fox/scanner.go @@ -6,6 +6,7 @@ package fox import ( + "errors" log "github.com/sirupsen/logrus" "github.com/zmap/zgrab2" ) @@ -107,6 +108,10 @@ func (scanner *Scanner) Scan(target zgrab2.ScanTarget) (zgrab2.ScanStatus, inter err = GetFoxBanner(result, conn) if !result.IsFox { result = nil + err = &zgrab2.ScanError{ + Err: errors.New("host responds, but is not a fox service"), + Status: zgrab2.SCAN_PROTOCOL_ERROR, + } } return zgrab2.TryGetScanStatus(err), result, err } diff --git a/modules/http/scanner.go b/modules/http/scanner.go index 346a29b5..c994014c 100644 --- a/modules/http/scanner.go +++ b/modules/http/scanner.go @@ -18,6 +18,7 @@ import ( "net" "net/url" "strconv" + "strings" "time" log "github.com/sirupsen/logrus" @@ -44,12 +45,13 @@ var ( type Flags struct { zgrab2.BaseFlags zgrab2.TLSFlags - Method string `long:"method" default:"GET" description:"Set HTTP request method type"` - Endpoint string `long:"endpoint" default:"/" description:"Send an HTTP request to an endpoint"` - UserAgent string `long:"user-agent" default:"Mozilla/5.0 zgrab/0.x" description:"Set a custom user agent"` - RetryHTTPS bool `long:"retry-https" description:"If the initial request fails, reconnect and try with HTTPS."` - MaxSize int `long:"max-size" default:"256" description:"Max kilobytes to read in response to an HTTP request"` - MaxRedirects int `long:"max-redirects" default:"0" description:"Max number of redirects to follow"` + Method string `long:"method" default:"GET" description:"Set HTTP request method type"` + Endpoint string `long:"endpoint" default:"/" description:"Send an HTTP request to an endpoint"` + FailHTTPToHTTPS bool `long:"fail-http-to-https" description:"Trigger retry-https logic on known HTTP/400 protocol mismatch responses"` + UserAgent string `long:"user-agent" default:"Mozilla/5.0 zgrab/0.x" description:"Set a custom user agent"` + RetryHTTPS bool `long:"retry-https" description:"If the initial request fails, reconnect and try with HTTPS."` + MaxSize int `long:"max-size" default:"256" description:"Max kilobytes to read in response to an HTTP request"` + MaxRedirects int `long:"max-redirects" default:"0" description:"Max number of redirects to follow"` // FollowLocalhostRedirects overrides the default behavior to return // ErrRedirLocalhost whenever a redirect points to localhost. @@ -431,7 +433,7 @@ func (scan *scan) Grab() *zgrab2.ScanError { bodyText := "" decodedSuccessfully := false decoder := encoder.NewDecoder() - + //"windows-1252" is the default value and will likely not decode correctly if certain || encoding != "windows-1252" { decoded, decErr := decoder.Bytes(buf.Bytes()) @@ -446,6 +448,23 @@ func (scan *scan) Grab() *zgrab2.ScanError { bodyText = buf.String() } + // Application-specific logic for retrying HTTP as HTTPS; if condition matches, return protocol error + if scan.scanner.config.FailHTTPToHTTPS && scan.results.Response.StatusCode == 400 && readLen < 1024 && readLen > 24 { + // Apache: "You're speaking plain HTTP to an SSL-enabled server port" + // NGINX: "The plain HTTP request was sent to HTTPS port" + var sliceLen int64 = 128 + if readLen < sliceLen { + sliceLen = readLen + } + sliceBuf := bodyText[:sliceLen] + if strings.Contains(sliceBuf, "The plain HTTP request was sent to HTTPS port") || + strings.Contains(sliceBuf, "You're speaking plain HTTP") || + strings.Contains(sliceBuf, "combination of host and port requires TLS") || + strings.Contains(sliceBuf, "Client sent an HTTP request to an HTTPS server") { + return zgrab2.NewScanError(zgrab2.SCAN_PROTOCOL_ERROR, errors.New("NGINX or Apache HTTP over HTTPS failure")) + } + } + // re-enforce readlen if int64(len(bodyText)) > readLen { scan.results.Response.BodyText = bodyText[:int(readLen)] diff --git a/modules/postgres/connection.go b/modules/postgres/connection.go index f5534995..c2634150 100644 --- a/modules/postgres/connection.go +++ b/modules/postgres/connection.go @@ -22,6 +22,8 @@ const maxOutputSize = 1024 // Don't read an unlimited number of tag/value pairs from the server const maxReadAllPackets = 64 +const uint32Len = 4 + // Connection wraps the state of a given connection to a server. type Connection struct { // Target is the requested scan target. @@ -143,7 +145,10 @@ func (c *Connection) tryReadPacket(header byte) (*ServerPacket, *zgrab2.ScanErro log.Debugf("postgres server %s reported packet size of %d bytes; only reading %d bytes.", c.Target.String(), bodyLen, maxPacketSize) sizeToRead = maxPacketSize } - body := make([]byte, sizeToRead - 4) // Length includes the length of the Length uint32 + if sizeToRead < uint32Len { + sizeToRead = uint32Len + } + body := make([]byte, sizeToRead - uint32Len) // Length includes the length of the Length uint32 _, err = io.ReadFull(c.Connection, body) if err != nil && err != io.EOF { return nil, zgrab2.DetectScanError(err) diff --git a/zgrab2_schemas/zgrab2/smb.py b/zgrab2_schemas/zgrab2/smb.py index 04ac5892..f1ac8c58 100644 --- a/zgrab2_schemas/zgrab2/smb.py +++ b/zgrab2_schemas/zgrab2/smb.py @@ -52,6 +52,9 @@ def extended(base, new): "revision": Unsigned8BitInteger(doc="Protocol Revision"), "version_string": String(doc="Full SMB Version String"), }), + "native_os": String(doc="Operating system claimed by server"), + "ntlm": String(doc="Native LAN Manager"), + "group_name": String(doc="Group name"), "smb_capabilities": SubRecord({ "smb_dfs_support": Boolean(doc="Server supports Distributed File System"), "smb_leasing_support": Boolean(doc="Server supports Leasing"), @@ -62,7 +65,7 @@ def extended(base, new): "smb_encryption_support": Boolean(doc="Server supports encryption"), }, doc="Capabilities flags for the connection. See [MS-SMB2] Sect. 2.2.4."), 'negotiation_log': negotiate_log, - 'has_ntlm': Boolean(), + 'has_ntlm': Boolean(doc="Server supports the NTLM authentication method"), 'session_setup_log': session_setup_log, }) }, extends=zgrab2.base_scan_response)