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"
|
"strings"
|
||||||
|
|
||||||
"github.com/evilsocket/bettercap-ng/core"
|
"github.com/evilsocket/bettercap-ng/core"
|
||||||
|
"github.com/evilsocket/bettercap-ng/packets"
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
"github.com/google/gopacket"
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
|
@ -14,6 +15,7 @@ var (
|
||||||
ntlmRe = regexp.MustCompile("(WWW-|Proxy-|)(Authenticate|Authorization): (NTLM|Negotiate)")
|
ntlmRe = regexp.MustCompile("(WWW-|Proxy-|)(Authenticate|Authorization): (NTLM|Negotiate)")
|
||||||
challRe = regexp.MustCompile("(WWW-|Proxy-|)(Authenticate): (NTLM|Negotiate)")
|
challRe = regexp.MustCompile("(WWW-|Proxy-|)(Authenticate): (NTLM|Negotiate)")
|
||||||
respRe = regexp.MustCompile("(WWW-|Proxy-|)(Authorization): (NTLM|Negotiate)")
|
respRe = regexp.MustCompile("(WWW-|Proxy-|)(Authorization): (NTLM|Negotiate)")
|
||||||
|
ntlm = packets.NewNTLMState()
|
||||||
)
|
)
|
||||||
|
|
||||||
func isNtlm(s string) bool {
|
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 {
|
if len(tokens) != 3 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
what := "?"
|
|
||||||
if isChallenge(line) {
|
if isChallenge(line) {
|
||||||
what = "challenge"
|
ntlm.AddServerResponse(tcp.Ack, tokens[2])
|
||||||
} else if isResponse(line) {
|
} 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
|
return true
|
||||||
|
|
|
@ -2,6 +2,7 @@ package packets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -17,8 +18,11 @@ const (
|
||||||
Krb5CryptRc4Hmac = 23
|
Krb5CryptRc4Hmac = 23
|
||||||
)
|
)
|
||||||
|
|
||||||
//https://github.com/heimdal/heimdal/blob/master/lib/asn1/krb5.asn1
|
|
||||||
var (
|
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"
|
Krb5AsReqParam = "application,explicit,tag:10"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -74,37 +78,37 @@ type Krb5Request struct {
|
||||||
|
|
||||||
func (kdc Krb5Request) String() (string, error) {
|
func (kdc Krb5Request) String() (string, error) {
|
||||||
var eType, cipher string
|
var eType, cipher string
|
||||||
var crypt []string
|
|
||||||
realm := kdc.ReqBody.Realm
|
|
||||||
|
|
||||||
if kdc.ReqBody.Cname.NameType == Krb5Krb5PrincipalNameType {
|
if kdc.ReqBody.Cname.NameType != Krb5Krb5PrincipalNameType {
|
||||||
crypt = kdc.ReqBody.Cname.NameString
|
return "", ErrNoCrypt
|
||||||
}
|
|
||||||
if len(crypt) != 1 {
|
|
||||||
return "", errors.New("No crypt alg found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
realm := kdc.ReqBody.Realm
|
||||||
|
crypt := kdc.ReqBody.Cname.NameString
|
||||||
|
|
||||||
for _, pn := range kdc.Krb5PnData {
|
for _, pn := range kdc.Krb5PnData {
|
||||||
if pn.Krb5PnDataType == 2 {
|
if pn.Krb5PnDataType == 2 {
|
||||||
enc, err := pn.getParsedValue()
|
enc, err := pn.getParsedValue()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("Failed to extract pnData from as-req")
|
return "", ErrReqData
|
||||||
}
|
}
|
||||||
eType = strconv.Itoa(enc.Etype)
|
eType = strconv.Itoa(enc.Etype)
|
||||||
cipher = hex.EncodeToString(enc.Cipher)
|
cipher = hex.EncodeToString(enc.Cipher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if eType == "" || 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) {
|
func (pd Krb5PnData) getParsedValue() (Krb5EncryptedData, error) {
|
||||||
var encData Krb5EncryptedData
|
var encData Krb5EncryptedData
|
||||||
_, err := asn1.Unmarshal(pd.Krb5PnDataValue, &encData)
|
_, err := asn1.Unmarshal(pd.Krb5PnDataValue, &encData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Krb5EncryptedData{}, errors.New("Failed to parse pdata value")
|
return Krb5EncryptedData{}, ErrReqData
|
||||||
}
|
}
|
||||||
return encData, nil
|
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