From f883358a29aa36b15d27bc0d055b63076eb6f199 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 11 Nov 2024 22:03:38 +0100 Subject: [PATCH] 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. --- go.mod | 1 + go.sum | 2 + smtp/auth_ntlm.go | 52 ++++++++++++++++ smtp/smtp_test.go | 149 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 smtp/auth_ntlm.go diff --git a/go.mod b/go.mod index 72ce847..c09e7d2 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 30d3dc0..b3463de 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/smtp/auth_ntlm.go b/smtp/auth_ntlm.go new file mode 100644 index 0000000..edd1993 --- /dev/null +++ b/smtp/auth_ntlm.go @@ -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 +} diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 1b616fc..78147e2 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -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())