mirror of
https://github.com/bettercap/bettercap
synced 2025-07-06 04:52:10 -07:00
new: proper parsing of NTLM challenge responses
This commit is contained in:
parent
f596541d1c
commit
0372e5f6c7
3 changed files with 273 additions and 31 deletions
|
@ -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."+what,
|
||||
"ntlm.response",
|
||||
ip.SrcIP.String(),
|
||||
ip.DstIP.String(),
|
||||
SniffData{
|
||||
what: tokens[2],
|
||||
"data": data,
|
||||
},
|
||||
"%s %s > %s | %s",
|
||||
core.W(core.BG_DGRAY+core.FG_WHITE, "ntlm."+what),
|
||||
core.W(core.BG_DGRAY+core.FG_WHITE, "ntlm.response"),
|
||||
vIP(ip.SrcIP),
|
||||
vIP(ip.DstIP),
|
||||
tokens[2],
|
||||
data.LcString(),
|
||||
).Push()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
|
|
@ -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
237
packets/ntlm.go
Normal 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"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue