Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 68 additions & 70 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,92 +154,90 @@ func Connect(addr, proxy string, timeout time.Duration, hostKerberos bool, stati

// If we get a 407 Proxy Authentication Required
if bytes.Contains(bytes.ToLower(responseStatus), []byte("407")) {
// Check if NTLM is supported
if bytes.Contains(bytes.ToLower(responseStatus), []byte(AskingForNTLMProxy)) {
//if we have specific credentials supplied by our user, attempt to use those for our NTLM negotiation with the proxy
schemes := ClassifyProxyAuth(responseStatus)
log.Printf("Proxy returned 407; advertised schemes: NTLM=%v Negotiate=%v", schemes.NTLM, schemes.Negotiate)

if staticNTLMCreds != nil {

// Start NTLM negotiation
ntlmHeader, err := getNTLMAuthHeader(staticNTLMCreds, nil)
if err != nil {
return nil, fmt.Errorf("NTLM negotiation failed: %v", err)
}

// Send Type 1 message
req = []string{
fmt.Sprintf("CONNECT %s HTTP/1.1", addr),
fmt.Sprintf("Host: %s", addr),
fmt.Sprintf("Proxy-Authorization: %s", ntlmHeader),
}
switch {
case schemes.NTLM && staticNTLMCreds != nil:
// Start NTLM negotiation
ntlmHeader, err := getNTLMAuthHeader(staticNTLMCreds, nil)
if err != nil {
return nil, fmt.Errorf("NTLM negotiation failed: %v", err)
}

err = WriteHTTPReq(req, proxyCon)
if err != nil {
return nil, fmt.Errorf("unable to send NTLM negotiate message: %s", err)
}
// Send Type 1 message
req = []string{
fmt.Sprintf("CONNECT %s HTTP/1.1", addr),
fmt.Sprintf("Host: %s", addr),
fmt.Sprintf("Proxy-Authorization: %s", ntlmHeader),
}

// Read challenge response
err = WriteHTTPReq(req, proxyCon)
if err != nil {
return nil, fmt.Errorf("unable to send NTLM negotiate message: %s", err)
}

challengeResponseOk := scanner.Scan()
if !challengeResponseOk {
return conn, fmt.Errorf("reading NTLM challenge failed")
}
// Read challenge response

challengeResponse := scanner.Text()
challengeResponseOk := scanner.Scan()
if !challengeResponseOk {
return conn, fmt.Errorf("reading NTLM challenge failed")
}

// Extract Type 2 message
ntlmParts := strings.SplitN(challengeResponse, NTLM, 2)
if len(ntlmParts) != 2 {
return nil, fmt.Errorf("no NTLM challenge received")
}
challengeResponse := scanner.Text()

challengeStr := strings.SplitN(ntlmParts[1], "\r\n", 2)[0]
challenge, err := base64.StdEncoding.DecodeString(challengeStr)
if err != nil {
return nil, fmt.Errorf("invalid NTLM challenge: %v", err)
}
// Extract Type 2 message
ntlmParts := strings.SplitN(challengeResponse, NTLM, 2)
if len(ntlmParts) != 2 {
return nil, fmt.Errorf("no NTLM challenge received")
}

// Generate Type 3 message
ntlmHeader, err = getNTLMAuthHeader(staticNTLMCreds, challenge)
if err != nil {
return nil, fmt.Errorf("NTLM authentication failed: %v", err)
}
challengeStr := strings.SplitN(ntlmParts[1], "\r\n", 2)[0]
challenge, err := base64.StdEncoding.DecodeString(challengeStr)
if err != nil {
return nil, fmt.Errorf("invalid NTLM challenge: %v", err)
}

// Send Type 3 message
req = []string{
fmt.Sprintf("CONNECT %s HTTP/1.1", addr),
fmt.Sprintf("Host: %s", addr),
fmt.Sprintf("Proxy-Authorization: %s", ntlmHeader),
}
// Generate Type 3 message
ntlmHeader, err = getNTLMAuthHeader(staticNTLMCreds, challenge)
if err != nil {
return nil, fmt.Errorf("NTLM authentication failed: %v", err)
}

err = WriteHTTPReq(req, proxyCon)
if err != nil {
return nil, fmt.Errorf("unable to send NTLM authenticate message: %v", err)
}
// Send Type 3 message
req = []string{
fmt.Sprintf("CONNECT %s HTTP/1.1", addr),
fmt.Sprintf("Host: %s", addr),
fmt.Sprintf("Proxy-Authorization: %s", ntlmHeader),
}

// Read final response
finalResponseOk := scanner.Scan()
if !finalResponseOk {
return conn, fmt.Errorf("failed to read final NTLM response")
}
err = WriteHTTPReq(req, proxyCon)
if err != nil {
return nil, fmt.Errorf("unable to send NTLM authenticate message: %v", err)
}

responseStatus = scanner.Bytes()
// Read final response
finalResponseOk := scanner.Scan()
if !finalResponseOk {
return conn, fmt.Errorf("failed to read final NTLM response")
}

} else if hostKerberos {
// otherwise, (if the user has allowed us to) use the host/user kerberos to auth with the proxy
req = addHostKerberosHeaders(proxy, req)
err = WriteHTTPReq(req, proxyCon)
if err != nil {
return nil, fmt.Errorf("unable to connect proxy %s", proxy)
}
responseStatus = scanner.Bytes()

proxyResponseOK := scanner.Scan()
if !proxyResponseOK {
return conn, fmt.Errorf("failed reading proxy after offering it host kerberos")
}
case (schemes.Negotiate || schemes.NTLM) && hostKerberos:
// otherwise, (if the user has allowed us to) use the host/user kerberos to auth with the proxy
req = addHostKerberosHeaders(proxy, req)
err = WriteHTTPReq(req, proxyCon)
if err != nil {
return nil, fmt.Errorf("unable to connect proxy %s", proxy)
}

responseStatus = scanner.Bytes()
proxyResponseOK := scanner.Scan()
if !proxyResponseOK {
return conn, fmt.Errorf("failed reading proxy after offering it host kerberos")
}

responseStatus = scanner.Bytes()
}
}

Expand Down
18 changes: 16 additions & 2 deletions internal/client/proxy_ntlm.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,24 @@ import (
)

const (
NTLM = "NTLM "
AskingForNTLMProxy = "proxy-authenticate: ntlm"
NTLM = "NTLM "
AskingForNTLMProxy = "proxy-authenticate: ntlm"
AskingForNegotiateProxy = "proxy-authenticate: negotiate"
)

type ProxyAuthSchemes struct {
NTLM bool
Negotiate bool
}

func ClassifyProxyAuth(response []byte) ProxyAuthSchemes {
lower := strings.ToLower(string(response))
return ProxyAuthSchemes{
NTLM: strings.Contains(lower, AskingForNTLMProxy),
Negotiate: strings.Contains(lower, AskingForNegotiateProxy),
}
}

func parseNTLMCreds(creds string) (domain, user, pass string, err error) {
if creds == "" {
return "", "", "", fmt.Errorf("NTLM credentials not provided. Use --ntlm-proxy-creds in format DOMAIN\\USER:PASS")
Expand Down
58 changes: 58 additions & 0 deletions internal/client/proxy_ntlm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,61 @@ func TestParseNTLMCreds(t *testing.T) {
})
}
}

func TestClassifyProxyAuth(t *testing.T) {
tests := []struct {
name string
response string
wantNTLM bool
wantNegotiate bool
}{
{
name: "Negotiate only",
response: "HTTP/1.1 407 Proxy Authentication Required\r\n" +
"Proxy-Authenticate: Negotiate\r\n",
wantNegotiate: true,
},
{
name: "NTLM only",
response: "HTTP/1.1 407 Proxy Authentication Required\r\n" +
"Proxy-Authenticate: NTLM\r\n",
wantNTLM: true,
},
{
name: "Negotiate and NTLM",
response: "HTTP/1.1 407 Proxy Authentication Required\r\n" +
"Proxy-Authenticate: Negotiate\r\n" +
"Proxy-Authenticate: NTLM\r\n" +
"Proxy-Authenticate: Basic realm=\"corp\"\r\n",
wantNTLM: true,
wantNegotiate: true,
},
{
name: "lowercase header",
response: "HTTP/1.1 407 Proxy Authentication Required\r\n" +
"proxy-authenticate: Negotiate\r\n",
wantNegotiate: true,
},
{
name: "Basic only",
response: "HTTP/1.1 407 Proxy Authentication Required\r\n" +
"Proxy-Authenticate: Basic realm=\"corp\"\r\n",
},
{
name: "empty response",
response: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ClassifyProxyAuth([]byte(tt.response))
if got.NTLM != tt.wantNTLM {
t.Errorf("NTLM = %v, want %v", got.NTLM, tt.wantNTLM)
}
if got.Negotiate != tt.wantNegotiate {
t.Errorf("Negotiate = %v, want %v", got.Negotiate, tt.wantNegotiate)
}
})
}
}