Initial QSfera import
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.vscode
|
||||
*.exe
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- godox
|
||||
- nakedret
|
||||
- predeclared
|
||||
- unconvert
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- internal/md4
|
||||
rules:
|
||||
- path: negotiate_flags.go
|
||||
linters:
|
||||
- unused
|
||||
- path: negotiator.go
|
||||
text: "QF1001:"
|
||||
formatters:
|
||||
enable:
|
||||
- gofumpt
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
- internal/md4
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
# E2E NTLM Tests
|
||||
|
||||
This directory contains end-to-end tests for the go-ntlmssp library that test against real NTLM servers.
|
||||
|
||||
## Running E2E Tests Locally
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Windows machine with IIS capabilities
|
||||
- Go 1.20 or later
|
||||
- Administrator privileges (for IIS setup)
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Enable IIS with Windows Authentication:**
|
||||
```powershell
|
||||
# Run as Administrator
|
||||
Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServerRole -All
|
||||
Enable-WindowsOptionalFeature -Online -FeatureName IIS-WindowsAuthentication -All
|
||||
```
|
||||
|
||||
2. **Create test site:**
|
||||
```powershell
|
||||
Import-Module WebAdministration
|
||||
New-Website -Name "ntlmtest" -Port 8080 -PhysicalPath "C:\inetpub\wwwroot"
|
||||
Set-WebConfigurationProperty -Filter "/system.webServer/security/authentication/anonymousAuthentication" -Name enabled -Value false -PSPath "IIS:\Sites\ntlmtest"
|
||||
Set-WebConfigurationProperty -Filter "/system.webServer/security/authentication/windowsAuthentication" -Name enabled -Value true -PSPath "IIS:\Sites\ntlmtest"
|
||||
```
|
||||
|
||||
3. **Set environment variables:**
|
||||
```powershell
|
||||
$env:NTLM_TEST_URL = "http://localhost:8080/"
|
||||
$env:NTLM_TEST_USER = "your_username"
|
||||
$env:NTLM_TEST_PASSWORD = "your_password"
|
||||
$env:NTLM_TEST_DOMAIN = "your_domain" # Optional
|
||||
```
|
||||
|
||||
> **Note**: The setup script automatically generates a random secure password if none is provided. For security, avoid hardcoded passwords in scripts or CI environments.
|
||||
|
||||
4. **Run tests:**
|
||||
```bash
|
||||
go test -v -tags=e2e ./e2e -run TestNTLM_E2E
|
||||
```
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
The E2E tests run automatically in GitHub Actions on Windows runners. The workflow:
|
||||
|
||||
1. Sets up a clean Windows Server environment
|
||||
2. Generates a random secure password for the test user
|
||||
3. Creates a test user account with the random password
|
||||
4. Configures IIS with Windows Authentication
|
||||
5. Runs the E2E tests against the real NTLM server
|
||||
5. Cleans up resources
|
||||
|
||||
## Test Coverage
|
||||
|
||||
The E2E tests cover:
|
||||
|
||||
- ✅ Basic NTLM authentication flow
|
||||
- ✅ UPN format usernames (`user@domain.com`)
|
||||
- ✅ SAM format usernames (`DOMAIN\user`)
|
||||
- ✅ Authentication failure scenarios
|
||||
- ✅ Server accessibility checks
|
||||
- ✅ Context cancellation handling
|
||||
- ✅ Direct ProcessChallenge function testing
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `NTLM_TEST_URL` | URL of NTLM-enabled server | `http://localhost:8080/` |
|
||||
| `NTLM_TEST_USER` | Username for authentication | `$USERNAME` (Windows) |
|
||||
| `NTLM_TEST_PASSWORD` | Password for authentication | Required |
|
||||
| `NTLM_TEST_DOMAIN` | Domain for authentication | `$USERDOMAIN` (Windows) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"No username available"** - Set `NTLM_TEST_USER` environment variable
|
||||
2. **"No password available"** - Set `NTLM_TEST_PASSWORD` environment variable
|
||||
3. **Connection refused** - Ensure IIS is running and accessible on the specified port
|
||||
4. **401 Unauthorized** - Check that Windows Authentication is enabled and working
|
||||
|
||||
### IIS Debugging
|
||||
|
||||
Check IIS status:
|
||||
```powershell
|
||||
Get-Website
|
||||
Get-WebApplication
|
||||
Get-WebConfigurationProperty -Filter "/system.webServer/security/authentication/windowsAuthentication" -Name enabled -PSPath "IIS:\Sites\Default Web Site"
|
||||
```
|
||||
|
||||
View IIS logs:
|
||||
```powershell
|
||||
Get-Content "C:\inetpub\logs\LogFiles\W3SVC1\*.log" | Select-Object -Last 50
|
||||
```
|
||||
|
||||
## Security Note
|
||||
|
||||
These tests use real authentication credentials. In CI/CD:
|
||||
- Test credentials are generated dynamically per job
|
||||
- Credentials are cleaned up after each test run
|
||||
- No persistent credentials are stored
|
||||
|
||||
For local development, use test accounts or ensure credentials are not committed to version control.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# go-ntlmssp
|
||||
|
||||
[](https://pkg.go.dev/github.com/Azure/go-ntlmssp) [](https://github.com/Azure/go-ntlmssp/actions/workflows/test.yml)
|
||||
|
||||
Go package that provides NTLM/Negotiate authentication over HTTP
|
||||
|
||||
* NTLM protocol details from https://msdn.microsoft.com/en-us/library/cc236621.aspx
|
||||
* NTLM over HTTP details from https://datatracker.ietf.org/doc/html/rfc4559
|
||||
* Implementation hints from http://davenport.sourceforge.net/ntlm.html
|
||||
|
||||
This package only implements authentication, no key exchange or encryption. It
|
||||
only supports Unicode (UTF16LE) encoding of protocol strings, no OEM encoding.
|
||||
This package implements NTLMv2.
|
||||
|
||||
# Installation
|
||||
|
||||
To install the package, use `go get`:
|
||||
|
||||
```bash
|
||||
go get github.com/Azure/go-ntlmssp
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
```go
|
||||
url, user, password := "http://www.example.com/secrets", "robpike", "pw123"
|
||||
client := &http.Client{
|
||||
Transport: ntlmssp.Negotiator{
|
||||
RoundTripper: &http.Transport{},
|
||||
},
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.SetBasicAuth(user, password)
|
||||
res, _ := client.Do(req)
|
||||
```
|
||||
|
||||
-----
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.8 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
|
||||
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
|
||||
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type authenicateMessage struct {
|
||||
LmChallengeResponse []byte
|
||||
NtChallengeResponse []byte
|
||||
|
||||
DomainName string
|
||||
UserName string
|
||||
Workstation string
|
||||
|
||||
// only set if negotiateFlag_NTLMSSP_NEGOTIATE_KEY_EXCH
|
||||
EncryptedRandomSessionKey []byte
|
||||
|
||||
NegotiateFlags negotiateFlags
|
||||
|
||||
MIC []byte
|
||||
}
|
||||
|
||||
type authenticateMessageFields struct {
|
||||
messageHeader
|
||||
LmChallengeResponse varField
|
||||
NtChallengeResponse varField
|
||||
DomainName varField
|
||||
UserName varField
|
||||
Workstation varField
|
||||
_ [8]byte
|
||||
NegotiateFlags negotiateFlags
|
||||
}
|
||||
|
||||
func (m *authenicateMessage) MarshalBinary() ([]byte, error) {
|
||||
if !m.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEUNICODE) {
|
||||
return nil, errors.New("only unicode is supported")
|
||||
}
|
||||
|
||||
domain, user := toUnicode(m.DomainName), toUnicode(m.UserName)
|
||||
workstation := toUnicode(m.Workstation)
|
||||
|
||||
ptr := binary.Size(&authenticateMessageFields{})
|
||||
f := authenticateMessageFields{
|
||||
messageHeader: newMessageHeader(3),
|
||||
NegotiateFlags: m.NegotiateFlags,
|
||||
LmChallengeResponse: newVarField(&ptr, len(m.LmChallengeResponse)),
|
||||
NtChallengeResponse: newVarField(&ptr, len(m.NtChallengeResponse)),
|
||||
DomainName: newVarField(&ptr, len(domain)),
|
||||
UserName: newVarField(&ptr, len(user)),
|
||||
Workstation: newVarField(&ptr, len(workstation)),
|
||||
}
|
||||
|
||||
f.NegotiateFlags.Unset(negotiateFlagNTLMSSPNEGOTIATEVERSION)
|
||||
|
||||
b := bytes.Buffer{}
|
||||
if err := binary.Write(&b, binary.LittleEndian, &f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Write(&b, binary.LittleEndian, &m.LmChallengeResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Write(&b, binary.LittleEndian, &m.NtChallengeResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Write(&b, binary.LittleEndian, &domain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Write(&b, binary.LittleEndian, &user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Write(&b, binary.LittleEndian, &workstation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func splitNameForAuth(username string) (user, domain string) {
|
||||
if strings.Contains(username, "\\") {
|
||||
ucomponents := strings.SplitN(username, "\\", 2)
|
||||
domain = ucomponents[0]
|
||||
user = ucomponents[1]
|
||||
} else if strings.Contains(username, "@") {
|
||||
user = username
|
||||
} else {
|
||||
user = username
|
||||
}
|
||||
return user, domain
|
||||
}
|
||||
|
||||
// AuthenticateMessageOptions contains optional parameters for the Authenticate message.
|
||||
type AuthenticateMessageOptions struct {
|
||||
WorkstationName string
|
||||
|
||||
// PasswordHashed indicates whether the provided password is already hashed.
|
||||
// If true, the password is expected to be in hexadecimal format.
|
||||
PasswordHashed bool
|
||||
}
|
||||
|
||||
// NewAuthenticateMessage creates a new AUTHENTICATE message in response to the CHALLENGE message that was received from the server.
|
||||
// The options parameter allows specifying additional settings for the message, it can be nil to use defaults.
|
||||
func NewAuthenticateMessage(challenge []byte, username, password string, options *AuthenticateMessageOptions) ([]byte, error) {
|
||||
if username == "" && password == "" {
|
||||
return nil, errors.New("anonymous authentication not supported")
|
||||
}
|
||||
|
||||
var cm challengeMessage
|
||||
if err := cm.UnmarshalBinary(challenge); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATELMKEY) {
|
||||
return nil, errors.New("only NTLM v2 is supported, but server requested v1 (NTLMSSP_NEGOTIATE_LM_KEY)")
|
||||
}
|
||||
if cm.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEKEYEXCH) {
|
||||
return nil, errors.New("key exchange requested but not supported (NTLMSSP_NEGOTIATE_KEY_EXCH)")
|
||||
}
|
||||
|
||||
am := authenicateMessage{
|
||||
NegotiateFlags: cm.NegotiateFlags,
|
||||
}
|
||||
am.UserName, am.DomainName = splitNameForAuth(username)
|
||||
if options != nil {
|
||||
am.Workstation = options.WorkstationName
|
||||
}
|
||||
|
||||
timestamp := cm.TargetInfo[avIDMsvAvTimestamp]
|
||||
if timestamp == nil { // no time sent, take current time
|
||||
ft := uint64(time.Now().UnixNano()) / 100
|
||||
ft += 116444736000000000 // add time between unix & windows offset
|
||||
timestamp = make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(timestamp, ft)
|
||||
}
|
||||
|
||||
clientChallenge := make([]byte, 8)
|
||||
if _, err := rand.Reader.Read(clientChallenge); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ntlmV2Hash []byte
|
||||
if options != nil && options.PasswordHashed {
|
||||
hashParts := strings.Split(password, ":")
|
||||
if len(hashParts) > 1 {
|
||||
password = hashParts[1]
|
||||
}
|
||||
hashBytes, err := hex.DecodeString(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ntlmV2Hash = getNtlmV2Hashed(hashBytes, am.UserName, am.DomainName)
|
||||
} else {
|
||||
ntlmV2Hash = getNtlmV2Hash(password, am.UserName, am.DomainName)
|
||||
}
|
||||
|
||||
am.NtChallengeResponse = computeNtlmV2Response(ntlmV2Hash,
|
||||
cm.ServerChallenge[:], clientChallenge, timestamp, cm.TargetInfoRaw)
|
||||
|
||||
if cm.TargetInfoRaw == nil {
|
||||
am.LmChallengeResponse = computeLmV2Response(ntlmV2Hash,
|
||||
cm.ServerChallenge[:], clientChallenge)
|
||||
}
|
||||
return am.MarshalBinary()
|
||||
}
|
||||
|
||||
// ProcessChallenge crafts an AUTHENTICATE message in response to the CHALLENGE message that was received from the server.
|
||||
// DomainNeeded is ignored, as the function extracts the domain from the username if needed.
|
||||
//
|
||||
// Deprecated: Use [NewAuthenticateMessage] instead.
|
||||
//
|
||||
//go:fix inline
|
||||
func ProcessChallenge(challengeMessageData []byte, username, password string, domainNeeded bool) ([]byte, error) {
|
||||
return NewAuthenticateMessage(challengeMessageData, username, password, nil)
|
||||
}
|
||||
|
||||
// ProcessChallengeWithHash is like ProcessChallenge but expects the password to be already hashed.
|
||||
// The hash should be provided in hexadecimal format.
|
||||
//
|
||||
// Deprecated: Use [NewAuthenticateMessage] with [AuthenticateMessageOptions.PasswordHashed] instead.
|
||||
//
|
||||
//go:fix inline
|
||||
func ProcessChallengeWithHash(challengeMessageData []byte, username, hash string) ([]byte, error) {
|
||||
return NewAuthenticateMessage(challengeMessageData, username, hash, &AuthenticateMessageOptions{
|
||||
PasswordHashed: true,
|
||||
})
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var schemaPreference = [...]string{"NTLM", "Negotiate", "Basic"}
|
||||
|
||||
type authheader struct {
|
||||
schema string
|
||||
data string
|
||||
}
|
||||
|
||||
// newAuthHeader extracts the authheader from the provided HTTP headers.
|
||||
// It selects the most preferred authentication scheme.
|
||||
// If no supported scheme is found, it returns an empty authheader.
|
||||
func newAuthHeader(req http.Header) authheader {
|
||||
auth := req.Values("Www-Authenticate")
|
||||
preferred, idx := -1, -1
|
||||
for i, s := range auth {
|
||||
for j, schema := range schemaPreference {
|
||||
if s == schema || strings.HasPrefix(s, schema+" ") {
|
||||
if preferred == -1 || j < preferred {
|
||||
preferred = j
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if idx == -1 {
|
||||
return authheader{}
|
||||
}
|
||||
schema, data, _ := strings.Cut(auth[idx], " ")
|
||||
return authheader{
|
||||
schema: schema,
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// isNTLM returns true if the authheader schema is NTLM or Negotiate.
|
||||
func (h authheader) isNTLM() bool {
|
||||
return h.schema == "NTLM" || h.schema == "Negotiate"
|
||||
}
|
||||
|
||||
// isBasic returns true if the authheader schema is Basic.
|
||||
func (h authheader) isBasic() bool {
|
||||
return h.schema == "Basic"
|
||||
}
|
||||
|
||||
// token extracts and decodes the base64 token from the authheader.
|
||||
// It returns nil if the schema is not NTLM or Negotiate.
|
||||
func (h authheader) token() ([]byte, error) {
|
||||
if !h.isNTLM() {
|
||||
// Schema not supported for token extraction
|
||||
return nil, nil
|
||||
}
|
||||
// RFC4559 4.2 - The token is a base64-encoded value
|
||||
return base64.StdEncoding.DecodeString(h.data)
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
type avID uint16
|
||||
|
||||
const (
|
||||
avIDMsvAvEOL avID = iota
|
||||
avIDMsvAvNbComputerName
|
||||
avIDMsvAvNbDomainName
|
||||
avIDMsvAvDNSComputerName
|
||||
avIDMsvAvDNSDomainName
|
||||
avIDMsvAvDNSTreeName
|
||||
avIDMsvAvFlags
|
||||
avIDMsvAvTimestamp
|
||||
avIDMsvAvSingleHost
|
||||
avIDMsvAvTargetName
|
||||
avIDMsvChannelBindings
|
||||
)
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type challengeMessageFields struct {
|
||||
messageHeader
|
||||
TargetName varField
|
||||
NegotiateFlags negotiateFlags
|
||||
ServerChallenge [8]byte
|
||||
_ [8]byte
|
||||
TargetInfo varField
|
||||
}
|
||||
|
||||
func (m challengeMessageFields) IsValid() bool {
|
||||
return m.messageHeader.IsValid() && m.MessageType == 2
|
||||
}
|
||||
|
||||
type challengeMessage struct {
|
||||
challengeMessageFields
|
||||
TargetName string
|
||||
TargetInfo map[avID][]byte
|
||||
TargetInfoRaw []byte
|
||||
}
|
||||
|
||||
func (m *challengeMessage) UnmarshalBinary(data []byte) error {
|
||||
r := bytes.NewReader(data)
|
||||
err := binary.Read(r, binary.LittleEndian, &m.challengeMessageFields)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !m.IsValid() {
|
||||
return fmt.Errorf("message is not a valid challenge message: %+v", m.messageHeader)
|
||||
}
|
||||
|
||||
if m.challengeMessageFields.TargetName.Len > 0 {
|
||||
m.TargetName, err = m.challengeMessageFields.TargetName.ReadStringFrom(data, m.NegotiateFlags.Has(negotiateFlagNTLMSSPNEGOTIATEUNICODE))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.challengeMessageFields.TargetInfo.Len > 0 {
|
||||
d, err := m.challengeMessageFields.TargetInfo.ReadFrom(data)
|
||||
m.TargetInfoRaw = d
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.TargetInfo = make(map[avID][]byte)
|
||||
r := bytes.NewReader(d)
|
||||
for {
|
||||
var id avID
|
||||
var l uint16
|
||||
err = binary.Read(r, binary.LittleEndian, &id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id == avIDMsvAvEOL {
|
||||
break
|
||||
}
|
||||
|
||||
err = binary.Read(r, binary.LittleEndian, &l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value := make([]byte, l)
|
||||
n, err := r.Read(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != int(l) {
|
||||
return fmt.Errorf("expected to read %d bytes, got only %d", l, n)
|
||||
}
|
||||
m.TargetInfo[id] = value
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# MD4 Implementation
|
||||
|
||||
This package contains an identical copy of the MD4 hash implementation from Go's extended cryptography package (`golang.org/x/crypto/md4`).
|
||||
|
||||
## Why Vendored?
|
||||
|
||||
This MD4 implementation is vendored locally to avoid depending on the `golang.org/x/crypto` package, which can introduce version conflicts and dependency management issues in `go.mod`. By maintaining our own copy, we ensure:
|
||||
|
||||
- **Stability**: No external dependency version conflicts
|
||||
- **Simplicity**: Cleaner `go.mod` file without xcrypto dependency
|
||||
- **Control**: Full control over the implementation without external changes
|
||||
|
||||
## Source
|
||||
|
||||
The original implementation can be found at:
|
||||
- Package: `golang.org/x/crypto/md4`
|
||||
- Repository: https://github.com/golang/crypto
|
||||
|
||||
## Usage
|
||||
|
||||
This package is intended for internal use within the go-ntlmssp library only. The MD4 hash algorithm is required for NTLM authentication but should not be used for general cryptographic purposes as MD4 is considered cryptographically broken.
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package md4 implements the MD4 hash algorithm as defined in RFC 1320.
|
||||
package md4
|
||||
|
||||
import (
|
||||
"hash"
|
||||
)
|
||||
|
||||
// The size of an MD4 checksum in bytes.
|
||||
const Size = 16
|
||||
|
||||
// The blocksize of MD4 in bytes.
|
||||
const BlockSize = 64
|
||||
|
||||
const (
|
||||
_Chunk = 64
|
||||
_Init0 = 0x67452301
|
||||
_Init1 = 0xEFCDAB89
|
||||
_Init2 = 0x98BADCFE
|
||||
_Init3 = 0x10325476
|
||||
)
|
||||
|
||||
// digest represents the partial evaluation of a checksum.
|
||||
type digest struct {
|
||||
s [4]uint32
|
||||
x [_Chunk]byte
|
||||
nx int
|
||||
len uint64
|
||||
}
|
||||
|
||||
func (d *digest) Reset() {
|
||||
d.s[0] = _Init0
|
||||
d.s[1] = _Init1
|
||||
d.s[2] = _Init2
|
||||
d.s[3] = _Init3
|
||||
d.nx = 0
|
||||
d.len = 0
|
||||
}
|
||||
|
||||
// New returns a new hash.Hash computing the MD4 checksum.
|
||||
func New() hash.Hash {
|
||||
d := new(digest)
|
||||
d.Reset()
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *digest) Size() int { return Size }
|
||||
|
||||
func (d *digest) BlockSize() int { return BlockSize }
|
||||
|
||||
func (d *digest) Write(p []byte) (nn int, err error) {
|
||||
nn = len(p)
|
||||
d.len += uint64(nn)
|
||||
if d.nx > 0 {
|
||||
n := len(p)
|
||||
if n > _Chunk-d.nx {
|
||||
n = _Chunk - d.nx
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
d.x[d.nx+i] = p[i]
|
||||
}
|
||||
d.nx += n
|
||||
if d.nx == _Chunk {
|
||||
_Block(d, d.x[0:])
|
||||
d.nx = 0
|
||||
}
|
||||
p = p[n:]
|
||||
}
|
||||
n := _Block(d, p)
|
||||
p = p[n:]
|
||||
if len(p) > 0 {
|
||||
d.nx = copy(d.x[:], p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d0 *digest) Sum(in []byte) []byte {
|
||||
// Make a copy of d0, so that caller can keep writing and summing.
|
||||
d := new(digest)
|
||||
*d = *d0
|
||||
|
||||
// Padding. Add a 1 bit and 0 bits until 56 bytes mod 64.
|
||||
len := d.len
|
||||
var tmp [64]byte
|
||||
tmp[0] = 0x80
|
||||
if len%64 < 56 {
|
||||
d.Write(tmp[0 : 56-len%64])
|
||||
} else {
|
||||
d.Write(tmp[0 : 64+56-len%64])
|
||||
}
|
||||
|
||||
// Length in bits.
|
||||
len <<= 3
|
||||
for i := uint(0); i < 8; i++ {
|
||||
tmp[i] = byte(len >> (8 * i))
|
||||
}
|
||||
d.Write(tmp[0:8])
|
||||
|
||||
if d.nx != 0 {
|
||||
panic("d.nx != 0")
|
||||
}
|
||||
|
||||
for _, s := range d.s {
|
||||
in = append(in, byte(s>>0))
|
||||
in = append(in, byte(s>>8))
|
||||
in = append(in, byte(s>>16))
|
||||
in = append(in, byte(s>>24))
|
||||
}
|
||||
return in
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// MD4 block step.
|
||||
// In its own file so that a faster assembly or C version
|
||||
// can be substituted easily.
|
||||
|
||||
package md4
|
||||
|
||||
import "math/bits"
|
||||
|
||||
var shift1 = []int{3, 7, 11, 19}
|
||||
var shift2 = []int{3, 5, 9, 13}
|
||||
var shift3 = []int{3, 9, 11, 15}
|
||||
|
||||
var xIndex2 = []uint{0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15}
|
||||
var xIndex3 = []uint{0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15}
|
||||
|
||||
func _Block(dig *digest, p []byte) int {
|
||||
a := dig.s[0]
|
||||
b := dig.s[1]
|
||||
c := dig.s[2]
|
||||
d := dig.s[3]
|
||||
n := 0
|
||||
var X [16]uint32
|
||||
for len(p) >= _Chunk {
|
||||
aa, bb, cc, dd := a, b, c, d
|
||||
|
||||
j := 0
|
||||
for i := 0; i < 16; i++ {
|
||||
X[i] = uint32(p[j]) | uint32(p[j+1])<<8 | uint32(p[j+2])<<16 | uint32(p[j+3])<<24
|
||||
j += 4
|
||||
}
|
||||
|
||||
// If this needs to be made faster in the future,
|
||||
// the usual trick is to unroll each of these
|
||||
// loops by a factor of 4; that lets you replace
|
||||
// the shift[] lookups with constants and,
|
||||
// with suitable variable renaming in each
|
||||
// unrolled body, delete the a, b, c, d = d, a, b, c
|
||||
// (or you can let the optimizer do the renaming).
|
||||
//
|
||||
// The index variables are uint so that % by a power
|
||||
// of two can be optimized easily by a compiler.
|
||||
|
||||
// Round 1.
|
||||
for i := uint(0); i < 16; i++ {
|
||||
x := i
|
||||
s := shift1[i%4]
|
||||
f := ((c ^ d) & b) ^ d
|
||||
a += f + X[x]
|
||||
a = bits.RotateLeft32(a, s)
|
||||
a, b, c, d = d, a, b, c
|
||||
}
|
||||
|
||||
// Round 2.
|
||||
for i := uint(0); i < 16; i++ {
|
||||
x := xIndex2[i]
|
||||
s := shift2[i%4]
|
||||
g := (b & c) | (b & d) | (c & d)
|
||||
a += g + X[x] + 0x5a827999
|
||||
a = bits.RotateLeft32(a, s)
|
||||
a, b, c, d = d, a, b, c
|
||||
}
|
||||
|
||||
// Round 3.
|
||||
for i := uint(0); i < 16; i++ {
|
||||
x := xIndex3[i]
|
||||
s := shift3[i%4]
|
||||
h := b ^ c ^ d
|
||||
a += h + X[x] + 0x6ed9eba1
|
||||
a = bits.RotateLeft32(a, s)
|
||||
a, b, c, d = d, a, b, c
|
||||
}
|
||||
|
||||
a += aa
|
||||
b += bb
|
||||
c += cc
|
||||
d += dd
|
||||
|
||||
p = p[_Chunk:]
|
||||
n += _Chunk
|
||||
}
|
||||
|
||||
dig.s[0] = a
|
||||
dig.s[1] = b
|
||||
dig.s[2] = c
|
||||
dig.s[3] = d
|
||||
return n
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
var signature = [8]byte{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0}
|
||||
|
||||
type messageHeader struct {
|
||||
Signature [8]byte
|
||||
MessageType uint32
|
||||
}
|
||||
|
||||
func (h messageHeader) IsValid() bool {
|
||||
return bytes.Equal(h.Signature[:], signature[:]) &&
|
||||
h.MessageType > 0 && h.MessageType < 4
|
||||
}
|
||||
|
||||
func newMessageHeader(messageType uint32) messageHeader {
|
||||
return messageHeader{signature, messageType}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
type negotiateFlags uint32
|
||||
|
||||
const (
|
||||
/*A*/ negotiateFlagNTLMSSPNEGOTIATEUNICODE negotiateFlags = 1 << 0
|
||||
/*B*/ negotiateFlagNTLMNEGOTIATEOEM = 1 << 1
|
||||
/*C*/ negotiateFlagNTLMSSPREQUESTTARGET = 1 << 2
|
||||
|
||||
/*D*/
|
||||
negotiateFlagNTLMSSPNEGOTIATESIGN = 1 << 4
|
||||
/*E*/ negotiateFlagNTLMSSPNEGOTIATESEAL = 1 << 5
|
||||
/*F*/ negotiateFlagNTLMSSPNEGOTIATEDATAGRAM = 1 << 6
|
||||
/*G*/ negotiateFlagNTLMSSPNEGOTIATELMKEY = 1 << 7
|
||||
|
||||
/*H*/
|
||||
negotiateFlagNTLMSSPNEGOTIATENTLM = 1 << 9
|
||||
|
||||
/*J*/
|
||||
negotiateFlagANONYMOUS = 1 << 11
|
||||
/*K*/ negotiateFlagNTLMSSPNEGOTIATEOEMDOMAINSUPPLIED = 1 << 12
|
||||
/*L*/ negotiateFlagNTLMSSPNEGOTIATEOEMWORKSTATIONSUPPLIED = 1 << 13
|
||||
|
||||
/*M*/
|
||||
negotiateFlagNTLMSSPNEGOTIATEALWAYSSIGN = 1 << 15
|
||||
/*N*/ negotiateFlagNTLMSSPTARGETTYPEDOMAIN = 1 << 16
|
||||
/*O*/ negotiateFlagNTLMSSPTARGETTYPESERVER = 1 << 17
|
||||
|
||||
/*P*/
|
||||
negotiateFlagNTLMSSPNEGOTIATEEXTENDEDSESSIONSECURITY = 1 << 19
|
||||
/*Q*/ negotiateFlagNTLMSSPNEGOTIATEIDENTIFY = 1 << 20
|
||||
|
||||
/*R*/
|
||||
negotiateFlagNTLMSSPREQUESTNONNTSESSIONKEY = 1 << 22
|
||||
/*S*/ negotiateFlagNTLMSSPNEGOTIATETARGETINFO = 1 << 23
|
||||
|
||||
/*T*/
|
||||
negotiateFlagNTLMSSPNEGOTIATEVERSION = 1 << 25
|
||||
|
||||
/*U*/
|
||||
negotiateFlagNTLMSSPNEGOTIATE128 = 1 << 29
|
||||
/*V*/ negotiateFlagNTLMSSPNEGOTIATEKEYEXCH = 1 << 30
|
||||
/*W*/ negotiateFlagNTLMSSPNEGOTIATE56 = 1 << 31
|
||||
)
|
||||
|
||||
func (field negotiateFlags) Has(flags negotiateFlags) bool {
|
||||
return field&flags == flags
|
||||
}
|
||||
|
||||
func (field *negotiateFlags) Unset(flags negotiateFlags) {
|
||||
*field ^= *field & flags
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const expMsgBodyLen = 40
|
||||
|
||||
type negotiateMessageFields struct {
|
||||
messageHeader
|
||||
NegotiateFlags negotiateFlags
|
||||
|
||||
Domain varField
|
||||
Workstation varField
|
||||
|
||||
Version
|
||||
}
|
||||
|
||||
var defaultFlags = negotiateFlagNTLMSSPNEGOTIATETARGETINFO |
|
||||
negotiateFlagNTLMSSPNEGOTIATE56 |
|
||||
negotiateFlagNTLMSSPNEGOTIATE128 |
|
||||
negotiateFlagNTLMSSPNEGOTIATEUNICODE |
|
||||
negotiateFlagNTLMSSPNEGOTIATEEXTENDEDSESSIONSECURITY |
|
||||
negotiateFlagNTLMSSPNEGOTIATENTLM |
|
||||
negotiateFlagNTLMSSPNEGOTIATEALWAYSSIGN
|
||||
|
||||
// NewNegotiateMessage creates a new NEGOTIATE message with the flags that this package supports.
|
||||
// Note that domain and workstation refer to the client machine, not the user that is authenticating.
|
||||
// It is recommended to leave them empty unless you know which are their correct values.
|
||||
//
|
||||
// The server may ignore these values, or may use them to infer that the client if running on the
|
||||
// same machine.
|
||||
func NewNegotiateMessage(domain, workstation string) ([]byte, error) {
|
||||
payloadOffset := expMsgBodyLen
|
||||
flags := defaultFlags
|
||||
|
||||
if domain != "" {
|
||||
flags |= negotiateFlagNTLMSSPNEGOTIATEOEMDOMAINSUPPLIED
|
||||
}
|
||||
|
||||
if workstation != "" {
|
||||
flags |= negotiateFlagNTLMSSPNEGOTIATEOEMWORKSTATIONSUPPLIED
|
||||
}
|
||||
|
||||
msg := negotiateMessageFields{
|
||||
messageHeader: newMessageHeader(1),
|
||||
NegotiateFlags: flags,
|
||||
Domain: newVarField(&payloadOffset, len(domain)),
|
||||
Workstation: newVarField(&payloadOffset, len(workstation)),
|
||||
Version: DefaultVersion(),
|
||||
}
|
||||
|
||||
b := bytes.Buffer{}
|
||||
if err := binary.Write(&b, binary.LittleEndian, &msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if b.Len() != expMsgBodyLen {
|
||||
return nil, errors.New("incorrect body length")
|
||||
}
|
||||
|
||||
payload := strings.ToUpper(domain + workstation)
|
||||
if _, err := b.WriteString(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// negotiatorBody wraps an io.ReadSeeker to allow waiting for its closure
|
||||
// before rewinding and reusing it.
|
||||
type negotiatorBody struct {
|
||||
body io.ReadSeeker
|
||||
closed chan struct{}
|
||||
startPos int64
|
||||
}
|
||||
|
||||
// newNegotiatorBody creates a negotiatorBody from the provided io.Reader.
|
||||
// If the body is nil, it returns nil.
|
||||
// If the body is already an io.ReadSeeker, it uses it directly.
|
||||
// Otherwise, it reads the entire body into memory to allow rewinding.
|
||||
func newNegotiatorBody(body io.Reader) (*negotiatorBody, error) {
|
||||
if body == nil {
|
||||
return nil, nil
|
||||
}
|
||||
// Check if body is already seekable to avoid buffering large bodies
|
||||
if seeker, ok := body.(io.ReadSeeker); ok {
|
||||
// Remember the current position
|
||||
startPos, err := seeker.Seek(0, io.SeekCurrent)
|
||||
if err == nil {
|
||||
// Seeking succeeded, use the seekable body directly
|
||||
return &negotiatorBody{
|
||||
body: seeker,
|
||||
closed: make(chan struct{}, 1),
|
||||
startPos: startPos,
|
||||
}, nil
|
||||
}
|
||||
// Seeking failed (e.g., pipes), fallback to buffering
|
||||
}
|
||||
// For non-seekable bodies, buffer in memory as required
|
||||
data, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &negotiatorBody{
|
||||
body: bytes.NewReader(data),
|
||||
closed: make(chan struct{}, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *negotiatorBody) Read(p []byte) (n int, err error) {
|
||||
if b == nil {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return b.body.Read(p)
|
||||
}
|
||||
|
||||
// Close signals that the body is no longer needed for the current request.
|
||||
// It allows the negotiator to rewind the body for potential reuse.
|
||||
// The underlying body is not closed here; use close() for that.
|
||||
func (b *negotiatorBody) Close() error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case b.closed <- struct{}{}:
|
||||
default:
|
||||
// Already signaled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// close closes the underlying body if it implements io.Closer.
|
||||
func (b *negotiatorBody) close() {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
if closer, ok := b.body.(io.Closer); ok {
|
||||
_ = closer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// rewind rewinds the body to the start position for reuse.
|
||||
func (b *negotiatorBody) rewind() error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
// Wait for the body to be closed before rewinding
|
||||
<-b.closed
|
||||
_, err := b.body.Seek(b.startPos, io.SeekStart)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDomain extracts the user domain from the username if present.
|
||||
//
|
||||
// Deprecated: Pass the username directly to [ProcessChallenge], it will handle domain extraction.
|
||||
// Don't pass the resulting domain to [NewNegotiateMessage], that function expects the client
|
||||
// machine domain, not the user domain.
|
||||
func GetDomain(username string) (user string, domain string, domainNeeded bool) {
|
||||
if strings.Contains(username, "\\") {
|
||||
ucomponents := strings.SplitN(username, "\\", 2)
|
||||
domain = ucomponents[0]
|
||||
user = ucomponents[1]
|
||||
domainNeeded = true
|
||||
} else if strings.Contains(username, "@") {
|
||||
user = username
|
||||
domainNeeded = false
|
||||
} else {
|
||||
user = username
|
||||
domainNeeded = true
|
||||
}
|
||||
return user, domain, domainNeeded
|
||||
}
|
||||
|
||||
// Negotiator is a [net/http.RoundTripper] decorator that automatically
|
||||
// converts basic authentication to NTLM/Negotiate authentication when appropriate.
|
||||
//
|
||||
// The credentials must be set using [net/http.Request.SetBasicAuth] on a per-request basis.
|
||||
//
|
||||
// By default, no credentials will be sent to the server unless it requests
|
||||
// Basic authentication and [Negotiator.AllowBasicAuth] is set to true.
|
||||
type Negotiator struct {
|
||||
// RoundTripper is the underlying round tripper to use.
|
||||
// If nil, http.DefaultTransport is used.
|
||||
http.RoundTripper
|
||||
|
||||
// AllowBasicAuth controls whether to send Basic authentication credentials
|
||||
// if the server requests it.
|
||||
//
|
||||
// If false (default), Basic authentication requests are ignored
|
||||
// and only NTLM/Negotiate authentication is performed.
|
||||
// If true, Basic authentication requests are honored.
|
||||
//
|
||||
// Only set this to true if you trust the server you are connecting to.
|
||||
// Basic authentication sends the credentials in clear text and may be
|
||||
// vulnerable to man-in-the-middle attacks and compromised servers.
|
||||
AllowBasicAuth bool
|
||||
|
||||
// WorkstationDomain is the domain of the client machine.
|
||||
// It is normally not needed to set this field.
|
||||
// It is passed to the negotiate message.
|
||||
WorkstationDomain string
|
||||
|
||||
// WorkstationName is the workstation name of the client machine.
|
||||
// It is passed to the negotiate and authenticate messages.
|
||||
// Useful for auditing purposes on the server side.
|
||||
WorkstationName string
|
||||
}
|
||||
|
||||
// RoundTrip sends the request to the server, handling any authentication
|
||||
// re-sends as needed.
|
||||
func (l Negotiator) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Use default round tripper if not provided
|
||||
rt := l.RoundTripper
|
||||
if rt == nil {
|
||||
rt = http.DefaultTransport
|
||||
}
|
||||
|
||||
// If it is not basic auth, just round trip the request as usual
|
||||
username, password, ok := req.BasicAuth()
|
||||
if !ok {
|
||||
return rt.RoundTrip(req)
|
||||
}
|
||||
id := identity{
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
|
||||
req = req.Clone(req.Context()) // Clone the request to avoid modifying the original
|
||||
|
||||
// We need to buffer or seek the request body to handle authentication challenges
|
||||
// that require resending the body multiple times during the NTLM handshake.
|
||||
body, err := newNegotiatorBody(req.Body)
|
||||
if err != nil {
|
||||
if req.Body != nil {
|
||||
_ = req.Body.Close()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer body.close()
|
||||
|
||||
// First try anonymous, in case the server still finds us authenticated from previous traffic
|
||||
req.Body = body
|
||||
req.Header.Del("Authorization")
|
||||
resp, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
// No authentication required, return the response as is
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Note that from here on, the response returned in case of error or unsuccessful
|
||||
// negotiation is the one we just got from the server. This is to allow the caller
|
||||
// to do its own handling in case we can't do it in this roundtrip.
|
||||
originalResp := resp
|
||||
|
||||
resauth := newAuthHeader(resp.Header)
|
||||
if l.AllowBasicAuth && resauth.isBasic() {
|
||||
// Basic auth requested instead of NTLM/Negotiate.
|
||||
//
|
||||
// Rewind the body, we will resend it.
|
||||
if body.rewind() != nil {
|
||||
return originalResp, nil
|
||||
}
|
||||
req.SetBasicAuth(id.username, id.password)
|
||||
resp, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
return originalResp, nil
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
// Basic auth succeeded, return the new response
|
||||
drainResponse(originalResp)
|
||||
return resp, nil
|
||||
}
|
||||
resauth = newAuthHeader(resp.Header)
|
||||
if !resauth.isNTLM() {
|
||||
// No NTLM/Negotiate requested, return the response as is
|
||||
return resp, nil
|
||||
}
|
||||
// Server upgraded from Basic to NTLM/Negotiate (rare but possible)
|
||||
drainResponse(resp)
|
||||
// After Basic-to-NTLM upgrade, update originalResp to the NTLM-triggering response
|
||||
originalResp = resp
|
||||
} else if !resauth.isNTLM() {
|
||||
// No NTLM/Negotiate requested, return the response as is
|
||||
return originalResp, nil
|
||||
}
|
||||
|
||||
// Server requested Negotiate/NTLM, start handshake
|
||||
|
||||
// First step: send negotiate message
|
||||
resp = clientHandshake(rt, req, resauth.schema, l.WorkstationDomain, l.WorkstationName)
|
||||
if resp == nil {
|
||||
return originalResp, nil
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
// We are expecting a 401 with challenge, but the server responded differently,
|
||||
// maybe it even accepted our negotiate message without further challenge, which is
|
||||
// valid per the spec (RFC 4559 Section 5).
|
||||
// Return the response as is, negotiation is over.
|
||||
drainResponse(originalResp)
|
||||
return resp, nil
|
||||
}
|
||||
resauth = newAuthHeader(resp.Header)
|
||||
drainResponse(resp)
|
||||
|
||||
// Second step: process challenge and resend the original body with the authenticate message
|
||||
resp = completeHandshake(rt, resauth, req, id, l.WorkstationName)
|
||||
if resp == nil {
|
||||
return originalResp, nil
|
||||
}
|
||||
// We could return the original response in case of 401 again, but at this point
|
||||
// it's better to return the latest response from the server, as it might be the case
|
||||
// that we are really not authorized.
|
||||
drainResponse(originalResp) // Done with the original response
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type identity struct {
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func drainResponse(res *http.Response) {
|
||||
// Drain body and close it to allow reusing the connection
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
}
|
||||
|
||||
func rewindBody(req *http.Request) error {
|
||||
if req.Body == nil {
|
||||
return nil
|
||||
}
|
||||
if nb, ok := req.Body.(*negotiatorBody); ok {
|
||||
return nb.rewind()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientHandshake(rt http.RoundTripper, req *http.Request, schema string, domain, workstation string) *http.Response {
|
||||
if rewindBody(req) != nil {
|
||||
return nil
|
||||
}
|
||||
auth, err := NewNegotiateMessage(domain, workstation)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
req.Header.Set("Authorization", schema+" "+base64.StdEncoding.EncodeToString(auth))
|
||||
res, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func completeHandshake(rt http.RoundTripper, resauth authheader, req *http.Request, id identity, workstation string) *http.Response {
|
||||
if rewindBody(req) != nil {
|
||||
return nil
|
||||
}
|
||||
challenge, err := resauth.token()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !resauth.isNTLM() || len(challenge) == 0 {
|
||||
// The only expected schema here is NTLM/Negotiate with a challenge token,
|
||||
// otherwise the negotiation is over.
|
||||
return nil
|
||||
}
|
||||
var opts *AuthenticateMessageOptions
|
||||
if workstation != "" {
|
||||
opts = &AuthenticateMessageOptions{
|
||||
WorkstationName: workstation,
|
||||
}
|
||||
}
|
||||
auth, err := NewAuthenticateMessage(challenge, id.username, id.password, opts)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
req.Header.Set("Authorization", resauth.schema+" "+base64.StdEncoding.EncodeToString(auth))
|
||||
resp, err := rt.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return resp
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
// Protocol details from https://msdn.microsoft.com/en-us/library/cc236621.aspx,
|
||||
// implementation hints from http://davenport.sourceforge.net/ntlm.html .
|
||||
// This package only implements authentication, no key exchange or encryption. It
|
||||
// only supports Unicode (UTF16LE) encoding of protocol strings, no OEM encoding.
|
||||
// This package implements NTLMv2.
|
||||
package ntlmssp
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/go-ntlmssp/internal/md4"
|
||||
)
|
||||
|
||||
func getNtlmV2Hash(password, username, domain string) []byte {
|
||||
return getNtlmV2Hashed(getNtlmHash(password), username, domain)
|
||||
}
|
||||
|
||||
func getNtlmV2Hashed(ntlmHash []byte, username, domain string) []byte {
|
||||
return hmacMd5(ntlmHash, toUnicode(strings.ToUpper(username)+domain))
|
||||
}
|
||||
|
||||
func getNtlmHash(password string) []byte {
|
||||
hash := md4.New()
|
||||
hash.Write(toUnicode(password))
|
||||
return hash.Sum(nil)
|
||||
}
|
||||
|
||||
func computeNtlmV2Response(ntlmV2Hash, serverChallenge, clientChallenge,
|
||||
timestamp, targetInfo []byte,
|
||||
) []byte {
|
||||
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)
|
||||
|
||||
NTProofStr := hmacMd5(ntlmV2Hash, serverChallenge, temp)
|
||||
return append(NTProofStr, temp...)
|
||||
}
|
||||
|
||||
func computeLmV2Response(ntlmV2Hash, serverChallenge, clientChallenge []byte) []byte {
|
||||
return append(hmacMd5(ntlmV2Hash, serverChallenge, clientChallenge), clientChallenge...)
|
||||
}
|
||||
|
||||
func hmacMd5(key []byte, data ...[]byte) []byte {
|
||||
mac := hmac.New(md5.New, key)
|
||||
for _, d := range data {
|
||||
mac.Write(d)
|
||||
}
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// helper func's for dealing with Windows Unicode (UTF16LE)
|
||||
|
||||
func fromUnicode(d []byte) (string, error) {
|
||||
if len(d)%2 > 0 {
|
||||
return "", errors.New("unicode (UTF 16 LE) specified, but uneven data length")
|
||||
}
|
||||
s := make([]uint16, len(d)/2)
|
||||
err := binary.Read(bytes.NewReader(d), binary.LittleEndian, &s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(utf16.Decode(s)), nil
|
||||
}
|
||||
|
||||
func toUnicode(s string) []byte {
|
||||
uints := utf16.Encode([]rune(s))
|
||||
b := bytes.Buffer{}
|
||||
_ = binary.Write(&b, binary.LittleEndian, &uints)
|
||||
return b.Bytes()
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type varField struct {
|
||||
Len uint16
|
||||
MaxLen uint16
|
||||
BufferOffset uint32
|
||||
}
|
||||
|
||||
func (f varField) ReadFrom(buffer []byte) ([]byte, error) {
|
||||
// f.Len is controlled by the sender, so we need to check that
|
||||
// it doesn't cause an overflow when added to f.BufferOffset.
|
||||
start := uint64(f.BufferOffset)
|
||||
end := start + uint64(f.Len)
|
||||
if end < start || end > uint64(len(buffer)) {
|
||||
return nil, errors.New("error reading data, varField extends beyond buffer")
|
||||
}
|
||||
return buffer[int(start):int(end)], nil
|
||||
}
|
||||
|
||||
func (f varField) ReadStringFrom(buffer []byte, unicode bool) (string, error) {
|
||||
d, err := f.ReadFrom(buffer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if unicode { // UTF-16LE encoding scheme
|
||||
return fromUnicode(d)
|
||||
}
|
||||
// OEM encoding, close enough to ASCII, since no code page is specified
|
||||
return string(d), err
|
||||
}
|
||||
|
||||
func newVarField(ptr *int, fieldsize int) varField {
|
||||
f := varField{
|
||||
Len: uint16(fieldsize),
|
||||
MaxLen: uint16(fieldsize),
|
||||
BufferOffset: uint32(*ptr),
|
||||
}
|
||||
*ptr += fieldsize
|
||||
return f
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package ntlmssp
|
||||
|
||||
// Version is a struct representing https://msdn.microsoft.com/en-us/library/cc236654.aspx
|
||||
type Version struct {
|
||||
ProductMajorVersion uint8
|
||||
ProductMinorVersion uint8
|
||||
ProductBuild uint16
|
||||
_ [3]byte
|
||||
NTLMRevisionCurrent uint8
|
||||
}
|
||||
|
||||
// DefaultVersion returns a Version with "sensible" defaults (Windows 7)
|
||||
func DefaultVersion() Version {
|
||||
return Version{
|
||||
ProductMajorVersion: 6,
|
||||
ProductMinorVersion: 1,
|
||||
ProductBuild: 7601,
|
||||
NTLMRevisionCurrent: 15,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user