Add experimental NTLMv2 authentication support

Introduced NTLMv2 authentication mechanism with tests in smtp package. Utilized the go-ntlmssp library to handle negotiation and response processing for NTLMv2 protocol.
This commit is contained in:
Winni Neessen 2024-11-11 22:03:38 +01:00
parent 50a4d0cc6f
commit f883358a29
Signed by: wneessen
GPG key ID: 385AC9889632126E
4 changed files with 204 additions and 0 deletions

1
go.mod
View file

@ -7,6 +7,7 @@ module github.com/wneessen/go-mail
go 1.16
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
golang.org/x/crypto v0.29.0
golang.org/x/text v0.20.0
)

2
go.sum
View file

@ -1,3 +1,5 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

52
smtp/auth_ntlm.go Normal file
View file

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: Copyright (c) 2024 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package smtp
import (
"fmt"
"github.com/Azure/go-ntlmssp"
)
// ErrNTLMChallangeEmpty is returned when the NTLMv2 ChallengeMessage received from the server is empty.
var ErrNTLMChallangeEmpty = fmt.Errorf("NTLMv2 ChallengeMessage is empty")
// ntlmAuth represents a NTLM client and satisfies the smtp.Auth interface.
type ntlmAuth struct {
domain, password, username, workstation string
domainNeeded bool
}
// NTLMv2Auth creates and returns a new NTLMv2 authentication mechanism with the given
// username and password.
func NTLMv2Auth(username, password, workstation string) Auth {
user, domain, domainNeeded := ntlmssp.GetDomain(username)
return &ntlmAuth{
domain: domain,
password: password,
username: user,
workstation: workstation,
domainNeeded: domainNeeded,
}
}
// Start initializes the NTLMv2 authentication process and returns the algorithm, the negotiation data, and
// a potential error
func (a *ntlmAuth) Start(_ *ServerInfo) (string, []byte, error) {
negotiateMessage, err := ntlmssp.NewNegotiateMessage(a.domain, a.workstation)
return "NTLM", negotiateMessage, err
}
// Next processes the server's challenge and returns the client's response for NTLMv2 authentication.
func (a *ntlmAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
if len(fromServer) == 0 {
return nil, ErrNTLMChallangeEmpty
}
authenticateMessage, err := ntlmssp.ProcessChallenge(fromServer, a.username, a.password, a.domainNeeded)
return authenticateMessage, err
}
return nil, nil
}

View file

@ -1462,6 +1462,155 @@ func TestCRAMMD5Auth(t *testing.T) {
})
}
func TestNTLMv2Auth(t *testing.T) {
var (
username = "user"
password = "SecREt01"
//ntlmHashHex = "CD06CA7C7E10C99B1D33B7485A2ED808" // NT Hash in hex of "SecREt01"
//target = "DOMAIN"
domain = "MYDOMAIN"
workstation = "MYPC"
//challenge = []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}
)
tests := []struct {
user string
workstation string
wantUser string
wantDomain string
wantNegotiate []byte
}{
{username, "", username, "", []byte{
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00,
0x88, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x28, 0x00, 0x00, 0x00, 0x06, 0x01, 0xb1, 0x1d, 0x00, 0x00, 0x00, 0x0f}},
{domain + "\\" + username, "", username, domain, []byte{
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x10,
0x88, 0xa0, 0x08, 0x00, 0x08, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x30, 0x00, 0x00, 0x00, 0x06, 0x01, 0xb1, 0x1d, 0x00, 0x00, 0x00, 0x0f, 0x4d, 0x59,
0x44, 0x4f, 0x4d, 0x41, 0x49, 0x4e}},
{domain + "\\" + username, workstation, username, domain, []byte{
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x30,
0x88, 0xa0, 0x08, 0x00, 0x08, 0x00, 0x28, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00,
0x30, 0x00, 0x00, 0x00, 0x06, 0x01, 0xb1, 0x1d, 0x00, 0x00, 0x00, 0x0f, 0x4d, 0x59,
0x44, 0x4f, 0x4d, 0x41, 0x49, 0x4e, 0x4d, 0x59, 0x50, 0x43}},
{username, workstation, username, "", []byte{
0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x20,
0x88, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00,
0x28, 0x00, 0x00, 0x00, 0x06, 0x01, 0xb1, 0x1d, 0x00, 0x00, 0x00, 0x0f, 0x4d, 0x59,
0x50, 0x43}},
}
t.Run("different user, domain and workstation combinations", func(t *testing.T) {
for _, tt := range tests {
t.Run("NTLMv2 init for "+tt.workstation+`\`+tt.user, func(t *testing.T) {
auth, ok := NTLMv2Auth(tt.user, password, tt.workstation).(*ntlmAuth)
if !ok {
t.Fatalf("failed to initialize NTLMv2 authentication")
}
if !strings.EqualFold(auth.username, tt.wantUser) {
t.Errorf("NTLMv2 authentication username failed, expected: %s, got: %s", tt.wantUser, auth.username)
}
if !strings.EqualFold(auth.domain, tt.wantDomain) {
t.Errorf("NTLMv2 authentication domain failed, expected: %s, got: %s", tt.wantDomain, auth.domain)
}
method, negotiation, err := auth.Start(nil)
if err != nil {
t.Fatalf("failed to start NTLMv2 authentication: %s", err)
}
if !strings.EqualFold(method, "NTLM") {
t.Errorf("NTLMv2 authentication method failed, expected method: NTLM, got: %s", method)
}
if !bytes.Equal(negotiation, tt.wantNegotiate) {
t.Errorf("NTLMv2 authentication negotiation failed, expected: %s, got: %s", tt.wantNegotiate,
negotiation)
}
})
}
})
t.Run("authenticate with NTLMv2 ", func(t *testing.T) {
auth := NTLMv2Auth("username", "password", "")
client, err := Dial("127.0.0.1:2525")
if err != nil {
t.Fatalf("failed to dial to test server: %s", err)
}
t.Cleanup(func() {
if err = client.Close(); err != nil {
t.Fatalf("failed to close client: %s", err)
}
})
if err = client.Hello("127.0.0.1"); err != nil {
t.Errorf("failed to send hello to test server: %s", err)
}
if err = client.Auth(auth); err != nil {
t.Errorf("failed to authenticate with NTLMv2: %s", err)
}
/*
for _, tt := range tests {
t.Run("NTLMv2 init for "+tt.workstation+`\`+tt.user, func(t *testing.T) {
_, negotiation, err := auth.Start(nil)
if err != nil {
t.Fatalf("failed to start NTLMv2 authentication: %s", err)
}
resp, err := auth.Next(challenge, true)
if err != nil {
t.Errorf("server challange failed: %s", err)
}
_ = negotiation
t.Log(resp)
hashBytes, err := hex.DecodeString(ntlmHashHex)
if err != nil {
t.Fatalf("failed to hex decode hash: %s", ntlmHashHex)
}
clientChallenge := []byte{0xff, 0xff, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44}
timestamp := []byte{0x00, 0x90, 0xd3, 0x36, 0xb7, 0x34, 0xc3, 0x01}
targetInfo := []byte{0x02, 0x00, 0x0c, 0x00, 0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x01, 0x00, 0x0c, 0x00, 0x53, 0x00, 0x45, 0x00, 0x52, 0x00, 0x56, 0x00, 0x45, 0x00, 0x52, 0x00, 0x04, 0x00, 0x14, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x03, 0x00, 0x22, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00}
mac := hmac.New(md5.New, hashBytes)
uints := utf16.Encode([]rune(strings.ToUpper(username) + target))
buf := bytes.NewBuffer(nil)
if err = binary.Write(buf, binary.LittleEndian, &uints); err != nil {
t.Fatalf("failed to convert username to utf16: %s", err)
}
mac.Write(buf.Bytes())
ntlmhash := mac.Sum(nil)
temp := []byte{1, 1, 0, 0, 0, 0, 0, 0}
temp = append(temp, timestamp...)
temp = append(temp, clientChallenge...)
temp = append(temp, 0, 0, 0, 0)
temp = append(temp, targetInfo...)
temp = append(temp, 0, 0, 0, 0)
mac = hmac.New(md5.New, ntlmhash)
mac.Write(challenge)
mac.Write(temp)
proofString := mac.Sum(nil)
proofString = append(proofString, temp...)
expected := []byte{
0xcb, 0xab, 0xbc, 0xa7, 0x13, 0xeb, 0x79, 0x5d, 0x04, 0xc9, 0x7a, 0xbc, 0x01, 0xee, 0x49, 0x83,
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xd3, 0x36, 0xb7, 0x34, 0xc3, 0x01,
0xff, 0xff, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x0c, 0x00,
0x44, 0x00, 0x4f, 0x00, 0x4d, 0x00, 0x41, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x01, 0x00, 0x0c, 0x00,
0x53, 0x00, 0x45, 0x00, 0x52, 0x00, 0x56, 0x00, 0x45, 0x00, 0x52, 0x00, 0x04, 0x00, 0x14, 0x00,
0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00,
0x6f, 0x00, 0x6d, 0x00, 0x03, 0x00, 0x22, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x76, 0x00,
0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00,
0x6e, 0x00, 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
}
if !bytes.Equal(expected, proofString) {
t.Errorf("failed to match expected %x, %x", expected, proofString)
}
})
}
*/
})
}
func TestNewClient(t *testing.T) {
t.Run("new client via Dial succeeds", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())