diff --git a/modules/net_sniff_ntlm.go b/modules/net_sniff_ntlm.go index e67c1061..f72db8f0 100644 --- a/modules/net_sniff_ntlm.go +++ b/modules/net_sniff_ntlm.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/evilsocket/bettercap-ng/core" + "github.com/evilsocket/bettercap-ng/packets" "github.com/google/gopacket" "github.com/google/gopacket/layers" @@ -14,6 +15,7 @@ var ( ntlmRe = regexp.MustCompile("(WWW-|Proxy-|)(Authenticate|Authorization): (NTLM|Negotiate)") challRe = regexp.MustCompile("(WWW-|Proxy-|)(Authenticate): (NTLM|Negotiate)") respRe = regexp.MustCompile("(WWW-|Proxy-|)(Authorization): (NTLM|Negotiate)") + ntlm = packets.NewNTLMState() ) func isNtlm(s string) bool { @@ -36,27 +38,26 @@ func ntlmParser(ip *layers.IPv4, pkt gopacket.Packet, tcp *layers.TCP) bool { if len(tokens) != 3 { continue } - what := "?" if isChallenge(line) { - what = "challenge" + ntlm.AddServerResponse(tcp.Ack, tokens[2]) } else if isResponse(line) { - what = "response" + ntlm.AddClientResponse(tcp.Seq, tokens[2], func(data packets.NTLMChallengeResponseParsed) { + NewSnifferEvent( + pkt.Metadata().Timestamp, + "ntlm.response", + ip.SrcIP.String(), + ip.DstIP.String(), + SniffData{ + "data": data, + }, + "%s %s > %s | %s", + core.W(core.BG_DGRAY+core.FG_WHITE, "ntlm.response"), + vIP(ip.SrcIP), + vIP(ip.DstIP), + data.LcString(), + ).Push() + }) } - - NewSnifferEvent( - pkt.Metadata().Timestamp, - "ntlm."+what, - ip.SrcIP.String(), - ip.DstIP.String(), - SniffData{ - what: tokens[2], - }, - "%s %s > %s | %s", - core.W(core.BG_DGRAY+core.FG_WHITE, "ntlm."+what), - vIP(ip.SrcIP), - vIP(ip.DstIP), - tokens[2], - ).Push() } } return true diff --git a/packets/krb5.go b/packets/krb5.go index 5d0a6599..295f4ced 100644 --- a/packets/krb5.go +++ b/packets/krb5.go @@ -2,6 +2,7 @@ package packets import ( "errors" + "fmt" "strconv" "time" @@ -17,8 +18,11 @@ const ( Krb5CryptRc4Hmac = 23 ) -//https://github.com/heimdal/heimdal/blob/master/lib/asn1/krb5.asn1 var ( + ErrNoCrypt = errors.New("No crypt alg found") + ErrReqData = errors.New("Failed to extract pnData from as-req") + ErrNoCipher = errors.New("No encryption type or cipher found") + Krb5AsReqParam = "application,explicit,tag:10" ) @@ -74,37 +78,37 @@ type Krb5Request struct { func (kdc Krb5Request) String() (string, error) { var eType, cipher string - var crypt []string - realm := kdc.ReqBody.Realm - if kdc.ReqBody.Cname.NameType == Krb5Krb5PrincipalNameType { - crypt = kdc.ReqBody.Cname.NameString - } - if len(crypt) != 1 { - return "", errors.New("No crypt alg found") + if kdc.ReqBody.Cname.NameType != Krb5Krb5PrincipalNameType { + return "", ErrNoCrypt } + + realm := kdc.ReqBody.Realm + crypt := kdc.ReqBody.Cname.NameString + for _, pn := range kdc.Krb5PnData { if pn.Krb5PnDataType == 2 { enc, err := pn.getParsedValue() if err != nil { - return "", errors.New("Failed to extract pnData from as-req") + return "", ErrReqData } eType = strconv.Itoa(enc.Etype) cipher = hex.EncodeToString(enc.Cipher) } } + if eType == "" || cipher == "" { - return "", errors.New("No encryption type or cipher found") + return "", ErrNoCipher } - hash := "$krb5$" + eType + "$" + crypt[0] + "$" + realm + "$nodata$" + cipher - return hash, nil + + return fmt.Sprintf("$krb5$%s$%s$%s$nodata$%s", eType, crypt[0], realm, cipher), nil } func (pd Krb5PnData) getParsedValue() (Krb5EncryptedData, error) { var encData Krb5EncryptedData _, err := asn1.Unmarshal(pd.Krb5PnDataValue, &encData) if err != nil { - return Krb5EncryptedData{}, errors.New("Failed to parse pdata value") + return Krb5EncryptedData{}, ErrReqData } return encData, nil } diff --git a/packets/ntlm.go b/packets/ntlm.go new file mode 100644 index 00000000..08f1030b --- /dev/null +++ b/packets/ntlm.go @@ -0,0 +1,237 @@ +package packets + +import ( + "encoding/base64" + "encoding/binary" + "encoding/hex" + "errors" + "strings" + "sync" + "unsafe" +) + +const ( + NTLM_SIG_OFFSET = 0 + NTLM_TYPE_OFFSET = 8 + + NTLM_TYPE1_FLAGS_OFFSET = 12 + NTLM_TYPE1_DOMAIN_OFFSET = 16 + NTLM_TYPE1_WORKSTN_OFFSET = 24 + NTLM_TYPE1_DATA_OFFSET = 32 + NTLM_TYPE1_MINSIZE = 16 + + NTLM_TYPE2_TARGET_OFFSET = 12 + NTLM_TYPE2_FLAGS_OFFSET = 20 + NTLM_TYPE2_CHALLENGE_OFFSET = 24 + NTLM_TYPE2_CONTEXT_OFFSET = 32 + NTLM_TYPE2_TARGETINFO_OFFSET = 40 + NTLM_TYPE2_DATA_OFFSET = 48 + NTLM_TYPE2_MINSIZE = 32 + + NTLM_TYPE3_LMRESP_OFFSET = 12 + NTLM_TYPE3_NTRESP_OFFSET = 20 + NTLM_TYPE3_DOMAIN_OFFSET = 28 + NTLM_TYPE3_USER_OFFSET = 36 + NTLM_TYPE3_WORKSTN_OFFSET = 44 + NTLM_TYPE3_SESSIONKEY_OFFSET = 52 + NTLM_TYPE3_FLAGS_OFFSET = 60 + NTLM_TYPE3_DATA_OFFSET = 64 + NTLM_TYPE3_MINSIZE = 52 + + NTLM_BUFFER_LEN_OFFSET = 0 + NTLM_BUFFER_MAXLEN_OFFSET = 2 + NTLM_BUFFER_OFFSET_OFFSET = 4 + NTLM_BUFFER_SIZE = 8 + + NtlmV1 = 1 + NtlmV2 = 2 +) + +type NTLMChallengeResponse struct { + Challenge string + Response string +} + +type NTLMChallengeResponseParsed struct { + Type int + ServerChallenge string + User string + Domain string + LmHash string + NtHashOne string + NtHashTwo string +} + +type NTLMResponseHeader struct { + Sig string + Type uint32 + LmLen uint16 + LmMax uint16 + LmOffset uint16 + NtLen uint16 + NtMax uint16 + NtOffset uint16 + DomainLen uint16 + DomainMax uint16 + DomainOffset uint16 + UserLen uint16 + UserMax uint16 + UserOffset uint16 + HostLen uint16 + HostMax uint16 + HostOffset uint16 +} + +type NTLMState struct { + sync.Mutex + + Responses map[uint32]string + Pairs []NTLMChallengeResponse +} + +func (s *NTLMState) AddServerResponse(key uint32, value string) { + s.Lock() + defer s.Unlock() + s.Responses[key] = value +} + +func (s *NTLMState) AddClientResponse(seq uint32, value string, cb func(data NTLMChallengeResponseParsed)) { + s.Lock() + defer s.Unlock() + + if chall, found := s.Responses[seq]; found == true { + pair := NTLMChallengeResponse{ + Challenge: chall, + Response: value, + } + s.Pairs = append(s.Pairs, pair) + + if data, err := pair.Parsed(); err == nil { + cb(data) + } + } +} + +func NewNTLMState() *NTLMState { + return &NTLMState{ + Responses: make(map[uint32]string), + Pairs: make([]NTLMChallengeResponse, 0), + } +} + +func (sr NTLMChallengeResponse) getServerChallenge() string { + dataCallenge := sr.getChallengeBytes() + //offset to the challenge and the challenge is 8 bytes long + return hex.EncodeToString(dataCallenge[NTLM_TYPE2_CHALLENGE_OFFSET : NTLM_TYPE2_CHALLENGE_OFFSET+8]) +} + +func (sr NTLMChallengeResponse) getChallengeBytes() []byte { + dataCallenge, _ := base64.StdEncoding.DecodeString(sr.Challenge) + return dataCallenge +} + +func (sr NTLMChallengeResponse) getResponseBytes() []byte { + dataResponse, _ := base64.StdEncoding.DecodeString(sr.Response) + return dataResponse +} +func (sr *NTLMChallengeResponse) Parsed() (NTLMChallengeResponseParsed, error) { + if sr.isNtlmV1() { + return sr.ParsedNtLMv1() + } + return sr.ParsedNtLMv2() +} + +func (sr *NTLMChallengeResponse) ParsedNtLMv2() (NTLMChallengeResponseParsed, error) { + r := sr.getResponseHeader() + if r.UserLen == 0 { + return NTLMChallengeResponseParsed{}, errors.New("No repsponse data") + } + b := sr.getResponseBytes() + nthash := b[r.NtOffset : r.NtOffset+r.NtLen] + // each char in user and domain is null terminated + return NTLMChallengeResponseParsed{ + Type: NtlmV2, + ServerChallenge: sr.getServerChallenge(), + User: strings.Replace(string(b[r.UserOffset:r.UserOffset+r.UserLen]), "\x00", "", -1), + Domain: strings.Replace(string(b[r.DomainOffset:r.DomainOffset+r.DomainLen]), "\x00", "", -1), + NtHashOne: hex.EncodeToString(nthash[:16]), // first part of the hash is 16 bytes + NtHashTwo: hex.EncodeToString(nthash[16:]), + }, nil +} + +func (sr NTLMChallengeResponse) isNtlmV1() bool { + headerValues := sr.getResponseHeader() + return headerValues.NtLen == 24 +} + +func (sr NTLMChallengeResponse) ParsedNtLMv1() (NTLMChallengeResponseParsed, error) { + r := sr.getResponseHeader() + if r.UserLen == 0 { + return NTLMChallengeResponseParsed{}, errors.New("No repsponse data") + } + b := sr.getResponseBytes() + // each char user and domain is null terminated + return NTLMChallengeResponseParsed{ + Type: NtlmV1, + ServerChallenge: sr.getServerChallenge(), + User: strings.Replace(string(b[r.UserOffset:r.UserOffset+r.UserLen]), "\x00", "", -1), + Domain: strings.Replace(string(b[r.DomainOffset:r.DomainOffset+r.DomainLen]), "\x00", "", -1), + LmHash: hex.EncodeToString(b[r.LmOffset : r.LmOffset+r.LmLen]), + }, nil +} + +func is_le() bool { + var i int32 = 0x01020304 + u := unsafe.Pointer(&i) + pb := (*byte)(u) + b := *pb + return (b == 0x04) +} + +func _uint32(b []byte, start, end int) uint32 { + if is_le() { + return binary.LittleEndian.Uint32(b[start:end]) + } + return binary.BigEndian.Uint32(b[start:end]) +} + +func _uint16(b []byte, start, end int) uint16 { + if is_le() { + return binary.LittleEndian.Uint16(b[start:end]) + } + return binary.BigEndian.Uint16(b[start:end]) +} + +func (sr NTLMChallengeResponse) getResponseHeader() NTLMResponseHeader { + b := sr.getResponseBytes() + if len(b) == 0 { + return NTLMResponseHeader{} + } + return NTLMResponseHeader{ + Sig: strings.Replace(string(b[NTLM_SIG_OFFSET:NTLM_SIG_OFFSET+8]), "\x00", "", -1), + Type: _uint32(b, NTLM_TYPE_OFFSET, NTLM_TYPE_OFFSET+4), + LmLen: _uint16(b, NTLM_TYPE3_LMRESP_OFFSET, NTLM_TYPE3_LMRESP_OFFSET+2), + LmMax: _uint16(b, NTLM_TYPE3_LMRESP_OFFSET+2, NTLM_TYPE3_LMRESP_OFFSET+4), + LmOffset: _uint16(b, NTLM_TYPE3_LMRESP_OFFSET+4, NTLM_TYPE3_LMRESP_OFFSET+6), + NtLen: _uint16(b, NTLM_TYPE3_NTRESP_OFFSET, NTLM_TYPE3_NTRESP_OFFSET+2), + NtMax: _uint16(b, NTLM_TYPE3_NTRESP_OFFSET+2, NTLM_TYPE3_NTRESP_OFFSET+4), + NtOffset: _uint16(b, NTLM_TYPE3_NTRESP_OFFSET+4, NTLM_TYPE3_NTRESP_OFFSET+6), + DomainLen: _uint16(b, NTLM_TYPE3_DOMAIN_OFFSET, NTLM_TYPE3_DOMAIN_OFFSET+2), + DomainMax: _uint16(b, NTLM_TYPE3_DOMAIN_OFFSET+2, NTLM_TYPE3_DOMAIN_OFFSET+4), + DomainOffset: _uint16(b, NTLM_TYPE3_DOMAIN_OFFSET+4, NTLM_TYPE3_DOMAIN_OFFSET+6), + UserLen: _uint16(b, NTLM_TYPE3_USER_OFFSET, NTLM_TYPE3_USER_OFFSET+2), + UserMax: _uint16(b, NTLM_TYPE3_USER_OFFSET+2, NTLM_TYPE3_USER_OFFSET+4), + UserOffset: _uint16(b, NTLM_TYPE3_USER_OFFSET+4, NTLM_TYPE3_USER_OFFSET+6), + HostLen: _uint16(b, NTLM_TYPE3_WORKSTN_OFFSET, NTLM_TYPE3_WORKSTN_OFFSET+2), + HostMax: _uint16(b, NTLM_TYPE3_WORKSTN_OFFSET+2, NTLM_TYPE3_WORKSTN_OFFSET+4), + HostOffset: _uint16(b, NTLM_TYPE3_WORKSTN_OFFSET+4, NTLM_TYPE3_WORKSTN_OFFSET+6), + } +} + +func (data NTLMChallengeResponseParsed) LcString() string { + // NTLM v1 in .lc format + if data.Type == NtlmV1 { + return data.User + "::" + data.Domain + ":" + data.LmHash + ":" + data.ServerChallenge + "\n" + } + return data.User + "::" + data.Domain + ":" + data.ServerChallenge + ":" + data.NtHashOne + ":" + data.NtHashTwo + "\n" +}