diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 8e0b18d..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,100 +0,0 @@ -version: 2.1 - -orbs: - win: circleci/windows@1.0.0 - -workflows: - workflow: - jobs: - - go-test: - name: Go 1.14 - docker-image: circleci/golang:1.14 - build-as-module: true - - go-test: - name: Go 1.13 - docker-image: circleci/golang:1.13 - build-as-module: true - - go-test: - name: Go 1.12 - docker-image: circleci/golang:1.12 - - go-test: - name: Go 1.11 - docker-image: circleci/golang:1.11 - - go-test: - name: Go 1.10 - docker-image: circleci/golang:1.10 - - go-test: - name: Go 1.9 - docker-image: circleci/golang:1.9 - - go-test: - name: Go 1.8 - docker-image: circleci/golang:1.8 - - go-test-windows: - name: Windows - -jobs: - go-test: - parameters: - docker-image: - type: string - build-as-module: - type: boolean - default: false - - docker: - - image: <> - environment: - CIRCLE_TEST_REPORTS: /tmp/circle-reports - CIRCLE_ARTIFACTS: /tmp/circle-artifacts - COMMON_GO_PACKAGES: > - github.com/jstemmer/go-junit-report - - working_directory: /go/src/github.com/launchdarkly/go-ntlmssp - - steps: - - checkout - - run: go get -u $COMMON_GO_PACKAGES - - - unless: - condition: <> - steps: - - run: go get -t . - - - run: - name: Run tests - command: | - mkdir -p $CIRCLE_TEST_REPORTS - mkdir -p $CIRCLE_ARTIFACTS - trap "go-junit-report < $CIRCLE_ARTIFACTS/report.txt > $CIRCLE_TEST_REPORTS/junit.xml" EXIT - go test -v -race | tee $CIRCLE_ARTIFACTS/report.txt - - - store_test_results: - path: /tmp/circle-reports - - - store_artifacts: - path: /tmp/circle-artifacts - - go-test-windows: - executor: - name: win/vs2019 - shell: powershell.exe - - environment: - GOPATH: C:\Users\VssAdministrator\go - PACKAGE_PATH: github.com/launchdarkly/go-ntlmssp - - steps: - - checkout - - run: go version - - run: - name: move source - command: | - go env GOPATH - mkdir ${env:GOPATH}\src\${env:PACKAGE_PATH} - mv * ${env:GOPATH}\src\${env:PACKAGE_PATH} - - run: - name: build and test - command: | - cd ${env:GOPATH}\src\${env:PACKAGE_PATH} - go get -t . - go test -v -race ./... diff --git a/.ldrelease/config.yml b/.ldrelease/config.yml deleted file mode 100644 index 98308a0..0000000 --- a/.ldrelease/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -template: - name: go - env: - LD_RELEASE_GO_IMPORT_PATH: github.com/launchdarkly/go-ntlmssp - -publications: - - url: https://godoc.org/github.com/launchdarkly/go-ntlmssp - description: documentation diff --git a/authenticate_message.go b/authenticate_message.go index c6fbe44..2c9507c 100644 --- a/authenticate_message.go +++ b/authenticate_message.go @@ -3,6 +3,7 @@ package ntlmssp import ( "bytes" "crypto/rand" + "encoding/base64" "encoding/binary" "errors" "time" @@ -78,13 +79,17 @@ func (m authenicateMessage) MarshalBinary() ([]byte, error) { return b.Bytes(), nil } -//ProcessChallenge crafts an AUTHENTICATE message in response to the CHALLENGE message -//that was received from the server +// ProcessChallenge crafts an AUTHENTICATE message in response to the CHALLENGE message +// that was received from the server func ProcessChallenge(challengeMessageData []byte, user, password string) ([]byte, error) { if user == "" && password == "" { return nil, errors.New("Anonymous authentication not supported") } + // debugging + PrintDebug("Received NTLM Type 2 Challenge: %s", base64.StdEncoding.EncodeToString(challengeMessageData)) + DecodeNTLMMessage(challengeMessageData) + var cm challengeMessage if err := cm.UnmarshalBinary(challengeMessageData); err != nil { return nil, err diff --git a/debug.go b/debug.go new file mode 100644 index 0000000..3f61d6b --- /dev/null +++ b/debug.go @@ -0,0 +1,153 @@ +package ntlmssp + +import ( + "encoding/binary" + "fmt" + "strings" +) + +// Debugging flag +var DebugMode = true + +// PrintDebug logs debug messages when DebugMode is enabled +func PrintDebug(format string, args ...interface{}) { + if DebugMode { + fmt.Printf(format+"\n", args...) + } +} + +// DecodeNTLMMessage decodes NTLM messages and prints details +func DecodeNTLMMessage(blob []byte) { + if len(blob) < 12 { + PrintDebug("Invalid NTLM message (too short)") + return + } + + if string(blob[:8]) != "NTLMSSP\x00" { + PrintDebug("Invalid NTLM signature") + return + } + + msgType := binary.LittleEndian.Uint32(blob[8:12]) + switch msgType { + case 1: + DecodeType1Message(blob) + case 2: + DecodeType2Message(blob) + case 3: + DecodeType3Message(blob) + default: + PrintDebug("Unknown NTLM message type: %d", msgType) + } +} + +// DecodeType1Message prints details of an NTLM Type 1 message +func DecodeType1Message(blob []byte) { + PrintDebug("==== Type1 ----") + PrintDebug("Signature: NTLMSSP") + PrintDebug("Type: 1") + + if len(blob) < 32 { + PrintDebug("Invalid NTLM Type 1 message") + return + } + + flags := binary.LittleEndian.Uint32(blob[12:16]) + PrintDebug("Flags: %08X", flags) + PrintDebug(DecodeFlags(flags)) + + domainLen := binary.LittleEndian.Uint16(blob[16:18]) + domainMaxLen := binary.LittleEndian.Uint16(blob[18:20]) + domainOffset := binary.LittleEndian.Uint32(blob[20:24]) + + workstationLen := binary.LittleEndian.Uint16(blob[24:26]) + workstationMaxLen := binary.LittleEndian.Uint16(blob[26:28]) + workstationOffset := binary.LittleEndian.Uint32(blob[28:32]) + + if domainMaxLen > 0 && int(domainOffset+uint32(domainLen)) <= len(blob) { + domain := string(blob[domainOffset : domainOffset+uint32(domainLen)]) + PrintDebug("Domain: %s", domain) + } + + if workstationMaxLen > 0 && int(workstationOffset+uint32(workstationLen)) <= len(blob) { + workstation := string(blob[workstationOffset : workstationOffset+uint32(workstationLen)]) + PrintDebug("Workstation: %s", workstation) + } +} + +// DecodeType2Message prints details of an NTLM Type 2 message +func DecodeType2Message(blob []byte) { + PrintDebug("==== Type2 ----") + PrintDebug("Signature: NTLMSSP") + PrintDebug("Type: 2") + + if len(blob) < 48 { + PrintDebug("Invalid NTLM Type 2 message") + return + } + + flags := binary.LittleEndian.Uint32(blob[20:24]) + PrintDebug("Flags: %08X", flags) + PrintDebug(DecodeFlags(flags)) + + challenge := blob[24:32] + PrintDebug("Challenge: %X", challenge) + + context := blob[40:48] + PrintDebug("Context: %X:%X", context[:4], context[4:]) +} + +// DecodeType3Message prints details of an NTLM Type 3 message +func DecodeType3Message(blob []byte) { + PrintDebug("==== Type3 ----") + PrintDebug("Signature: NTLMSSP") + PrintDebug("Type: 3") + + if len(blob) < 64 { + PrintDebug("Invalid NTLM Type 3 message") + return + } + + flags := binary.LittleEndian.Uint32(blob[60:64]) + PrintDebug("Flags: %08X", flags) + PrintDebug(DecodeFlags(flags)) +} + +// DecodeFlags returns a formatted string listing NTLM flag names +func DecodeFlags(flags uint32) string { + var flagStrings []string + + flagMap := map[uint32]string{ + 0x00000001: "NTLMSSP_NEGOTIATE_UNICODE", + 0x00000002: "NTLMSSP_NEGOTIATE_OEM", + 0x00000004: "NTLMSSP_REQUEST_TARGET", + 0x00000010: "NTLMSSP_NEGOTIATE_SIGN", + 0x00000020: "NTLMSSP_NEGOTIATE_SEAL", + 0x00000040: "NTLMSSP_NEGOTIATE_DATAGRAM", + 0x00000080: "NTLMSSP_NEGOTIATE_LM_KEY", + 0x00000100: "NTLMSSP_NEGOTIATE_NETWARE", + 0x00000200: "NTLMSSP_NEGOTIATE_NTLM", + 0x00000800: "NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED", + 0x00001000: "NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED", + 0x00002000: "NTLMSSP_NEGOTIATE_ALWAYS_SIGN", + 0x00020000: "NTLMSSP_NEGOTIATE_TARGET_INFO", + 0x00040000: "NTLMSSP_REQUEST_NON_NT_SESSION_KEY", + 0x00080000: "NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY", + 0x00100000: "NTLMSSP_NEGOTIATE_IDENTIFY", + 0x00200000: "NTLMSSP_REQUEST_TARGET", + 0x00800000: "NTLMSSP_TARGET_TYPE_DOMAIN", + 0x01000000: "NTLMSSP_TARGET_TYPE_SERVER", + 0x02000000: "NTLMSSP_TARGET_TYPE_SHARE", + 0x08000000: "NTLMSSP_NEGOTIATE_KEY_EXCH", + 0x20000000: "NTLMSSP_NEGOTIATE_128", + 0x80000000: "NTLMSSP_NEGOTIATE_56", + } + + for bit, name := range flagMap { + if flags&bit != 0 { + flagStrings = append(flagStrings, name) + } + } + + return strings.Join(flagStrings, "\n") +} diff --git a/go.mod b/go.mod index d630210..8f9f6d6 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ -module github.com/launchdarkly/go-ntlmssp +module git.coadcorp.com/nathan/go-ntlmssp -go 1.13 +go 1.24.1 -require golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 +require golang.org/x/crypto v0.36.0 diff --git a/go.sum b/go.sum index ce78ae2..8228770 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,2 @@ -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM= -golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= diff --git a/negotiator.go b/negotiator.go index 6e30454..e3acede 100644 --- a/negotiator.go +++ b/negotiator.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/base64" "io" - "io/ioutil" "net/http" "strings" ) @@ -21,12 +20,12 @@ func GetDomain(user string) (string, string) { return user, domain } -//Negotiator is a http.Roundtripper decorator that automatically -//converts basic authentication to NTLM/Negotiate authentication when appropriate. +// Negotiator is a http.Roundtripper decorator that automatically +// converts basic authentication to NTLM/Negotiate authentication when appropriate. type Negotiator struct{ http.RoundTripper } -//RoundTrip sends the request to the server, handling any authentication -//re-sends as needed. +// RoundTrip sends the request to the server, handling any authentication +// re-sends as needed. func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) { // Use default round tripper if not provided rt := l.RoundTripper @@ -47,7 +46,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) } req.Body.Close() - req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes())) + req.Body = io.NopCloser(bytes.NewReader(body.Bytes())) } // first try anonymous, in case the server still finds us // authenticated from previous traffic @@ -64,9 +63,9 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) if !resauth.IsNegotiate() && !resauth.IsNTLM() { // Unauthorized, Negotiate not requested, let's try with basic auth req.Header.Set("Authorization", string(reqauth)) - io.Copy(ioutil.Discard, res.Body) + io.Copy(io.Discard, res.Body) res.Body.Close() - req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes())) + req.Body = io.NopCloser(bytes.NewReader(body.Bytes())) res, err = rt.RoundTrip(req) if err != nil { @@ -80,7 +79,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) if resauth.IsNegotiate() || resauth.IsNTLM() { // 401 with request:Basic and response:Negotiate - io.Copy(ioutil.Discard, res.Body) + io.Copy(io.Discard, res.Body) res.Body.Close() // recycle credentials @@ -98,13 +97,18 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) if err != nil { return nil, err } + + // debugging + PrintDebug("Generated NTLM Type 1 Message: %s", base64.StdEncoding.EncodeToString(negotiateMessage)) + DecodeNTLMMessage(negotiateMessage) + if resauth.IsNTLM() { req.Header.Set("Authorization", "NTLM "+base64.StdEncoding.EncodeToString(negotiateMessage)) } else { req.Header.Set("Authorization", "Negotiate "+base64.StdEncoding.EncodeToString(negotiateMessage)) } - req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes())) + req.Body = io.NopCloser(bytes.NewReader(body.Bytes())) res, err = rt.RoundTrip(req) if err != nil { @@ -121,7 +125,7 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) // Negotiation failed, let client deal with response return res, nil } - io.Copy(ioutil.Discard, res.Body) + io.Copy(io.Discard, res.Body) res.Body.Close() // send authenticate @@ -129,13 +133,18 @@ func (l Negotiator) RoundTrip(req *http.Request) (res *http.Response, err error) if err != nil { return nil, err } + + // debugging + PrintDebug("Generated NTLM Type 3 Response: %s", base64.StdEncoding.EncodeToString(authenticateMessage)) + DecodeNTLMMessage(authenticateMessage) + if resauth.IsNTLM() { req.Header.Set("Authorization", "NTLM "+base64.StdEncoding.EncodeToString(authenticateMessage)) } else { req.Header.Set("Authorization", "Negotiate "+base64.StdEncoding.EncodeToString(authenticateMessage)) } - req.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes())) + req.Body = io.NopCloser(bytes.NewReader(body.Bytes())) res, err = rt.RoundTrip(req) }