From 70314ce92bf3ec9f36f3c27e230515b207fbfcd0 Mon Sep 17 00:00:00 2001 From: Justin Bastress Date: Thu, 15 Mar 2018 16:58:57 -0400 Subject: [PATCH] Port SMB (???) scanner from ZGrab --- lib/smb/encoder/encoder.go | 490 +++++++++++++++++++++++++++++++++++++ lib/smb/encoder/unicode.go | 53 ++++ lib/smb/gss/gss.go | 136 ++++++++++ lib/smb/gss/oid.go | 42 ++++ lib/smb/log.go | 5 + lib/smb/session.go | 161 ++++++++++++ lib/smb/smb.go | 221 +++++++++++++++++ modules/smb.go | 7 + modules/smb/scanner.go | 106 ++++++++ schemas/__init__.py | 1 + schemas/smb.py | 18 ++ 11 files changed, 1240 insertions(+) create mode 100644 lib/smb/encoder/encoder.go create mode 100644 lib/smb/encoder/unicode.go create mode 100644 lib/smb/gss/gss.go create mode 100644 lib/smb/gss/oid.go create mode 100644 lib/smb/log.go create mode 100644 lib/smb/session.go create mode 100644 lib/smb/smb.go create mode 100644 modules/smb.go create mode 100644 modules/smb/scanner.go create mode 100644 schemas/smb.py diff --git a/lib/smb/encoder/encoder.go b/lib/smb/encoder/encoder.go new file mode 100644 index 0000000..1c2b7e6 --- /dev/null +++ b/lib/smb/encoder/encoder.go @@ -0,0 +1,490 @@ +/* +* MIT License +* +* Copyright (c) 2017 stacktitan +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. + */ + +package encoder + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "reflect" + "strconv" + "strings" +) + +type BinaryMarshallable interface { + MarshalBinary(*Metadata) ([]byte, error) + UnmarshalBinary([]byte, *Metadata) error +} + +type Metadata struct { + Tags *TagMap + Lens map[string]uint64 + Offsets map[string]uint64 + Parent interface{} + ParentBuf []byte + CurrOffset uint64 + CurrField string +} + +type TagMap struct { + m map[string]interface{} + has map[string]bool +} + +func (t TagMap) Has(key string) bool { + return t.has[key] +} + +func (t TagMap) Set(key string, val interface{}) { + t.m[key] = val + t.has[key] = true +} + +func (t TagMap) Get(key string) interface{} { + return t.m[key] +} + +func (t TagMap) GetInt(key string) (int, error) { + if !t.Has(key) { + return 0, errors.New("Key does not exist in tag") + } + return t.Get(key).(int), nil +} + +func (t TagMap) GetString(key string) (string, error) { + if !t.Has(key) { + return "", errors.New("Key does not exist in tag") + } + return t.Get(key).(string), nil +} + +func parseTags(sf reflect.StructField) (*TagMap, error) { + ret := &TagMap{ + m: make(map[string]interface{}), + has: make(map[string]bool), + } + tag := sf.Tag.Get("smb") + smbTags := strings.Split(tag, ",") + for _, smbTag := range smbTags { + tokens := strings.Split(smbTag, ":") + switch tokens[0] { + case "len", "offset", "count": + if len(tokens) != 2 { + return nil, errors.New("Missing required tag data. Expecting key:val") + } + ret.Set(tokens[0], tokens[1]) + case "fixed": + if len(tokens) != 2 { + return nil, errors.New("Missing required tag data. Expecting key:val") + } + i, err := strconv.Atoi(tokens[1]) + if err != nil { + return nil, err + } + ret.Set(tokens[0], i) + case "asn1": + ret.Set(tokens[0], true) + } + } + + return ret, nil +} + +func getOffsetByFieldName(fieldName string, meta *Metadata) (uint64, error) { + if meta == nil || meta.Tags == nil || meta.Parent == nil || meta.Lens == nil { + return 0, errors.New("Cannot determine field offset. Missing required metadata") + } + var ret uint64 + var found bool + parentvf := reflect.Indirect(reflect.ValueOf(meta.Parent)) + // To determine offset, we loop through all fields of the struct, summing lengths of previous elements + // until we reach our field + for i := 0; i < parentvf.NumField(); i++ { + tf := parentvf.Type().Field(i) + if tf.Name == fieldName { + found = true + break + } + if l, ok := meta.Lens[tf.Name]; ok { + // Length of field is in cache + ret += l + } else { + // Not in cache. Must marshal field to determine length. Add to cache after + buf, err := Marshal(parentvf.Field(i).Interface()) + if err != nil { + return 0, err + } + l := uint64(len(buf)) + meta.Lens[tf.Name] = l + ret += l + } + } + if !found { + return 0, errors.New("Cannot find field name within struct: " + fieldName) + } + return ret, nil +} + +func getFieldLengthByName(fieldName string, meta *Metadata) (uint64, error) { + var ret uint64 + if meta == nil || meta.Tags == nil || meta.Parent == nil || meta.Lens == nil { + return 0, errors.New("Cannot determine field length. Missing required metadata") + } + + // Check if length is stored in field length cache + if val, ok := meta.Lens[fieldName]; ok { + return uint64(val), nil + } + + parentvf := reflect.Indirect(reflect.ValueOf(meta.Parent)) + + field := parentvf.FieldByName(fieldName) + if !field.IsValid() { + return 0, errors.New("Invalid field. Cannot determine length.") + } + + bm, ok := field.Interface().(BinaryMarshallable) + if ok { + // Custom marshallable interface found. + buf, err := bm.(BinaryMarshallable).MarshalBinary(meta) + if err != nil { + return 0, err + } + return uint64(len(buf)), nil + } + + if field.Kind() == reflect.Ptr { + field = field.Elem() + } + + switch field.Kind() { + case reflect.Struct: + buf, err := Marshal(field.Interface()) + if err != nil { + return 0, err + } + ret = uint64(len(buf)) + case reflect.Interface: + return 0, errors.New("Interface length calculation not implemented") + case reflect.Slice, reflect.Array: + switch field.Type().Elem().Kind() { + case reflect.Uint8: + ret = uint64(len(field.Interface().([]byte))) + default: + return 0, errors.New("Cannot calculate the length of unknown slice type for " + fieldName) + } + case reflect.Uint8: + ret = uint64(binary.Size(field.Interface().(uint8))) + case reflect.Uint16: + ret = uint64(binary.Size(field.Interface().(uint16))) + case reflect.Uint32: + ret = uint64(binary.Size(field.Interface().(uint32))) + case reflect.Uint64: + ret = uint64(binary.Size(field.Interface().(uint64))) + default: + return 0, errors.New("Cannot calculate the length of unknown kind for field " + fieldName) + } + meta.Lens[fieldName] = ret + return ret, nil +} + +func Marshal(v interface{}) ([]byte, error) { + return marshal(v, nil) +} + +func marshal(v interface{}, meta *Metadata) ([]byte, error) { + var ret []byte + tf := reflect.TypeOf(v) + vf := reflect.ValueOf(v) + + bm, ok := v.(BinaryMarshallable) + if ok { + // Custom marshallable interface found. + buf, err := bm.MarshalBinary(meta) + if err != nil { + return nil, err + } + return buf, nil + } + + if tf.Kind() == reflect.Ptr { + vf = reflect.Indirect(reflect.ValueOf(v)) + tf = vf.Type() + } + + w := bytes.NewBuffer(ret) + switch tf.Kind() { + case reflect.Struct: + m := &Metadata{ + Tags: &TagMap{}, + Lens: make(map[string]uint64), + Parent: v, + } + for j := 0; j < vf.NumField(); j++ { + tags, err := parseTags(tf.Field(j)) + if err != nil { + return nil, err + } + m.Tags = tags + buf, err := marshal(vf.Field(j).Interface(), m) + if err != nil { + return nil, err + } + m.Lens[tf.Field(j).Name] = uint64(len(buf)) + if err := binary.Write(w, binary.LittleEndian, buf); err != nil { + return nil, err + } + } + case reflect.Slice, reflect.Array: + switch tf.Elem().Kind() { + case reflect.Uint8: + if err := binary.Write(w, binary.LittleEndian, v.([]uint8)); err != nil { + return nil, err + } + case reflect.Uint16: + if err := binary.Write(w, binary.LittleEndian, v.([]uint16)); err != nil { + return nil, err + } + } + case reflect.Uint8: + if err := binary.Write(w, binary.LittleEndian, vf.Interface().(uint8)); err != nil { + return nil, err + } + case reflect.Uint16: + data := vf.Interface().(uint16) + if meta != nil && meta.Tags.Has("len") { + fieldName, err := meta.Tags.GetString("len") + if err != nil { + return nil, err + } + l, err := getFieldLengthByName(fieldName, meta) + if err != nil { + return nil, err + } + data = uint16(l) + } + if meta != nil && meta.Tags.Has("offset") { + fieldName, err := meta.Tags.GetString("offset") + if err != nil { + return nil, err + } + l, err := getOffsetByFieldName(fieldName, meta) + if err != nil { + return nil, err + } + data = uint16(l) + } + if err := binary.Write(w, binary.LittleEndian, data); err != nil { + return nil, err + } + case reflect.Uint32: + data := vf.Interface().(uint32) + if meta != nil && meta.Tags.Has("len") { + fieldName, err := meta.Tags.GetString("len") + if err != nil { + return nil, err + } + l, err := getFieldLengthByName(fieldName, meta) + if err != nil { + return nil, err + } + data = uint32(l) + } + if meta != nil && meta.Tags.Has("offset") { + fieldName, err := meta.Tags.GetString("offset") + if err != nil { + return nil, err + } + l, err := getOffsetByFieldName(fieldName, meta) + if err != nil { + return nil, err + } + data = uint32(l) + } + if err := binary.Write(w, binary.LittleEndian, data); err != nil { + return nil, err + } + case reflect.Uint64: + if err := binary.Write(w, binary.LittleEndian, vf.Interface().(uint64)); err != nil { + return nil, err + } + default: + return nil, errors.New(fmt.Sprintf("Marshal not implemented for kind: %s", tf.Kind())) + } + return w.Bytes(), nil +} + +func unmarshal(buf []byte, v interface{}, meta *Metadata) (interface{}, error) { + tf := reflect.TypeOf(v) + vf := reflect.ValueOf(v) + + bm, ok := v.(BinaryMarshallable) + if ok { + // Custom marshallable interface found. + if err := bm.UnmarshalBinary(buf, meta); err != nil { + return nil, err + } + return bm, nil + } + + if tf.Kind() == reflect.Ptr { + vf = reflect.ValueOf(v).Elem() + tf = vf.Type() + } + + if meta == nil { + meta = &Metadata{ + Tags: &TagMap{}, + Lens: make(map[string]uint64), + Parent: v, + ParentBuf: buf, + Offsets: make(map[string]uint64), + CurrOffset: 0, + } + } + + r := bytes.NewBuffer(buf) + switch tf.Kind() { + case reflect.Struct: + m := &Metadata{ + Tags: &TagMap{}, + Lens: make(map[string]uint64), + Parent: v, + ParentBuf: buf, + Offsets: make(map[string]uint64), + CurrOffset: 0, + } + for i := 0; i < tf.NumField(); i++ { + m.CurrField = tf.Field(i).Name + tags, err := parseTags(tf.Field(i)) + if err != nil { + return nil, err + } + m.Tags = tags + var data interface{} + switch tf.Field(i).Type.Kind() { + case reflect.Struct: + data, err = unmarshal(buf[m.CurrOffset:], vf.Field(i).Addr().Interface(), m) + default: + data, err = unmarshal(buf[m.CurrOffset:], vf.Field(i).Interface(), m) + } + if err != nil { + return nil, err + } + vf.Field(i).Set(reflect.ValueOf(data)) + } + v = reflect.Indirect(reflect.ValueOf(v)).Interface() + meta.CurrOffset += m.CurrOffset + return v, nil + case reflect.Uint8: + var ret uint8 + if err := binary.Read(r, binary.LittleEndian, &ret); err != nil { + return nil, err + } + meta.CurrOffset += uint64(binary.Size(ret)) + return ret, nil + case reflect.Uint16: + var ret uint16 + if err := binary.Read(r, binary.LittleEndian, &ret); err != nil { + return nil, err + } + if meta.Tags.Has("len") { + ref, err := meta.Tags.GetString("len") + if err != nil { + return nil, err + } + meta.Lens[ref] = uint64(ret) + } + meta.CurrOffset += uint64(binary.Size(ret)) + return ret, nil + case reflect.Uint32: + var ret uint32 + if err := binary.Read(r, binary.LittleEndian, &ret); err != nil { + return nil, err + } + if meta.Tags.Has("offset") { + ref, err := meta.Tags.GetString("offset") + if err != nil { + return nil, err + } + meta.Offsets[ref] = uint64(ret) + } + meta.CurrOffset += uint64(binary.Size(ret)) + return ret, nil + case reflect.Uint64: + var ret uint64 + if err := binary.Read(r, binary.LittleEndian, &ret); err != nil { + return nil, err + } + meta.CurrOffset += uint64(binary.Size(ret)) + return ret, nil + case reflect.Slice, reflect.Array: + switch tf.Elem().Kind() { + case reflect.Uint8: + var l, o int + var err error + if meta.Tags.Has("fixed") { + if l, err = meta.Tags.GetInt("fixed"); err != nil { + return nil, err + } + // Fixed length fields advance current offset + meta.CurrOffset += uint64(l) + } else { + if val, ok := meta.Lens[meta.CurrField]; ok { + l = int(val) + } else { + return nil, errors.New("Variable length field missing length reference in struct: " + meta.CurrField) + } + if val, ok := meta.Offsets[meta.CurrField]; ok { + o = int(val) + } else { + // No offset found in map. Use current offset + o = int(meta.CurrOffset) + } + // Variable length data is relative to parent/outer struct. Reset reader to point to beginning of data + r = bytes.NewBuffer(meta.ParentBuf[o : o+l]) + // Variable length data fields do NOT advance current offset. + } + data := make([]byte, l) + if err := binary.Read(r, binary.LittleEndian, &data); err != nil { + return nil, err + } + return data, nil + case reflect.Uint16: + return errors.New("Unmarshal not implemented for slice kind:" + tf.Kind().String()), nil + } + default: + return errors.New("Unmarshal not implemented for kind:" + tf.Kind().String()), nil + } + + return nil, nil + +} + +func Unmarshal(buf []byte, v interface{}) error { + _, err := unmarshal(buf, v, nil) + return err +} diff --git a/lib/smb/encoder/unicode.go b/lib/smb/encoder/unicode.go new file mode 100644 index 0000000..e14d9e3 --- /dev/null +++ b/lib/smb/encoder/unicode.go @@ -0,0 +1,53 @@ +/* +* MIT License +* +* Copyright (c) 2017 stacktitan +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. + */ + +package encoder + +import ( + "bytes" + "encoding/binary" + "errors" + "unicode/utf16" +) + +func FromUnicode(d []byte) (string, error) { + // Credit to https://github.com/Azure/go-ntlmssp/blob/master/unicode.go for logic + if len(d)%2 > 0 { + return "", errors.New("Unicode (UTF 16 LE) specified, but uneven data length") + } + s := make([]uint16, len(d)/2) + err := binary.Read(bytes.NewReader(d), binary.LittleEndian, &s) + if err != nil { + return "", err + } + return string(utf16.Decode(s)), nil +} + +func ToUnicode(s string) []byte { + // Credit to https://github.com/Azure/go-ntlmssp/blob/master/unicode.go for logic + uints := utf16.Encode([]rune(s)) + b := bytes.Buffer{} + binary.Write(&b, binary.LittleEndian, &uints) + return b.Bytes() +} diff --git a/lib/smb/gss/gss.go b/lib/smb/gss/gss.go new file mode 100644 index 0000000..2499de1 --- /dev/null +++ b/lib/smb/gss/gss.go @@ -0,0 +1,136 @@ +/* +* MIT License +* +* Copyright (c) 2017 stacktitan +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. + */ + +package gss + +import ( + "encoding/asn1" + "log" + + "github.com/zmap/zgrab2/lib/smb/encoder" +) + +const SpnegoOid = "1.3.6.1.5.5.2" +const NtLmSSPMechTypeOid = "1.3.6.1.4.1.311.2.2.10" + +const GssStateAcceptCompleted = 0 +const GssStateAcceptIncomplete = 1 +const GssStateReject = 2 +const GssStateRequestMic = 3 + +type NegTokenInitData struct { + MechTypes []asn1.ObjectIdentifier `asn1:"explicit,tag:0"` + ReqFlags asn1.BitString `asn1:"explicit,optional,omitempty,tag:1"` + MechToken []byte `asn1:"explicit,optional,omitempty,tag:2"` + MechTokenMIC []byte `asn1:"explicit,optional,omitempty,tag:3"` +} + +type NegTokenInit struct { + OID asn1.ObjectIdentifier + Data NegTokenInitData `asn1:"explicit"` +} + +type NegTokenResp struct { + State asn1.Enumerated `asn1:"explicit,optional,omitempty,tag:0"` + SupportedMech asn1.ObjectIdentifier `asn1:"explicit,optional,omitempty,tag:1"` + ResponseToken []byte `asn1:"explicit,optional,omitempty,tag:2"` + MechListMIC []byte `asn1:"explicit,optional,omitempty,tag:3"` +} + +// gsswrapped used to force ASN1 encoding to include explicit sequence tags +// Type does not fulfill the BinaryMarshallable interface and is used only as a +// helper to marshal a NegTokenResp +type gsswrapped struct{ G interface{} } + +func NewNegTokenInit() (NegTokenInit, error) { + oid, err := ObjectIDStrToInt(SpnegoOid) + if err != nil { + return NegTokenInit{}, err + } + ntlmoid, err := ObjectIDStrToInt(NtLmSSPMechTypeOid) + if err != nil { + return NegTokenInit{}, err + } + return NegTokenInit{ + OID: oid, + Data: NegTokenInitData{ + MechTypes: []asn1.ObjectIdentifier{ntlmoid}, + ReqFlags: asn1.BitString{}, + MechToken: []byte{}, + MechTokenMIC: []byte{}, + }, + }, nil +} + +func NewNegTokenResp() (NegTokenResp, error) { + return NegTokenResp{}, nil +} + +func (n *NegTokenInit) MarshalBinary(meta *encoder.Metadata) ([]byte, error) { + buf, err := asn1.Marshal(*n) + if err != nil { + log.Panicln(err) + return nil, err + } + + // When marshalling struct, asn1 uses 30 (sequence) tag by default. + // Override to set 60 (application) to remain consistent with GSS/SMB + buf[0] = 0x60 + return buf, nil +} + +func (n *NegTokenInit) UnmarshalBinary(buf []byte, meta *encoder.Metadata) error { + data := NegTokenInit{} + if _, err := asn1.UnmarshalWithParams(buf, &data, "application"); err != nil { + return err + } + *n = data + return nil +} + +func (r *NegTokenResp) MarshalBinary(meta *encoder.Metadata) ([]byte, error) { + // Oddities in Go's ASN1 package vs SMB encoding mean we have to wrap our + // struct in another struct to ensure proper tags and lengths are added + // to encoded data + wrapped := &gsswrapped{*r} + return wrapped.MarshalBinary(meta) +} + +func (r *NegTokenResp) UnmarshalBinary(buf []byte, meta *encoder.Metadata) error { + data := NegTokenResp{} + if _, err := asn1.UnmarshalWithParams(buf, &data, "explicit,tag:1"); err != nil { + return err + } + *r = data + return nil +} + +func (g *gsswrapped) MarshalBinary(meta *encoder.Metadata) ([]byte, error) { + buf, err := asn1.Marshal(*g) + if err != nil { + return nil, err + } + buf[0] = 0xa1 + return buf, nil +} diff --git a/lib/smb/gss/oid.go b/lib/smb/gss/oid.go new file mode 100644 index 0000000..9620740 --- /dev/null +++ b/lib/smb/gss/oid.go @@ -0,0 +1,42 @@ +/* +* MIT License +* +* Copyright (c) 2017 stacktitan +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. + */ +package gss + +import ( + "strconv" + "strings" +) + +func ObjectIDStrToInt(oid string) ([]int, error) { + ret := []int{} + tokens := strings.Split(oid, ".") + for _, token := range tokens { + i, err := strconv.Atoi(token) + if err != nil { + return nil, err + } + ret = append(ret, i) + } + return ret, nil +} diff --git a/lib/smb/log.go b/lib/smb/log.go new file mode 100644 index 0000000..6adbd40 --- /dev/null +++ b/lib/smb/log.go @@ -0,0 +1,5 @@ +package smb + +type SMBLog struct { + SupportV1 bool `json:"smbv1_support"` +} diff --git a/lib/smb/session.go b/lib/smb/session.go new file mode 100644 index 0000000..e78abdc --- /dev/null +++ b/lib/smb/session.go @@ -0,0 +1,161 @@ +/* +* MIT License +* +* Copyright (c) 2017 stacktitan +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. + */ + +package smb + +import ( + "bufio" + "bytes" + "encoding/binary" + "encoding/hex" + "errors" + "io" + "log" + "net" + + "github.com/zmap/zgrab2/lib/smb/encoder" +) + +type Session struct { + IsSigningRequired bool + IsAuthenticated bool + debug bool + securityMode uint16 + messageID uint64 + sessionID uint64 + conn net.Conn + dialect uint16 + options Options + trees map[string]uint32 +} + +type Options struct { + Host string + Port int + Workstation string + Domain string + User string + Password string + Hash string +} + +func GetSMBBanner(logStruct *SMBLog, conn net.Conn) (err error) { + opt := Options{} + + s := &Session{ + IsSigningRequired: false, + IsAuthenticated: false, + debug: false, + securityMode: 0, + messageID: 0, + sessionID: 0, + dialect: 0, + conn: conn, + options: opt, + trees: make(map[string]uint32), + } + + err = s.NegotiateProtocol(logStruct) + return err + +} + +func (s *Session) NegotiateProtocol(logStruct *SMBLog) error { + negReq := s.NewNegotiateReq() + buf, err := s.send(negReq, logStruct) + if err != nil { + return err + } + + negRes := NewNegotiateRes() + if err := encoder.Unmarshal(buf, &negRes); err != nil { + s.Debug("Raw:\n"+hex.Dump(buf), err) + } + + if negRes.Header.Status != StatusOk { + return nil + } + + s.conn.Close() + return nil +} + +func (s *Session) send(req interface{}, logStruct *SMBLog) (res []byte, err error) { + buf, err := encoder.Marshal(req) + if err != nil { + s.Debug("", err) + return nil, err + } + + b := new(bytes.Buffer) + if err = binary.Write(b, binary.BigEndian, uint32(len(buf))); err != nil { + s.Debug("", err) + return + } + + rw := bufio.NewReadWriter(bufio.NewReader(s.conn), bufio.NewWriter(s.conn)) + if _, err = rw.Write(append(b.Bytes(), buf...)); err != nil { + s.Debug("", err) + return + } + rw.Flush() + + var size uint32 + if err = binary.Read(rw, binary.BigEndian, &size); err != nil { + s.Debug("", err) + return + } + if size > 0x00FFFFFF { + return nil, errors.New("invalid NetBIOS Session message") + } + + data := make([]byte, size) + l, err := io.ReadFull(rw, data) + if err != nil { + s.Debug("", err) + return nil, err + } + + if uint32(l) != size || len(data) < 4 { + return nil, errors.New("message size invalid") + } + + //First four bytes contain protocol ID + protID := data[0:4] + switch string(protID) { + default: + return nil, errors.New("protocol not implemented") + case ProtocolSmb: + logStruct.SupportV1 = true + } + + s.messageID++ + return data, nil +} + +func (s *Session) Debug(msg string, err error) { + if s.debug { + log.Println("[ DEBUG ] ", msg) + } +} diff --git a/lib/smb/smb.go b/lib/smb/smb.go new file mode 100644 index 0000000..50c40e9 --- /dev/null +++ b/lib/smb/smb.go @@ -0,0 +1,221 @@ +/* +* MIT License +* +* Copyright (c) 2017 stacktitan +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. + */ + +package smb + +import ( + "github.com/zmap/zgrab2/lib/smb/gss" +) + +const ProtocolSmb = "\xFFSMB" +const ProtocolSmb2 = "\xFESMB" + +const StatusOk = 0x00000000 +const StatusMoreProcessingRequired = 0xc0000016 +const StatusInvalidParameter = 0xc000000d +const StatusLogonFailure = 0xc000006d +const StatusUserSessionDeleted = 0xc0000203 + +var StatusMap = map[uint32]string{ + StatusOk: "OK", + StatusMoreProcessingRequired: "More Processing Required", + StatusInvalidParameter: "Invalid Parameter", + StatusLogonFailure: "Logon failed", + StatusUserSessionDeleted: "User session deleted", +} + +const DialectSmb_2_0_2 = 0x0202 +const DialectSmb_2_1 = 0x0210 +const DialectSmb_3_0 = 0x0300 +const DialectSmb_3_0_2 = 0x0302 +const DialectSmb_3_1_1 = 0x0311 +const DialectSmb2_ALL = 0x02FF + +const ( + CommandNegotiate uint16 = iota + CommandSessionSetup + CommandLogoff + CommandTreeConnect + CommandTreeDisconnect + CommandCreate + CommandClose + CommandFlush + CommandRead + CommandWrite + CommandLock + CommandIOCtl + CommandCancel + CommandEcho + CommandQueryDirectory + CommandChangeNotify + CommandQueryInfo + CommandSetInfo + CommandOplockBreak +) + +const ( + _ uint16 = iota + SecurityModeSigningEnabled + SecurityModeSigningRequired +) + +const ( + _ byte = iota + ShareTypeDisk + ShareTypePipe + ShareTypePrint +) + +const ( + ShareFlagManualCaching uint32 = 0x00000000 + ShareFlagAutoCaching uint32 = 0x00000010 + ShareFlagVDOCaching uint32 = 0x00000020 + ShareFlagNoCaching uint32 = 0x00000030 + ShareFlagDFS uint32 = 0x00000001 + ShareFlagDFSRoot uint32 = 0x00000002 + ShareFlagRestriceExclusiveOpens uint32 = 0x00000100 + ShareFlagForceSharedDelete uint32 = 0x00000200 + ShareFlagAllowNamespaceCaching uint32 = 0x00000400 + ShareFlagAccessBasedDirectoryEnum uint32 = 0x00000800 + ShareFlagForceLevelIIOplock uint32 = 0x00001000 + ShareFlagEnableHashV1 uint32 = 0x00002000 + ShareFlagEnableHashV2 uint32 = 0x00004000 + ShareFlagEncryptData uint32 = 0x00008000 +) + +const ( + ShareCapDFS uint32 = 0x00000008 + ShareCapContinuousAvailability uint32 = 0x00000010 + ShareCapScaleout uint32 = 0x00000020 + ShareCapCluster uint32 = 0x00000040 + ShareCapAsymmetric uint32 = 0x00000080 +) + +type Header struct { + ProtocolID []byte `smb:"fixed:4"` + StructureSize uint16 + CreditCharge uint16 + Status uint32 + Command uint16 + Credits uint16 + Flags uint32 + NextCommand uint32 + MessageID uint64 + Reserved uint32 + TreeID uint32 + SessionID uint64 + Signature []byte `smb:"fixed:16"` +} + +type NegotiateReq struct { + Header + StructureSize uint16 + DialectCount uint16 `smb:"count:Dialects"` + SecurityMode uint16 + Reserved uint16 + Capabilities uint32 + ClientGuid []byte `smb:"fixed:16"` + ClientStartTime uint64 + Dialects []uint16 +} + +type NegotiateRes struct { + Header + StructureSize uint16 + SecurityMode uint16 + DialectRevision uint16 + Reserved uint16 + ServerGuid []byte `smb:"fixed:16"` + Capabilities uint32 + MaxTransactSize uint32 + MaxReadSize uint32 + MaxWriteSize uint32 + SystemTime uint64 + ServerStartTime uint64 + SecurityBufferOffset uint16 `smb:"offset:SecurityBlob"` + SecurityBufferLength uint16 `smb:"len:SecurityBlob"` + Reserved2 uint32 + SecurityBlob *gss.NegTokenInit +} + +func newHeader() Header { + return Header{ + ProtocolID: []byte(ProtocolSmb), + StructureSize: 64, + CreditCharge: 0, + Status: 0, + Command: 0, + Credits: 0, + Flags: 0, + NextCommand: 0, + MessageID: 0, + Reserved: 0, + TreeID: 0, + SessionID: 0, + Signature: make([]byte, 16), + } +} + +func (s *Session) NewNegotiateReq() NegotiateReq { + header := newHeader() + header.Command = CommandNegotiate + header.CreditCharge = 1 + header.MessageID = s.messageID + + dialects := []uint16{ + uint16(DialectSmb_2_1), + } + return NegotiateReq{ + Header: header, + StructureSize: 36, + DialectCount: uint16(len(dialects)), + SecurityMode: SecurityModeSigningEnabled, + Reserved: 0, + Capabilities: 0, + ClientGuid: make([]byte, 16), + ClientStartTime: 0, + Dialects: dialects, + } +} + +func NewNegotiateRes() NegotiateRes { + return NegotiateRes{ + Header: newHeader(), + StructureSize: 0, + SecurityMode: 0, + DialectRevision: 0, + Reserved: 0, + ServerGuid: make([]byte, 16), + Capabilities: 0, + MaxTransactSize: 0, + MaxReadSize: 0, + MaxWriteSize: 0, + SystemTime: 0, + ServerStartTime: 0, + SecurityBufferOffset: 0, + SecurityBufferLength: 0, + Reserved2: 0, + SecurityBlob: &gss.NegTokenInit{}, + } +} diff --git a/modules/smb.go b/modules/smb.go new file mode 100644 index 0000000..6fd60af --- /dev/null +++ b/modules/smb.go @@ -0,0 +1,7 @@ +package modules + +import "github.com/zmap/zgrab2/modules/smb" + +func init() { + smb.RegisterModule() +} diff --git a/modules/smb/scanner.go b/modules/smb/scanner.go new file mode 100644 index 0000000..a7ed859 --- /dev/null +++ b/modules/smb/scanner.go @@ -0,0 +1,106 @@ +// Package smb provides a zgrab2 module that scans for smb. +// This was ported directly from zgrab. +package smb + +import ( + log "github.com/sirupsen/logrus" + "github.com/zmap/zgrab2" + "github.com/zmap/zgrab2/lib/smb" +) + +// ScanResults instances are returned by the module's Scan function. +type ScanResults struct { + smb.SMBLog +} + +// Flags holds the command-line configuration for the smb scan module. +// Populated by the framework. +type Flags struct { + zgrab2.BaseFlags + Verbose bool `long:"verbose" description:"More verbose logging, include debug fields in the scan results"` +} + +// Module implements the zgrab2.Module interface. +type Module struct { +} + +// Scanner implements the zgrab2.Scanner interface. +type Scanner struct { + config *Flags +} + +// RegisterModule registers the zgrab2 module. +func RegisterModule() { + var module Module + _, err := zgrab2.AddCommand("smb", "smb", "Probe for smb", 445, &module) + if err != nil { + log.Fatal(err) + } +} + +// NewFlags returns a default Flags object. +func (module *Module) NewFlags() interface{} { + return new(Flags) +} + +// NewScanner returns a new Scanner instance. +func (module *Module) NewScanner() zgrab2.Scanner { + return new(Scanner) +} + +// Validate checks that the flags are valid. +// On success, returns nil. +// On failure, returns an error instance describing the error. +func (flags *Flags) Validate(args []string) error { + return nil +} + +// Help returns the module's help string. +func (flags *Flags) Help() string { + return "" +} + +// Init initializes 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 +} + +// GetName returns the Scanner name defined in the Flags. +func (scanner *Scanner) GetName() string { + return scanner.config.Name +} + +// Protocol returns the protocol identifier of the scan. +func (scanner *Scanner) Protocol() string { + return "smb" +} + +// GetPort returns the port being scanned. +func (scanner *Scanner) GetPort() uint { + return scanner.config.Port +} + +// Scan performs the following: +// 1. Connect to the TCP port (default 445). +// 2. Call smb.GetSMBBanner() on the connection +// 3. Return the result. +func (scanner *Scanner) Scan(target zgrab2.ScanTarget) (zgrab2.ScanStatus, interface{}, error) { + conn, err:= target.Open(&scanner.config.BaseFlags) + if err != nil { + return zgrab2.TryGetScanStatus(err), nil, err + } + smbLog := smb.SMBLog{} + + if err := smb.GetSMBBanner(&smbLog, conn); err != nil { + return zgrab2.TryGetScanStatus(err), nil, err + } + ret := ScanResults{SMBLog: smbLog} + return zgrab2.SCAN_SUCCESS, ret, nil +} diff --git a/schemas/__init__.py b/schemas/__init__.py index f7dc9f9..1b1a491 100644 --- a/schemas/__init__.py +++ b/schemas/__init__.py @@ -10,3 +10,4 @@ import schemas.redis import schemas.smtp import schemas.telnet import schemas.pop3 +import schemas.smb diff --git a/schemas/smb.py b/schemas/smb.py new file mode 100644 index 0000000..5ff7732 --- /dev/null +++ b/schemas/smb.py @@ -0,0 +1,18 @@ +# zschema sub-schema for zgrab2's smb module +# Registers zgrab2-smb globally, and smb with the main zgrab2 schema. +from zschema.leaves import * +from zschema.compounds import * +import zschema.registry + +import schemas.zcrypto as zcrypto +import schemas.zgrab2 as zgrab2 + +smb_scan_response = SubRecord({ + "result": SubRecord({ + "smbv1_support": Boolean(), + }) +}, extends=zgrab2.base_scan_response) + +zschema.registry.register_schema("zgrab2-smb", smb_scan_response) + +zgrab2.register_scan_response_type("smb", smb_scan_response)