new: proper parsing of NTLM challenge responses

This commit is contained in:
evilsocket 2018-02-11 16:09:42 +01:00
parent f596541d1c
commit 0372e5f6c7
3 changed files with 273 additions and 31 deletions

View file

@ -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

View file

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

237
packets/ntlm.go Normal file
View file

@ -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"
}