├── .github
└── workflows
│ └── check.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── README.md
├── ccachetools
├── ccache.go
└── ccache_test.go
├── compat
└── compat.go
├── credentials.go
├── credentials_test.go
├── dcerpcauth
└── dcerpcauth.go
├── dialer.go
├── examples
├── dcerpc
│ └── main.go
├── ldap
│ └── main.go
├── pkinit
│ └── main.go
└── smb
│ └── main.go
├── go.mod
├── go.sum
├── ldapauth
├── gssapi.go
├── ldap.go
└── ntlm.go
├── options.go
├── options_test.go
├── othername
├── othername.go
└── othername_test.go
├── pkinit
├── asn1.go
├── asrep.go
├── asreq.go
├── diffie_hellman.go
├── exchange.go
├── pkcs7.go
└── unpacthehash.go
├── resolver.go
├── resolver_test.go
├── smbauth
└── smbauth.go
├── target.go
├── target_test.go
├── testdata
├── empty.ccache
└── someuser@domain.tld.pfx
└── workspace.code-workspace
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Check
2 | on:
3 | # run tests on push to main, but not when other branches are pushed to
4 | push:
5 | branches:
6 | - main
7 |
8 | # run tests for all pull requests
9 | pull_request:
10 |
11 | jobs:
12 | lint:
13 | name: Lint
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Set up Go
17 | uses: actions/setup-go@v5
18 | with:
19 | go-version: 1.24.x
20 | id: go
21 |
22 | - name: Checkout code
23 | uses: actions/checkout@v4
24 |
25 | - name: golangci-lint
26 | uses: golangci/golangci-lint-action@v8
27 | with:
28 | version: v2.1
29 | args: --verbose --timeout 5m
30 |
31 | - name: Check go.mod
32 | run: |
33 | echo "check if go.mod is up to date"
34 | go mod tidy
35 | git diff --exit-code go.mod
36 |
37 | test:
38 | name: Test
39 | runs-on: ubuntu-latest
40 | steps:
41 | - name: Set up Go
42 | uses: actions/setup-go@v5
43 | with:
44 | go-version: 1.24.x
45 | id: go
46 |
47 | - name: Checkout code
48 | uses: actions/checkout@v4
49 |
50 | - name: Run tests
51 | run: |
52 | go test ./...
53 |
54 | build:
55 | strategy:
56 | matrix:
57 | go-version:
58 | - 1.24.x
59 | runs-on: ubuntu-latest
60 | name: Build with Go ${{ matrix.go-version }}
61 | env:
62 | GOPROXY: https://proxy.golang.org
63 | steps:
64 | - name: Set up Go ${{ matrix.go-version }}
65 | uses: actions/setup-go@v5
66 | with:
67 | go-version: ${{ matrix.go-version }}
68 | id: go
69 |
70 | - name: Checkout code
71 | uses: actions/checkout@v3
72 |
73 | - name: Build
74 | run: go build ./...
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dcerpc
2 | /examples/dcerpc/dcerpc
3 | /ldap
4 | /examples/ldap/ldap
5 | /examples/pkinit/pkinit
6 | /smb
7 | /examples/smb/smb
8 | *.exe
9 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | formatters:
3 | enable:
4 | - gci
5 | - gofmt
6 | - gofumpt
7 | - goimports
8 | - golines
9 | settings:
10 | golines:
11 | max-len: 120
12 | linters:
13 | default: none
14 | enable:
15 | - asasalint
16 | - asciicheck
17 | - bidichk
18 | - bodyclose
19 | - canonicalheader
20 | - containedctx
21 | - decorder
22 | - dupl
23 | - dupword
24 | - durationcheck
25 | - errchkjson
26 | - errname
27 | - errorlint
28 | - fatcontext
29 | - forcetypeassert
30 | - ginkgolinter
31 | - gocheckcompilerdirectives
32 | - gochecksumtype
33 | - goconst
34 | - gocritic
35 | - gocyclo
36 | - godot
37 | - godox
38 | - goheader
39 | - gosec
40 | - gomodguard
41 | - goprintffuncname
42 | - gosmopolitan
43 | - govet
44 | - grouper
45 | - importas
46 | - inamedparam
47 | - ineffassign
48 | - interfacebloat
49 | - lll
50 | - loggercheck
51 | - maintidx
52 | - makezero
53 | - mirror
54 | - misspell
55 | - nakedret
56 | - nestif
57 | - nilerr
58 | - nilnil
59 | - nlreturn
60 | - noctx
61 | - nolintlint
62 | - nosprintfhostport
63 | - prealloc
64 | - predeclared
65 | - promlinter
66 | - protogetter
67 | - reassign
68 | - sloglint
69 | - spancheck
70 | - sqlclosecheck
71 | - staticcheck
72 | - tagalign
73 | - tagliatelle
74 | - usetesting
75 | - testableexamples
76 | - testifylint
77 | - thelper
78 | - tparallel
79 | - unconvert
80 | - unparam
81 | - unused
82 | - usestdlibvars
83 | - wastedassign
84 | - whitespace
85 | - wsl
86 | - zerologlint
87 | settings:
88 | godox:
89 | keywords:
90 | - FIXME # FIXME generates a linter warning
91 | goconst:
92 | min-occurrences: 5
93 | tagliatelle:
94 | # check the struck tag name case
95 | case:
96 | rules:
97 | json: snake
98 | yaml: snake
99 | gosec:
100 | excludes:
101 | - G304 # command execution
102 | - G204 # file inclusion
103 | - G115 # integer overflow
104 | - G401 # weak cryptographic primitive (tell Microsoft, not me)
105 | - G501 # weak cryptographic primitive (tell Microsoft, not me)
106 | - G505 # weak cryptographic primitive (tell Microsoft, not me)
107 | - G402 # InsecureSkipVerify may be true
108 | gocyclo:
109 | min-complexity: 35
110 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2024 RedTeam Pentesting GmbH
2 |
3 | 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:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | 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.
8 |
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
adauth
3 | Active Directory Authentication Library
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | **Warning: The API of this library is not yet stable. Expect breaking changes.**
15 |
16 | `adauth` is a Go library for active directory authentication. It can be used to
17 | quickly set up authentication options:
18 |
19 | ```go
20 | var (
21 | ctx = context.Background()
22 | authOpts = &adauth.Options{}
23 | )
24 |
25 | authOpts.RegisterFlags(pflag.CommandLine)
26 | pflag.Parse()
27 | // --aes-key hex key Kerberos AES hex key
28 | // --ccache file Kerberos CCache file name (defaults to $KRB5CCNAME, currently unset)
29 | // --dc string Domain controller
30 | // -k, --kerberos Use Kerberos authentication
31 | // -H, --nt-hash hash NT hash ('NT', ':NT' or 'LM:NT')
32 | // -p, --password string Password
33 | // --pfx file Client certificate and private key as PFX file
34 | // --pfx-password string Password for PFX file
35 | // -u, --user user@domain Username ('user@domain', 'domain\user', 'domain/user' or 'user')
36 |
37 | // Credentials for an arbitrary target:
38 | creds, target, err := authOpts.WithTarget(ctx, "smb", pflag.Arg(0))
39 | if err != nil { /* error handling */ }
40 |
41 |
42 | // Only credentials are needed, no specific target:
43 | creds, err := authOpts.NoTarget()
44 | if err != nil { /* error handling */ }
45 |
46 | // Credentials to authenticate to the corresponding DC:
47 | creds, dc, err := authOpts.WithDCTarget(ctx, "ldap")
48 | if err != nil { /* error handling */ }
49 | ```
50 |
51 | It deduces as much information from the parameters as possible. For example,
52 | Kerberos authentication is possible even when specifying the target via IP
53 | address if reverse lookups are possible. Similarly, the domain can be omitted
54 | when the target hostname contains the domain.
55 |
56 | The library also contains helper packages for LDAP, SMB and DCERPC, a Kerebros
57 | PKINIT implementation as well as helpers for creating and writing CCache files
58 | (see examples).
59 |
60 | ## Features
61 |
62 | * Kerberos:
63 | * PKINIT
64 | * UnPAC-the-Hash
65 | * Pass-the-Hash (RC4/NT or AES key)
66 | * CCache (containing TGT or ST)
67 | * SOCKS5 support
68 | * NTLM:
69 | * Pass-the-Hash
70 | * LDAP:
71 | * Kerberos, NTLM, Simple Bind
72 | * mTLS Authentication / Pass-the-Certificate (LDAPS or LDAP+StartTLS)
73 | * Channel Binding (Kerberos and NTLM)
74 | * SOCKS5 support
75 | * SMB:
76 | * Kerberos, NTLM
77 | * Signing and Sealing
78 | * SOCKS5 support
79 | * DCERPC:
80 | * Kerberos, NTLM
81 | * Raw endpoits (with port mapping)
82 | * Named pipes (SMB)
83 | * Signing and Sealing
84 | * SOCKS5 support
85 |
86 | ## Caveats
87 |
88 | **LDAP:**
89 |
90 | The LDAP helper package does not support authentication using RC4 service
91 | tickets from `ccache`, since Windows returns unsupported GSSAPI wrap tokens
92 | during the SASL handshake when presented with an RC4 service ticket (see
93 | [github.com/jcmturner/gokrb5/pull/498](https://github.com/jcmturner/gokrb5/pull/498)).
94 |
95 | However, it should still be possible to request an AES256 service ticket
96 | instead, even when an NT hash was used for pre-authentication . Unfortunately,
97 | [impacket](https://github.com/fortra/impacket) always requests RC4 tickets. This
98 | behavior can be changed by adding
99 | `int(constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value),` as the first
100 | element of [this
101 | list](https://github.com/fortra/impacket/blob/af91d617c382e1eb132506159debcbc10da7a567/impacket/krb5/kerberosv5.py#L447-L450).
102 |
103 | The LDAP library does not (yet) support LDAP signing, but it supports channel
104 | binding for LDAPS and LDAP+StartTLS which is typically sufficient as a
105 | workaround unless the server lacks a TLS certificate.
106 |
107 |
--------------------------------------------------------------------------------
/ccachetools/ccache.go:
--------------------------------------------------------------------------------
1 | package ccachetools
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | "strings"
7 | "time"
8 |
9 | "github.com/jcmturner/gokrb5/v8/credentials"
10 | "github.com/jcmturner/gokrb5/v8/messages"
11 | "github.com/jcmturner/gokrb5/v8/types"
12 | )
13 |
14 | // NewCCache constructs an in-memory CCache.
15 | func NewCCache(
16 | ticket messages.Ticket, key types.EncryptionKey,
17 | serverName types.PrincipalName, clientName types.PrincipalName, clientRealm string,
18 | authTime time.Time, startTime time.Time, endTime time.Time, renewTill time.Time,
19 | ) (*credentials.CCache, error) {
20 | ticketBytes, err := ticket.Marshal()
21 | if err != nil {
22 | return nil, fmt.Errorf("marshal ticket for ccache: %w", err)
23 | }
24 |
25 | entry := &credentials.Credential{
26 | Key: key,
27 | AuthTime: authTime,
28 | StartTime: startTime,
29 | EndTime: endTime,
30 | RenewTill: renewTill,
31 | Ticket: ticketBytes,
32 | }
33 |
34 | entry.Client.PrincipalName = clientName
35 | entry.Client.Realm = clientRealm
36 | entry.Server.PrincipalName = serverName
37 | entry.Server.Realm = clientRealm
38 |
39 | ccache := &credentials.CCache{
40 | Credentials: []*credentials.Credential{entry},
41 | }
42 |
43 | ccache.DefaultPrincipal.PrincipalName = clientName
44 | ccache.DefaultPrincipal.Realm = strings.ToUpper(clientRealm)
45 |
46 | return ccache, nil
47 | }
48 |
49 | // NewCCacheFromASRep constructs an in-memory CCache based on the ticket and key
50 | // in the provided Kerberos ASRep message. The ASRep message must already be
51 | // decrypted. The CCache will contain only this ticket and the ticket user will
52 | // be set as the default principal of the CCache.
53 | func NewCCacheFromASRep(asRep messages.ASRep) (*credentials.CCache, error) {
54 | if len(asRep.DecryptedEncPart.Key.KeyValue) == 0 {
55 | return nil, fmt.Errorf("ASRep key was not decrypted")
56 | }
57 |
58 | return NewCCache(
59 | asRep.Ticket, asRep.DecryptedEncPart.Key,
60 | asRep.DecryptedEncPart.SName, asRep.CName, asRep.CRealm,
61 | asRep.DecryptedEncPart.AuthTime, asRep.DecryptedEncPart.StartTime,
62 | asRep.DecryptedEncPart.EndTime, asRep.DecryptedEncPart.RenewTill)
63 | }
64 |
65 | // NewCCacheFromTGSRep constructs an in-memory CCache based on the ticket and
66 | // key in the provided Kerberos TGSRep message. The TGSRep message must already
67 | // be decrypted. The CCache will contain only this ticket and the ticket user
68 | // will be set as the default principal of the CCache.
69 | func NewCCacheFromTGSRep(tgsRep messages.TGSRep) (*credentials.CCache, error) {
70 | if len(tgsRep.DecryptedEncPart.Key.KeyValue) == 0 {
71 | return nil, fmt.Errorf("TGSRep key was not decrypted")
72 | }
73 |
74 | return NewCCache(
75 | tgsRep.Ticket, tgsRep.DecryptedEncPart.Key,
76 | tgsRep.DecryptedEncPart.SName, tgsRep.CName, tgsRep.CRealm,
77 | tgsRep.DecryptedEncPart.AuthTime, tgsRep.DecryptedEncPart.StartTime,
78 | tgsRep.DecryptedEncPart.EndTime, tgsRep.DecryptedEncPart.RenewTill)
79 | }
80 |
81 | // MarshalCCache returns the byte representation of the provided CCache such
82 | // that it can be saved on-disk.
83 | func MarshalCCache(ccache *credentials.CCache) ([]byte, error) {
84 | switch ccache.Version {
85 | case 0, 1, 2, 3, 4:
86 | default:
87 | return nil, fmt.Errorf("unsupported CCache version: %d", ccache.Version)
88 | }
89 |
90 | version := ccache.Version
91 | if version == 0 {
92 | version = 4
93 | }
94 |
95 | var bo binary.AppendByteOrder = binary.BigEndian
96 |
97 | if version == 1 || version == 2 {
98 | bo = binary.LittleEndian
99 | }
100 |
101 | buf := []byte{5, version}
102 |
103 | // header
104 | if version == 4 {
105 | buf = bo.AppendUint16(buf, 0)
106 | }
107 |
108 | // default principal
109 | buf = append(buf, principalBytes(bo, version,
110 | ccache.DefaultPrincipal.PrincipalName, ccache.DefaultPrincipal.Realm)...)
111 |
112 | // credentials
113 | for _, cred := range ccache.Credentials {
114 | buf = append(buf, credentialBytes(bo, version, cred)...)
115 | }
116 |
117 | return buf, nil
118 | }
119 |
120 | func principalBytes(bo binary.AppendByteOrder, v uint8, p types.PrincipalName, realm string) (res []byte) {
121 | if v != 1 {
122 | res = bo.AppendUint32(res, uint32(p.NameType))
123 | }
124 |
125 | nCompontents := len(p.NameString)
126 | if v == 1 {
127 | nCompontents--
128 | }
129 |
130 | res = bo.AppendUint32(res, uint32(nCompontents))
131 | res = bo.AppendUint32(res, uint32(len(realm)))
132 |
133 | res = append(res, []byte(realm)...)
134 |
135 | for _, part := range p.NameString {
136 | res = bo.AppendUint32(res, uint32(len(part)))
137 | res = append(res, []byte(part)...)
138 | }
139 |
140 | return res
141 | }
142 |
143 | func credentialBytes(bo binary.AppendByteOrder, v uint8, cred *credentials.Credential) (res []byte) {
144 | res = append(res, principalBytes(bo, v, cred.Client.PrincipalName, cred.Client.Realm)...)
145 | res = append(res, principalBytes(bo, v, cred.Server.PrincipalName, cred.Server.Realm)...)
146 |
147 | res = bo.AppendUint16(res, uint16(cred.Key.KeyType))
148 | res = bo.AppendUint32(res, uint32(len(cred.Key.KeyValue)))
149 | res = append(res, cred.Key.KeyValue...)
150 |
151 | res = bo.AppendUint32(res, uint32(cred.AuthTime.Unix()))
152 | res = bo.AppendUint32(res, uint32(cred.StartTime.Unix()))
153 | res = bo.AppendUint32(res, uint32(cred.EndTime.Unix()))
154 | res = bo.AppendUint32(res, uint32(cred.RenewTill.Unix()))
155 |
156 | if cred.IsSKey {
157 | res = append(res, 1)
158 | } else {
159 | res = append(res, 0)
160 | }
161 |
162 | flags := cred.TicketFlags.Bytes
163 | if len(flags) == 0 {
164 | flags = make([]byte, 4)
165 | }
166 |
167 | res = append(res, flags...)
168 |
169 | res = bo.AppendUint32(res, uint32(len(cred.Addresses)))
170 |
171 | for _, addr := range cred.Addresses {
172 | res = bo.AppendUint16(res, uint16(addr.AddrType))
173 | res = bo.AppendUint32(res, uint32(len(addr.Address)))
174 | res = append(res, addr.Address...)
175 | }
176 |
177 | res = bo.AppendUint32(res, uint32(len(cred.AuthData)))
178 |
179 | for _, data := range cred.AuthData {
180 | res = bo.AppendUint16(res, uint16(data.ADType))
181 | res = bo.AppendUint32(res, uint32(len(data.ADData)))
182 | res = append(res, data.ADData...)
183 | }
184 |
185 | res = bo.AppendUint32(res, uint32(len(cred.Ticket)))
186 | res = append(res, cred.Ticket...)
187 |
188 | res = bo.AppendUint32(res, uint32(len(cred.SecondTicket)))
189 | res = append(res, cred.SecondTicket...)
190 |
191 | return res
192 | }
193 |
--------------------------------------------------------------------------------
/ccachetools/ccache_test.go:
--------------------------------------------------------------------------------
1 | package ccachetools_test
2 |
3 | import (
4 | "bytes"
5 | "slices"
6 | "strings"
7 | "testing"
8 | "time"
9 |
10 | "github.com/RedTeamPentesting/adauth/ccachetools"
11 | "github.com/jcmturner/gokrb5/v8/credentials"
12 | "github.com/jcmturner/gokrb5/v8/iana/nametype"
13 | "github.com/jcmturner/gokrb5/v8/messages"
14 | "github.com/jcmturner/gokrb5/v8/types"
15 | )
16 |
17 | var (
18 | testASRep = messages.ASRep{
19 | KDCRepFields: messages.KDCRepFields{
20 | PVNO: 5,
21 | MsgType: 11,
22 | CRealm: "realm",
23 | CName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "user"),
24 | Ticket: messages.Ticket{
25 | TktVNO: 5,
26 | Realm: "realm",
27 | SName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "krbtgt/redteam"),
28 | DecryptedEncPart: messages.EncTicketPart{
29 | Key: types.EncryptionKey{},
30 | CName: types.NewPrincipalName(0, ""),
31 | StartTime: time.Now(),
32 | AuthTime: time.Now(),
33 | EndTime: time.Now(),
34 | RenewTill: time.Now(),
35 | },
36 | },
37 | EncPart: types.EncryptedData{
38 | EType: 18,
39 | KVNO: 2,
40 | Cipher: []byte{1, 2, 3},
41 | },
42 | DecryptedEncPart: messages.EncKDCRepPart{
43 | Key: types.EncryptionKey{
44 | KeyType: 18,
45 | KeyValue: []byte{1, 3, 3, 7},
46 | },
47 | SRealm: "realm",
48 | SName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "krbtgt/redteam"),
49 | StartTime: time.Now(),
50 | AuthTime: time.Now(),
51 | EndTime: time.Now(),
52 | RenewTill: time.Now(),
53 | },
54 | },
55 | }
56 | testTGSRep = messages.TGSRep(testASRep)
57 | )
58 |
59 | func TestNewCCacheFromASRep(t *testing.T) {
60 | t.Parallel()
61 |
62 | ccache, err := ccachetools.NewCCacheFromASRep(testASRep)
63 | if err != nil {
64 | t.Fatalf("NewCCacheFromASRep: %v", err)
65 | }
66 |
67 | validateCCache(t, ccache)
68 | }
69 |
70 | func TestNewCCacheFromTGSRep(t *testing.T) {
71 | t.Parallel()
72 |
73 | ccache, err := ccachetools.NewCCacheFromTGSRep(testTGSRep)
74 | if err != nil {
75 | t.Fatalf("NewCCacheFromTGSRep: %v", err)
76 | }
77 |
78 | validateCCache(t, ccache)
79 | }
80 |
81 | func TestNewCCache(t *testing.T) {
82 | t.Parallel()
83 |
84 | ccache, err := ccachetools.NewCCache(
85 | testASRep.Ticket, testASRep.DecryptedEncPart.Key,
86 | testASRep.DecryptedEncPart.SName, testASRep.CName, testASRep.CRealm,
87 | testASRep.DecryptedEncPart.AuthTime, testASRep.DecryptedEncPart.StartTime,
88 | testASRep.DecryptedEncPart.EndTime, testASRep.DecryptedEncPart.RenewTill)
89 | if err != nil {
90 | t.Fatalf("NewCCacheFromTGSRep: %v", err)
91 | }
92 |
93 | validateCCache(t, ccache)
94 | }
95 |
96 | func TestMarshalCCache(t *testing.T) {
97 | t.Parallel()
98 |
99 | ccache, err := ccachetools.NewCCacheFromASRep(testASRep)
100 | if err != nil {
101 | t.Fatalf("new CCache: %v", err)
102 | }
103 |
104 | ccacheBytes, err := ccachetools.MarshalCCache(ccache)
105 | if err != nil {
106 | t.Fatalf("marshal CCache: %v", err)
107 | }
108 |
109 | var parsedCCache credentials.CCache
110 |
111 | err = parsedCCache.Unmarshal(ccacheBytes)
112 | if err != nil {
113 | t.Fatalf("unmarshal CCache: %v", err)
114 | }
115 |
116 | validateCCache(t, &parsedCCache)
117 | }
118 |
119 | func TestUnencryptedASRep(t *testing.T) {
120 | t.Parallel()
121 |
122 | _, err := ccachetools.NewCCacheFromASRep(messages.ASRep{
123 | KDCRepFields: messages.KDCRepFields{
124 | PVNO: 5,
125 | MsgType: 11,
126 | CRealm: "realm",
127 | CName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "user"),
128 | Ticket: messages.Ticket{
129 | TktVNO: 5,
130 | Realm: "realm",
131 | SName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "krbtgt/redteam"),
132 | DecryptedEncPart: messages.EncTicketPart{
133 | Key: types.EncryptionKey{},
134 | CName: types.NewPrincipalName(0, ""),
135 | StartTime: time.Now(),
136 | AuthTime: time.Now(),
137 | EndTime: time.Now(),
138 | RenewTill: time.Now(),
139 | },
140 | },
141 | EncPart: types.EncryptedData{
142 | EType: 18,
143 | KVNO: 2,
144 | Cipher: []byte{1, 2, 3},
145 | },
146 | },
147 | })
148 | if err == nil {
149 | t.Fatalf("NewCCache did not fail for undecrypted CCache")
150 | }
151 | }
152 |
153 | func TestUnencryptedTGSRep(t *testing.T) {
154 | t.Parallel()
155 |
156 | _, err := ccachetools.NewCCacheFromTGSRep(messages.TGSRep{
157 | KDCRepFields: messages.KDCRepFields{
158 | PVNO: 5,
159 | MsgType: 11,
160 | CRealm: "realm",
161 | CName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "user"),
162 | Ticket: messages.Ticket{
163 | TktVNO: 5,
164 | Realm: "realm",
165 | SName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, "krbtgt/redteam"),
166 | DecryptedEncPart: messages.EncTicketPart{
167 | Key: types.EncryptionKey{},
168 | CName: types.NewPrincipalName(0, ""),
169 | StartTime: time.Now(),
170 | AuthTime: time.Now(),
171 | EndTime: time.Now(),
172 | RenewTill: time.Now(),
173 | },
174 | },
175 | EncPart: types.EncryptedData{
176 | EType: 18,
177 | KVNO: 2,
178 | Cipher: []byte{1, 2, 3},
179 | },
180 | },
181 | })
182 | if err == nil {
183 | t.Fatalf("NewCCache did not fail for undecrypted CCache")
184 | }
185 | }
186 |
187 | func validateCCache(t *testing.T, ccache *credentials.CCache) {
188 | t.Helper()
189 |
190 | if ccache.DefaultPrincipal.Realm != strings.ToUpper(testASRep.CRealm) {
191 | t.Errorf("default principal realm %q does not match %q",
192 | ccache.DefaultPrincipal.Realm, strings.ToUpper(testASRep.CRealm))
193 | }
194 |
195 | if ccache.DefaultPrincipal.PrincipalName.NameType != testASRep.CName.NameType {
196 | t.Errorf("default principal name type %d does not match %d",
197 | ccache.DefaultPrincipal.PrincipalName.NameType, testASRep.CName.NameType)
198 | }
199 |
200 | if !slices.Equal(ccache.DefaultPrincipal.PrincipalName.NameString, testASRep.CName.NameString) {
201 | t.Errorf("default principal name string %v does not match %v",
202 | ccache.DefaultPrincipal.PrincipalName.NameString, testASRep.CName.NameString)
203 | }
204 |
205 | if len(ccache.Credentials) != 1 {
206 | t.Fatalf("found %d credentials instead of 1", len(ccache.Credentials))
207 | }
208 |
209 | cred := ccache.Credentials[0]
210 |
211 | if cred.Key.KeyType != testASRep.DecryptedEncPart.Key.KeyType {
212 | t.Errorf("key type %d does not match %d",
213 | cred.Key.KeyType, testASRep.DecryptedEncPart.Key.KeyType)
214 | }
215 |
216 | if !bytes.Equal(cred.Key.KeyValue, testASRep.DecryptedEncPart.Key.KeyValue) {
217 | t.Errorf("key value does not match")
218 | }
219 |
220 | if cred.Ticket == nil {
221 | t.Errorf("ticket is empty")
222 | }
223 |
224 | if cred.Client.Realm != testASRep.CRealm {
225 | t.Errorf("client realm %q does not match %s", cred.Client.Realm, testASRep.CRealm)
226 | }
227 |
228 | if cred.Client.PrincipalName.NameType != testASRep.CName.NameType {
229 | t.Errorf("client name type %d does not match %d",
230 | cred.Client.PrincipalName.NameType, testASRep.CName.NameType)
231 | }
232 |
233 | if !slices.Equal(cred.Client.PrincipalName.NameString, testASRep.CName.NameString) {
234 | t.Errorf("client name string %v does not match %v",
235 | cred.Client.PrincipalName.NameString, testASRep.CName.NameString)
236 | }
237 |
238 | if cred.Server.Realm != testASRep.CRealm {
239 | t.Errorf("server realm %q does not match %s", cred.Server.Realm, testASRep.CRealm)
240 | }
241 |
242 | if cred.Server.PrincipalName.NameType != testASRep.CName.NameType {
243 | t.Errorf("server name type %d does not match %d",
244 | cred.Server.PrincipalName.NameType, testASRep.DecryptedEncPart.SName.NameType)
245 | }
246 |
247 | if !slices.Equal(cred.Server.PrincipalName.NameString, testASRep.DecryptedEncPart.SName.NameString) {
248 | t.Errorf("server name string %v does not match %v",
249 | cred.Server.PrincipalName.NameString, testASRep.DecryptedEncPart.SName.NameString)
250 | }
251 |
252 | if cred.AuthTime.Unix() != testASRep.DecryptedEncPart.AuthTime.Unix() {
253 | t.Errorf("auth time %s does not match %s",
254 | cred.AuthTime, testASRep.DecryptedEncPart.AuthTime)
255 | }
256 |
257 | if cred.StartTime.Unix() != testASRep.DecryptedEncPart.StartTime.Unix() {
258 | t.Errorf("start time %s does not match %s",
259 | cred.StartTime, testASRep.DecryptedEncPart.StartTime)
260 | }
261 |
262 | if cred.EndTime.Unix() != testASRep.DecryptedEncPart.EndTime.Unix() {
263 | t.Errorf("end time %s does not match %s",
264 | cred.EndTime, testASRep.DecryptedEncPart.EndTime)
265 | }
266 |
267 | if cred.RenewTill.Unix() != testASRep.DecryptedEncPart.RenewTill.Unix() {
268 | t.Errorf("renew time %s does not match %s",
269 | cred.RenewTill, testASRep.DecryptedEncPart.RenewTill)
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/compat/compat.go:
--------------------------------------------------------------------------------
1 | // Package compat holds compatibility functions for interoperability between
2 | // forks or different libraries for the same purpose.
3 | package compat
4 |
5 | import (
6 | "github.com/jcmturner/gokrb5/v8/config"
7 | "github.com/jcmturner/gokrb5/v8/credentials"
8 | "github.com/jcmturner/gokrb5/v8/types"
9 | gokrb5ForkConfig "github.com/oiweiwei/gokrb5.fork/v9/config"
10 | gokrb5ForkCredentials "github.com/oiweiwei/gokrb5.fork/v9/credentials"
11 | gokrb5ForkTypes "github.com/oiweiwei/gokrb5.fork/v9/types"
12 | )
13 |
14 | func Gokrb5ForkV9KerberosConfig(cfg *config.Config) *gokrb5ForkConfig.Config {
15 | realms := make([]gokrb5ForkConfig.Realm, 0, len(cfg.Realms))
16 |
17 | for _, realm := range cfg.Realms {
18 | realms = append(realms, gokrb5ForkConfig.Realm(realm))
19 | }
20 |
21 | return &gokrb5ForkConfig.Config{
22 | LibDefaults: gokrb5ForkConfig.LibDefaults(cfg.LibDefaults),
23 | Realms: realms,
24 | DomainRealm: gokrb5ForkConfig.DomainRealm(cfg.DomainRealm),
25 | }
26 | }
27 |
28 | func Gokrb5ForkV9CCache(ccache *credentials.CCache) *gokrb5ForkCredentials.CCache {
29 | creds := make([]*gokrb5ForkCredentials.Credential, 0, len(ccache.Credentials))
30 |
31 | for _, cred := range ccache.Credentials {
32 | addrs := make([]gokrb5ForkTypes.HostAddress, 0, len(cred.Addresses))
33 |
34 | for _, addr := range cred.Addresses {
35 | addrs = append(addrs, gokrb5ForkTypes.HostAddress(addr))
36 | }
37 |
38 | adEntries := make([]gokrb5ForkTypes.AuthorizationDataEntry, 0, len(cred.AuthData))
39 |
40 | for _, adEntry := range cred.AuthData {
41 | adEntries = append(adEntries, gokrb5ForkTypes.AuthorizationDataEntry(adEntry))
42 | }
43 |
44 | creds = append(creds, &gokrb5ForkCredentials.Credential{
45 | Client: Gokrb5ForkV9Principal(cred.Client.Realm, cred.Client.PrincipalName),
46 | Server: Gokrb5ForkV9Principal(cred.Server.Realm, cred.Server.PrincipalName),
47 | Key: gokrb5ForkTypes.EncryptionKey(cred.Key),
48 | AuthTime: cred.AuthTime,
49 | StartTime: cred.StartTime,
50 | EndTime: cred.EndTime,
51 | RenewTill: cred.RenewTill,
52 | IsSKey: cred.IsSKey,
53 | TicketFlags: cred.TicketFlags,
54 | Addresses: addrs,
55 | AuthData: adEntries,
56 | Ticket: cred.Ticket,
57 | SecondTicket: cred.SecondTicket,
58 | })
59 | }
60 |
61 | return &gokrb5ForkCredentials.CCache{
62 | Version: ccache.Version,
63 | DefaultPrincipal: Gokrb5ForkV9Principal(ccache.DefaultPrincipal.Realm, ccache.DefaultPrincipal.PrincipalName),
64 | Credentials: creds,
65 | Path: ccache.Path,
66 | }
67 | }
68 |
69 | func Gokrb5ForkV9Principal(realm string, principalName types.PrincipalName) gokrb5ForkCredentials.Principal {
70 | return gokrb5ForkCredentials.Principal{
71 | Realm: realm,
72 | PrincipalName: gokrb5ForkTypes.PrincipalName(principalName),
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/credentials.go:
--------------------------------------------------------------------------------
1 | package adauth
2 |
3 | import (
4 | "context"
5 | "crypto/rsa"
6 | "crypto/x509"
7 | "fmt"
8 | "net"
9 | "os"
10 | "strings"
11 | "time"
12 |
13 | "github.com/RedTeamPentesting/adauth/othername"
14 | "github.com/jcmturner/gokrb5/v8/config"
15 | "github.com/jcmturner/gokrb5/v8/iana/etypeID"
16 | "software.sslmate.com/src/go-pkcs12"
17 | )
18 |
19 | // Credential represents Active Directory credentials.
20 | type Credential struct {
21 | // Username is the username without the domain.
22 | Username string
23 | // Password contains the users cleartext password if available.
24 | Password string
25 | // Domain holds the user's domain.
26 | Domain string
27 | // NTHash holds the user's NT hash or Kerberos RC4 key if available.
28 | NTHash string
29 | // AESKey holds the user's Kerberos AES128 or AES256 key if available.
30 | AESKey string
31 | // CCache contains the path to the user's CCache file.
32 | CCache string
33 | // ClientCert holds a client certificate for Kerberos or LDAP authentication if available.
34 | ClientCert *x509.Certificate
35 | // ClientCertKey holds the private key that corresponds to ClientCert.
36 | ClientCertKey any
37 | // CACerts holds CA certificates that were loaded alongside the ClientCert.
38 | CACerts []*x509.Certificate
39 | dc string
40 | // PasswordIsEmptyString is true when an empty Password field should not be
41 | // interpreted as a missing password but as a password that happens to be
42 | // empty.
43 | PasswordIsEmtpyString bool
44 | // CCacheIsFromEnv indicates whether the CCache was set explicitly or
45 | // implicitly through an environment variable.
46 | CCacheIsFromEnv bool
47 |
48 | // Resolver can be used to set an alternative DNS resolver. If empty,
49 | // net.DefaultResolver is used.
50 | Resolver Resolver
51 | }
52 |
53 | // CredentialFromPFX creates a Credential structure for certificate-based
54 | // authentication based on a PFX file.
55 | func CredentialFromPFX(
56 | username string, domain string, pfxFile string, pfxPassword string,
57 | ) (*Credential, error) {
58 | pfxData, err := os.ReadFile(pfxFile)
59 | if err != nil {
60 | return nil, fmt.Errorf("read PFX: %w", err)
61 | }
62 |
63 | return CredentialFromPFXBytes(username, domain, pfxData, pfxPassword)
64 | }
65 |
66 | // CredentialFromPFX creates a Credential structure for certificate-based
67 | // authentication based on PFX data.
68 | func CredentialFromPFXBytes(
69 | username string, domain string, pfxData []byte, pfxPassword string,
70 | ) (*Credential, error) {
71 | cred := &Credential{
72 | Username: username,
73 | Domain: domain,
74 | }
75 |
76 | key, cert, caCerts, err := pkcs12.DecodeChain(pfxData, pfxPassword)
77 | if err != nil {
78 | return nil, fmt.Errorf("decode PFX: %w", err)
79 | }
80 |
81 | rsaKey, ok := key.(*rsa.PrivateKey)
82 | if !ok {
83 | return nil, fmt.Errorf("PFX key is not an RSA private key but %T", rsaKey)
84 | }
85 |
86 | cred.ClientCert = cert
87 | cred.ClientCertKey = rsaKey
88 | cred.CACerts = caCerts
89 |
90 | user, domain, err := othername.UserAndDomain(cert)
91 | if err == nil {
92 | if cred.Username == "" {
93 | cred.Username = user
94 | }
95 |
96 | if cred.Domain == "" {
97 | cred.Domain = domain
98 | }
99 | }
100 |
101 | return cred, nil
102 | }
103 |
104 | // UPN is the user principal name (username@domain). If the credential does not
105 | // contain a domain, only the username is returned. If username and domain are
106 | // empty, the UPN will be empty, too.
107 | func (c *Credential) UPN() string {
108 | switch {
109 | case c.Username == "" && c.Domain == "":
110 | return ""
111 | case c.Domain == "":
112 | return c.Username
113 | default:
114 | return c.Username + "@" + c.Domain
115 | }
116 | }
117 |
118 | // LogonName is the legacy logon name (domain\username).
119 | func (c *Credential) LogonName() string {
120 | return c.Domain + `\` + c.Username
121 | }
122 |
123 | // LogonNameWithUpperCaseDomain is like LogonName with the domain capitalized
124 | // for compatibility with the Kerberos library (DOMAIN\username).
125 | func (c *Credential) LogonNameWithUpperCaseDomain() string {
126 | return strings.ToUpper(c.Domain) + `\` + c.Username
127 | }
128 |
129 | // ImpacketLogonName is the Impacket-style logon name (domain/username).
130 | func (c *Credential) ImpacketLogonName() string {
131 | return c.Domain + "/" + c.Username
132 | }
133 |
134 | // SetDC configures a specific domain controller for this credential.
135 | func (c *Credential) SetDC(dc string) {
136 | c.dc = dc
137 | }
138 |
139 | // DC returns the domain controller of the credential's domain as a target.
140 | func (c *Credential) DC(ctx context.Context, protocol string) (*Target, error) {
141 | if c.dc != "" {
142 | return newTarget(protocol, c.dc, true, c.CCache, c.Resolver), nil
143 | }
144 |
145 | if c.Domain == "" {
146 | return nil, fmt.Errorf("domain unknown")
147 | }
148 |
149 | _, addrs, err := ensureResolver(c.Resolver, nil).LookupSRV(ctx, "kerberos", "tcp", c.Domain)
150 | if err != nil {
151 | return nil, fmt.Errorf("lookup %q service of domain %q: %w", "kerberos", c.Domain, err)
152 | }
153 |
154 | if len(addrs) == 0 {
155 | return nil, fmt.Errorf("no %q services were discovered for domain %q", "kerberos", c.Domain)
156 | }
157 |
158 | return newTarget(protocol, strings.TrimRight(addrs[0].Target, "."), true, c.CCache, c.Resolver), nil
159 | }
160 |
161 | func (c *Credential) mustUseKerberos() bool {
162 | return c.Password == "" && c.NTHash == "" && (c.CCache != "" || c.AESKey != "")
163 | }
164 |
165 | // KerberosConfig returns the Kerberos configuration for the credential's
166 | // domain. For compatibility with other Kerberos libraries, see the `compat`
167 | // package.
168 | func (c *Credential) KerberosConfig(ctx context.Context) (*config.Config, error) {
169 | dc, err := c.DC(ctx, "krbtgt")
170 | if err != nil {
171 | return nil, fmt.Errorf("find DC: %w", err)
172 | }
173 |
174 | krbConf := config.New()
175 | krbConf.LibDefaults.DefaultRealm = strings.ToUpper(c.Domain)
176 | krbConf.LibDefaults.AllowWeakCrypto = true
177 | krbConf.LibDefaults.DNSLookupRealm = false
178 | krbConf.LibDefaults.DNSLookupKDC = false
179 | krbConf.LibDefaults.TicketLifetime = time.Duration(24) * time.Hour
180 | krbConf.LibDefaults.RenewLifetime = time.Duration(24*7) * time.Hour
181 | krbConf.LibDefaults.Forwardable = true
182 | krbConf.LibDefaults.Proxiable = true
183 | krbConf.LibDefaults.RDNS = false
184 | krbConf.LibDefaults.UDPPreferenceLimit = 1 // Force use of tcp
185 |
186 | if c.NTHash != "" {
187 | // use RC4 for pre-auth but AES256 for ephemeral keys, otherwise we get
188 | // unsupported GSSAPI tokens during LDAP SASL handshake
189 | krbConf.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.AES256_CTS_HMAC_SHA1_96}
190 | krbConf.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.RC4_HMAC}
191 | krbConf.LibDefaults.PermittedEnctypeIDs = []int32{etypeID.AES256_CTS_HMAC_SHA1_96}
192 | krbConf.LibDefaults.PreferredPreauthTypes = []int{int(etypeID.RC4_HMAC)}
193 | } else {
194 | krbConf.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.AES256_CTS_HMAC_SHA1_96}
195 | krbConf.LibDefaults.DefaultTktEnctypeIDs = []int32{
196 | etypeID.AES256_CTS_HMAC_SHA1_96, etypeID.AES128_CTS_HMAC_SHA1_96, etypeID.RC4_HMAC,
197 | }
198 | krbConf.LibDefaults.PermittedEnctypeIDs = []int32{etypeID.AES256_CTS_HMAC_SHA1_96}
199 | krbConf.LibDefaults.PreferredPreauthTypes = []int{
200 | int(etypeID.AES256_CTS_HMAC_SHA1_96), int(etypeID.AES128_CTS_HMAC_SHA1_96), int(etypeID.RC4_HMAC),
201 | }
202 | }
203 |
204 | krbConf.Realms = []config.Realm{
205 | {
206 | Realm: strings.ToUpper(c.Domain),
207 | DefaultDomain: strings.ToUpper(c.Domain),
208 | AdminServer: []string{dc.AddressWithoutPort()},
209 | KDC: []string{net.JoinHostPort(dc.AddressWithoutPort(), "88")},
210 | KPasswdServer: []string{net.JoinHostPort(dc.AddressWithoutPort(), "464")},
211 | MasterKDC: []string{dc.AddressWithoutPort()},
212 | },
213 | {
214 | Realm: c.Domain,
215 | DefaultDomain: c.Domain,
216 | AdminServer: []string{dc.AddressWithoutPort()},
217 | KDC: []string{net.JoinHostPort(dc.AddressWithoutPort(), "88")},
218 | KPasswdServer: []string{net.JoinHostPort(dc.AddressWithoutPort(), "464")},
219 | MasterKDC: []string{dc.AddressWithoutPort()},
220 | },
221 | }
222 | krbConf.DomainRealm = map[string]string{
223 | "." + c.Domain: strings.ToUpper(c.Domain),
224 | c.Domain: strings.ToUpper(c.Domain),
225 | }
226 |
227 | return krbConf, nil
228 | }
229 |
230 | func splitUserIntoDomainAndUsername(user string) (domain string, username string) {
231 | switch {
232 | case strings.Contains(user, "@"):
233 | parts := strings.Split(user, "@")
234 | if len(parts) == 2 {
235 | return parts[1], parts[0]
236 | }
237 |
238 | return "", user
239 | case strings.Contains(user, `\`):
240 | parts := strings.Split(user, `\`)
241 | if len(parts) == 2 {
242 | return parts[0], parts[1]
243 | }
244 |
245 | return "", user
246 | case strings.Contains(user, "/"):
247 | parts := strings.Split(user, "/")
248 | if len(parts) == 2 {
249 | return parts[0], parts[1]
250 | }
251 |
252 | return "", user
253 | default:
254 | return "", user
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/credentials_test.go:
--------------------------------------------------------------------------------
1 | package adauth_test
2 |
3 | import (
4 | "context"
5 | "net"
6 | "testing"
7 |
8 | "github.com/RedTeamPentesting/adauth"
9 | )
10 |
11 | func TestSetDC(t *testing.T) {
12 | creds := adauth.Credential{
13 | Username: testUser,
14 | Domain: testDomain,
15 | Resolver: &testResolver{},
16 | }
17 |
18 | _, err := creds.DC(context.Background(), "host")
19 | if err == nil {
20 | t.Fatalf("expected creds.DC() to fail initially")
21 | }
22 |
23 | dcHostname := "dc." + testDomain
24 | creds.SetDC(dcHostname)
25 |
26 | dc, err := creds.DC(context.Background(), "host")
27 | if err != nil {
28 | t.Fatalf("get DC: %v", err)
29 | }
30 |
31 | if dc.Address() != dcHostname {
32 | t.Fatalf("DC address is %q instead of %q", dc.Address(), dcHostname)
33 | }
34 | }
35 |
36 | func TestLookupDC(t *testing.T) {
37 | dcHostname := "dc." + testDomain
38 |
39 | creds := adauth.Credential{
40 | Username: testUser,
41 | Domain: testDomain,
42 | Resolver: &testResolver{
43 | SRV: map[string]map[string]map[string]struct {
44 | Name string
45 | SRV []*net.SRV
46 | }{
47 | "kerberos": {
48 | "tcp": {
49 | testDomain: {
50 | Name: dcHostname,
51 | SRV: []*net.SRV{
52 | {Target: dcHostname, Port: 88},
53 | },
54 | },
55 | },
56 | },
57 | },
58 | },
59 | }
60 |
61 | dc, err := creds.DC(context.Background(), "host")
62 | if err != nil {
63 | t.Fatalf("get DC: %v", err)
64 | }
65 |
66 | if dc.AddressWithoutPort() != dcHostname {
67 | t.Fatalf("DC address is %q instead of %q", dc.Address(), dcHostname)
68 | }
69 | }
70 |
71 | func TestUPN(t *testing.T) {
72 | t.Run("user and domain", func(t *testing.T) {
73 | expcetedUPN := "foo@bar"
74 | upn := (&adauth.Credential{Username: "foo", Domain: "bar"}).UPN()
75 |
76 | if upn != expcetedUPN {
77 | t.Errorf("UPN is %q insteaf of %q", upn, expcetedUPN)
78 | }
79 | })
80 |
81 | t.Run("user without domain", func(t *testing.T) {
82 | expcetedUPN := "foo"
83 | upn := (&adauth.Credential{Username: "foo"}).UPN()
84 |
85 | if upn != expcetedUPN {
86 | t.Errorf("UPN is %q insteaf of %q", upn, expcetedUPN)
87 | }
88 | })
89 |
90 | t.Run("domain without username", func(t *testing.T) {
91 | expcetedUPN := "@bar"
92 | upn := (&adauth.Credential{Domain: "bar"}).UPN()
93 |
94 | if upn != expcetedUPN {
95 | t.Errorf("UPN is %q insteaf of %q", upn, expcetedUPN)
96 | }
97 | })
98 |
99 | t.Run("no username and no domain", func(t *testing.T) {
100 | expcetedUPN := ""
101 | upn := (&adauth.Credential{}).UPN()
102 |
103 | if upn != expcetedUPN {
104 | t.Errorf("UPN is %q insteaf of %q", upn, expcetedUPN)
105 | }
106 | })
107 | }
108 |
--------------------------------------------------------------------------------
/dcerpcauth/dcerpcauth.go:
--------------------------------------------------------------------------------
1 | package dcerpcauth
2 |
3 | import (
4 | "context"
5 | "crypto/rsa"
6 | "encoding/hex"
7 | "fmt"
8 | "net"
9 | "strings"
10 |
11 | "github.com/RedTeamPentesting/adauth"
12 | "github.com/RedTeamPentesting/adauth/pkinit"
13 | "github.com/oiweiwei/go-msrpc/dcerpc"
14 | "github.com/oiweiwei/go-msrpc/smb2"
15 | "github.com/oiweiwei/go-msrpc/ssp"
16 | "github.com/oiweiwei/go-msrpc/ssp/credential"
17 | "github.com/oiweiwei/go-msrpc/ssp/gssapi"
18 | "github.com/oiweiwei/go-msrpc/ssp/krb5"
19 | "github.com/oiweiwei/gokrb5.fork/v9/iana/etypeID"
20 | )
21 |
22 | // Options holds options that modify the behavior of the AuthenticationOptions
23 | // function.
24 | type Options struct {
25 | // SMBOptions holds options for the SMB dialer. This dialer is only used
26 | // with the named pipe transport. If SMBOptions is nil, encryption/sealing
27 | // will be enabled for the SMB dialer, specify an empty slice to disable
28 | // this default.
29 | SMBOptions []smb2.DialerOption
30 |
31 | // KerberosDialer is a custom dialer that is used to request Kerberos
32 | // tickets.
33 | KerberosDialer adauth.Dialer
34 |
35 | // Debug can be set to enable debug output, for example with
36 | // adauth.NewDebugFunc(...).
37 | Debug func(string, ...any)
38 | }
39 |
40 | func (opts *Options) debug(format string, a ...any) {
41 | if opts == nil || opts.Debug == nil {
42 | return
43 | }
44 |
45 | opts.Debug(format, a...)
46 | }
47 |
48 | // AuthenticationOptions returns dcerpc.Options for dcerpc.Dial or for
49 | // constructing an DCERPC API client. It is possible to configure the SMB,
50 | // PKINIT and debug behavior using the optional upstreamOptions argument.
51 | func AuthenticationOptions(
52 | ctx context.Context, creds *adauth.Credential, target *adauth.Target,
53 | upstreamOptions *Options,
54 | ) (dcerpcOptions []dcerpc.Option, err error) {
55 | if upstreamOptions == nil {
56 | upstreamOptions = &Options{}
57 | }
58 |
59 | dcerpcCredentials, err := DCERPCCredentials(ctx, creds, upstreamOptions)
60 | if err != nil {
61 | return nil, err
62 | }
63 |
64 | dcerpcOptions = append(dcerpcOptions, dcerpc.WithMechanism(ssp.SPNEGO))
65 |
66 | switch {
67 | case target.UseKerberos || creds.ClientCert != nil:
68 | spn, err := target.SPN(ctx)
69 | if err != nil {
70 | return nil, fmt.Errorf("build SPN: %w", err)
71 | }
72 |
73 | upstreamOptions.debug("Using Kerberos with SPN %q", spn)
74 |
75 | krbConf, err := creds.KerberosConfig(ctx)
76 | if err != nil {
77 | return nil, fmt.Errorf("generate Kerberos config: %w", err)
78 | }
79 |
80 | smbOptions := upstreamOptions.SMBOptions
81 | if smbOptions == nil {
82 | smbOptions = append(smbOptions, smb2.WithSeal())
83 | }
84 |
85 | smbOptions = append(smbOptions, smb2.WithSecurity(
86 | gssapi.WithTargetName(spn),
87 | gssapi.WithCredential(dcerpcCredentials),
88 | gssapi.WithMechanismFactory(ssp.KRB5, &krb5.Config{
89 | KRB5Config: krbConf,
90 | CCachePath: creds.CCache,
91 | DisablePAFXFAST: true,
92 | DCEStyle: true,
93 | KDCDialer: upstreamOptions.KerberosDialer,
94 | }),
95 | ))
96 |
97 | dcerpcOptions = append(dcerpcOptions,
98 | dcerpc.WithTargetName(spn),
99 | dcerpc.WithMechanism(ssp.KRB5),
100 | dcerpc.WithSecurityConfig(&krb5.Config{
101 | KRB5Config: krbConf,
102 | CCachePath: creds.CCache,
103 | DisablePAFXFAST: true,
104 | DCEStyle: true,
105 | KDCDialer: upstreamOptions.KerberosDialer,
106 | }),
107 | dcerpc.WithSMBDialer(smb2.NewDialer(smbOptions...)),
108 | )
109 | default:
110 | upstreamOptions.debug("Using NTLM")
111 |
112 | dcerpcOptions = append(dcerpcOptions,
113 | dcerpc.WithMechanism(ssp.NTLM),
114 | )
115 |
116 | spn, err := target.SPN(ctx)
117 | if err == nil {
118 | dcerpcOptions = append(dcerpcOptions, dcerpc.WithTargetName(spn))
119 | }
120 | }
121 |
122 | dcerpcOptions = append(dcerpcOptions, dcerpc.WithCredentials(dcerpcCredentials))
123 |
124 | return dcerpcOptions, nil
125 | }
126 |
127 | func DCERPCCredentials(ctx context.Context, creds *adauth.Credential, options *Options) (credential.Credential, error) {
128 | switch {
129 | case creds.Password != "":
130 | options.debug("Authenticating with password")
131 |
132 | return credential.NewFromPassword(creds.LogonNameWithUpperCaseDomain(), creds.Password), nil
133 | case creds.AESKey != "":
134 | options.debug("Authenticating with AES key")
135 |
136 | keyBytes, err := hex.DecodeString(creds.AESKey)
137 | if err != nil {
138 | return nil, fmt.Errorf("decode hex key: %w", err)
139 | }
140 |
141 | var keyType int
142 |
143 | switch len(keyBytes) {
144 | case 32:
145 | keyType = int(etypeID.AES256_CTS_HMAC_SHA1_96)
146 | case 16:
147 | keyType = int(etypeID.AES128_CTS_HMAC_SHA1_96)
148 | default:
149 | return nil, fmt.Errorf("invalid AES128/AES256 key: key size is %d bytes", len(keyBytes))
150 | }
151 |
152 | return credential.NewFromEncryptionKeyBytes(creds.LogonNameWithUpperCaseDomain(), keyType, keyBytes), nil
153 | case creds.NTHash != "":
154 | options.debug("Authenticating with NT hash")
155 |
156 | return credential.NewFromNTHash(creds.LogonNameWithUpperCaseDomain(), creds.NTHash), nil
157 | case creds.PasswordIsEmtpyString:
158 | options.debug("Authenticating with empty password")
159 |
160 | return credential.NewFromPassword(strings.ToUpper(creds.Domain)+`\`+creds.Username, ""), nil
161 | case creds.ClientCert != nil:
162 | options.debug("Authenticating with client certificate (PKINIT)")
163 |
164 | krbConf, err := creds.KerberosConfig(ctx)
165 | if err != nil {
166 | return nil, fmt.Errorf("generate kerberos config: %w", err)
167 | }
168 |
169 | dialer := options.KerberosDialer
170 | if dialer == nil {
171 | dialer = &net.Dialer{Timeout: pkinit.DefaultKerberosRoundtripDeadline}
172 | }
173 |
174 | rsaKey, ok := creds.ClientCertKey.(*rsa.PrivateKey)
175 | if !ok {
176 | return nil, fmt.Errorf("cannot use %T because PKINIT requires an RSA key", creds.ClientCertKey)
177 | }
178 |
179 | ccache, err := pkinit.Authenticate(ctx, creds.Username, strings.ToUpper(creds.Domain),
180 | creds.ClientCert, rsaKey, krbConf, pkinit.WithDialer(adauth.AsContextDialer(dialer)))
181 | if err != nil {
182 | return nil, fmt.Errorf("PKINIT: %w", err)
183 | }
184 |
185 | return credential.NewFromCCache(creds.LogonNameWithUpperCaseDomain(), ccache), nil
186 | case creds.CCache != "":
187 | options.debug("Authenticating with ccache")
188 |
189 | return credential.NewFromPassword(creds.LogonNameWithUpperCaseDomain(), ""), nil
190 | default:
191 | return nil, fmt.Errorf("no credentials available")
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/dialer.go:
--------------------------------------------------------------------------------
1 | package adauth
2 |
3 | import (
4 | "context"
5 | "net"
6 | "strings"
7 |
8 | "golang.org/x/net/proxy"
9 | )
10 |
11 | type Dialer interface {
12 | Dial(net string, addr string) (net.Conn, error)
13 | }
14 |
15 | type ContextDialer interface {
16 | DialContext(ctx context.Context, net string, addr string) (net.Conn, error)
17 | Dial(net string, addr string) (net.Conn, error)
18 | }
19 |
20 | type nopContextDialer func(string, string) (net.Conn, error)
21 |
22 | func (f nopContextDialer) DialContext(ctx context.Context, net string, addr string) (net.Conn, error) {
23 | return f(net, addr)
24 | }
25 |
26 | func (f nopContextDialer) Dial(net string, addr string) (net.Conn, error) {
27 | return f(net, addr)
28 | }
29 |
30 | // AsContextDialer converts a Dialer into a ContextDialer that either uses the
31 | // dialer's DialContext method if implemented or it uses a DialContext method
32 | // that simply calls Dial ignoring the context.
33 | func AsContextDialer(d Dialer) ContextDialer {
34 | ctxDialer, ok := d.(ContextDialer)
35 | if !ok {
36 | ctxDialer = nopContextDialer(d.Dial)
37 | }
38 |
39 | return ctxDialer
40 | }
41 |
42 | // SOCKS5Dialer returns a SOCKS5 dialer.
43 | func SOCKS5Dialer(
44 | network string,
45 | address string,
46 | auth *proxy.Auth,
47 | forward *net.Dialer,
48 | ) ContextDialer {
49 | proxyDialer, err := proxy.SOCKS5(network, address, auth, forward)
50 | if err != nil {
51 | return nopContextDialer(func(s1, s2 string) (net.Conn, error) {
52 | return nil, err
53 | })
54 | }
55 |
56 | return AsContextDialer(proxyDialer)
57 | }
58 |
59 | // DialerWithSOCKS5ProxyIfSet returns a SOCKS5 dialer if socks5Server is not
60 | // empty and it returns the forward dialer otherwise.
61 | func DialerWithSOCKS5ProxyIfSet(socks5Server string, forward *net.Dialer) ContextDialer {
62 | if forward == nil {
63 | forward = &net.Dialer{}
64 | }
65 |
66 | if strings.TrimSpace(socks5Server) == "" {
67 | return AsContextDialer(forward)
68 | }
69 |
70 | return SOCKS5Dialer("tcp", socks5Server, nil, forward)
71 | }
72 |
--------------------------------------------------------------------------------
/examples/dcerpc/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "os"
8 | "path/filepath"
9 |
10 | "github.com/RedTeamPentesting/adauth"
11 | "github.com/RedTeamPentesting/adauth/dcerpcauth"
12 | "github.com/oiweiwei/go-msrpc/dcerpc"
13 | "github.com/oiweiwei/go-msrpc/msrpc/dtyp"
14 | "github.com/oiweiwei/go-msrpc/msrpc/epm/epm/v3"
15 | "github.com/oiweiwei/go-msrpc/msrpc/samr/samr/v1"
16 | "github.com/spf13/pflag"
17 | )
18 |
19 | func run() error {
20 | var (
21 | debug bool
22 | socksServer = os.Getenv("SOCKS5_SERVER")
23 | authOpts = &adauth.Options{
24 | Debug: adauth.NewDebugFunc(&debug, os.Stderr, true),
25 | }
26 | dcerpcauthOpts = &dcerpcauth.Options{
27 | Debug: authOpts.Debug,
28 | }
29 | namedPipe bool
30 | )
31 |
32 | pflag.CommandLine.BoolVar(&debug, "debug", false, "Enable debug output")
33 | pflag.CommandLine.StringVar(&socksServer, "socks", socksServer, "SOCKS5 proxy server")
34 | pflag.CommandLine.BoolVar(&namedPipe, "named-pipe", false, "Use named pipe (SMB) as transport")
35 | authOpts.RegisterFlags(pflag.CommandLine)
36 | pflag.Parse()
37 |
38 | if len(pflag.Args()) != 1 {
39 | return fmt.Errorf("usage: %s [options] ", binaryName())
40 | }
41 |
42 | dcerpcauthOpts.KerberosDialer = adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil)
43 |
44 | creds, target, err := authOpts.WithTarget(context.Background(), "host", pflag.Arg(0))
45 | if err != nil {
46 | return err
47 | }
48 |
49 | ctx := context.Background()
50 |
51 | dcerpcOpts, err := dcerpcauth.AuthenticationOptions(ctx, creds, target, dcerpcauthOpts)
52 | if err != nil {
53 | return err
54 | }
55 |
56 | dcerpcOpts = append(dcerpcOpts,
57 | epm.EndpointMapper(ctx,
58 | net.JoinHostPort(target.AddressWithoutPort(), "135"),
59 | dcerpc.WithInsecure(),
60 | ),
61 | dcerpc.WithDialer(adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil)),
62 | )
63 |
64 | proto := "ncacn_ip_tcp:"
65 | if namedPipe {
66 | proto = "ncacn_np:"
67 | }
68 |
69 | conn, err := dcerpc.Dial(ctx, proto+target.Address(), dcerpcOpts...)
70 | if err != nil {
71 | return fmt.Errorf("dial DCERPC: %w", err)
72 | }
73 |
74 | defer conn.Close(ctx) //nolint:errcheck
75 |
76 | samrClient, err := samr.NewSamrClient(ctx, conn, dcerpc.WithSeal())
77 | if err != nil {
78 | return fmt.Errorf("create SAMR client: %w", err)
79 | }
80 |
81 | connectResponse, err := samrClient.Connect(ctx, &samr.ConnectRequest{DesiredAccess: 0x02000000})
82 | if err != nil {
83 | return fmt.Errorf("SAMR connect: %w", err)
84 | }
85 |
86 | lookupDomainResponse, err := samrClient.LookupDomainInSAMServer(ctx, &samr.LookupDomainInSAMServerRequest{
87 | Server: connectResponse.Server,
88 | Name: &dtyp.UnicodeString{
89 | Buffer: creds.Domain,
90 | },
91 | })
92 | if err != nil {
93 | return fmt.Errorf("SAMR lookup domain %q: %w", creds.Domain, err)
94 | }
95 |
96 | fmt.Println("Domain SID:", lookupDomainResponse.DomainID)
97 |
98 | return nil
99 | }
100 |
101 | func binaryName() string {
102 | executable, err := os.Executable()
103 | if err == nil {
104 | return filepath.Base(executable)
105 | }
106 |
107 | if len(os.Args) > 0 {
108 | return filepath.Base(os.Args[0])
109 | }
110 |
111 | return "msrpc"
112 | }
113 |
114 | func main() {
115 | err := run()
116 | if err != nil {
117 | fmt.Fprintf(os.Stderr, "Error: %v\n", err)
118 |
119 | os.Exit(1)
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/examples/ldap/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/RedTeamPentesting/adauth"
9 | "github.com/RedTeamPentesting/adauth/ldapauth"
10 | "github.com/spf13/pflag"
11 | )
12 |
13 | func run() error {
14 | var (
15 | debug bool
16 | socksServer = os.Getenv("SOCKS5_SERVER")
17 | authOpts = &adauth.Options{
18 | Debug: adauth.NewDebugFunc(&debug, os.Stderr, true),
19 | }
20 | ldapOpts = &ldapauth.Options{
21 | Debug: adauth.NewDebugFunc(&debug, os.Stderr, true),
22 | }
23 | )
24 |
25 | pflag.CommandLine.BoolVar(&debug, "debug", false, "Enable debug output")
26 | pflag.CommandLine.StringVar(&socksServer, "socks", socksServer, "SOCKS5 proxy server")
27 | authOpts.RegisterFlags(pflag.CommandLine)
28 | ldapOpts.RegisterFlags(pflag.CommandLine)
29 | pflag.Parse()
30 |
31 | ldapOpts.SetDialer(adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil))
32 |
33 | conn, err := ldapauth.Connect(context.Background(), authOpts, ldapOpts)
34 | if err != nil {
35 | return fmt.Errorf("%s connect: %w", ldapOpts.Scheme, err)
36 | }
37 |
38 | defer conn.Close() //nolint:errcheck
39 |
40 | res, err := conn.WhoAmI(nil)
41 | if err != nil {
42 | return fmt.Errorf("whoami: %w", err)
43 | }
44 |
45 | fmt.Println("whoami:", res.AuthzID)
46 |
47 | return nil
48 | }
49 |
50 | func main() {
51 | err := run()
52 | if err != nil {
53 | fmt.Fprintf(os.Stderr, "Error: %v\n", err)
54 |
55 | os.Exit(1)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/examples/pkinit/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/RedTeamPentesting/adauth"
9 | "github.com/RedTeamPentesting/adauth/ccachetools"
10 | "github.com/RedTeamPentesting/adauth/pkinit"
11 | "github.com/spf13/pflag"
12 | )
13 |
14 | func run() error {
15 | var (
16 | username string
17 | domain string
18 | pfxFile string
19 | pfxPassword string
20 | ccacheName string
21 | dc string
22 | socksServer = os.Getenv("SOCKS5_SERVER")
23 | )
24 |
25 | pflag.StringVarP(&username, "username", "u", "", "Username (overrides UPN in PFX)")
26 | pflag.StringVarP(&domain, "domain", "d", "", "Domain (overrides UPN in PFX)")
27 | pflag.StringVar(&pfxFile, "pfx", "", "PFX file")
28 | pflag.StringVarP(&pfxPassword, "pfx-password", "p", "", "PFX file password")
29 | pflag.StringVar(&ccacheName, "cache", "", "CCache output file name")
30 | pflag.StringVar(&dc, "dc", "", "Domain controller (optional)")
31 | pflag.StringVar(&socksServer, "socks", socksServer, "SOCKS5 server")
32 |
33 | pflag.Parse()
34 |
35 | ccache, hash, err := pkinit.UnPACTheHashFromPFX(context.Background(), username, domain, pfxFile, pfxPassword, dc,
36 | pkinit.WithDialer(adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil)))
37 | if err != nil {
38 | return fmt.Errorf("UnPAC-the-Hash: %w", err)
39 | }
40 |
41 | fmt.Println("Authentication was successful")
42 | fmt.Printf("%s\\%s: %s\n", ccache.DefaultPrincipal.Realm,
43 | ccache.DefaultPrincipal.PrincipalName.PrincipalNameString(), hash.Combined())
44 |
45 | if ccacheName != "" {
46 | ccacheBytes, err := ccachetools.MarshalCCache(ccache)
47 | if err != nil {
48 | return fmt.Errorf("marshal CCache: %w", err)
49 | }
50 |
51 | err = os.WriteFile(ccacheName, ccacheBytes, 0o600)
52 | if err != nil {
53 | return fmt.Errorf("write CCache: %w", err)
54 | }
55 |
56 | fmt.Println("Saved CCache at", ccacheName)
57 | }
58 |
59 | return nil
60 | }
61 |
62 | func main() {
63 | err := run()
64 | if err != nil {
65 | fmt.Fprintf(os.Stderr, "Error: %v\n", err)
66 |
67 | os.Exit(1)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/examples/smb/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/RedTeamPentesting/adauth"
10 | "github.com/RedTeamPentesting/adauth/smbauth"
11 | "github.com/spf13/pflag"
12 | )
13 |
14 | func run() error {
15 | var (
16 | debug bool
17 | socksServer = os.Getenv("SOCKS5_SERVER")
18 | authOpts = &adauth.Options{
19 | Debug: adauth.NewDebugFunc(&debug, os.Stderr, true),
20 | }
21 | smbauthOpts = &smbauth.Options{
22 | Debug: authOpts.Debug,
23 | }
24 | )
25 |
26 | pflag.CommandLine.BoolVar(&debug, "debug", false, "Enable debug output")
27 | pflag.CommandLine.StringVar(&socksServer, "socks", socksServer, "SOCKS5 proxy server")
28 | authOpts.RegisterFlags(pflag.CommandLine)
29 | pflag.Parse()
30 |
31 | if len(pflag.Args()) != 1 {
32 | return fmt.Errorf("usage: %s [options] ", binaryName())
33 | }
34 |
35 | creds, target, err := authOpts.WithTarget(context.Background(), "host", pflag.Arg(0))
36 | if err != nil {
37 | return err
38 | }
39 |
40 | if target.Port == "" {
41 | target.Port = "445"
42 | }
43 |
44 | ctx := context.Background()
45 |
46 | smbauthOpts.KerberosDialer = adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil)
47 |
48 | smbDialer, err := smbauth.Dialer(ctx, creds, target, smbauthOpts)
49 | if err != nil {
50 | return fmt.Errorf("setup SMB authentication: %w", err)
51 | }
52 |
53 | conn, err := adauth.DialerWithSOCKS5ProxyIfSet(socksServer, nil).DialContext(ctx, "tcp", target.Address())
54 | if err != nil {
55 | return fmt.Errorf("dial: %w", err)
56 | }
57 |
58 | defer conn.Close()
59 |
60 | sess, err := smbDialer.DialContext(ctx, conn)
61 | if err != nil {
62 | return fmt.Errorf("create session: %w", err)
63 | }
64 |
65 | defer sess.Logoff()
66 |
67 | shares, err := sess.ListSharenames()
68 | if err != nil {
69 | return fmt.Errorf("list share names: %w", err)
70 | }
71 |
72 | if len(shares) == 0 {
73 | fmt.Println("No shares available")
74 |
75 | return nil
76 | }
77 |
78 | fmt.Println("Shares:")
79 |
80 | for _, share := range shares {
81 | fmt.Printf(" - %s\n", share)
82 | }
83 |
84 | return nil
85 | }
86 |
87 | func binaryName() string {
88 | executable, err := os.Executable()
89 | if err == nil {
90 | return filepath.Base(executable)
91 | }
92 |
93 | if len(os.Args) > 0 {
94 | return filepath.Base(os.Args[0])
95 | }
96 |
97 | return "smb"
98 | }
99 |
100 | func main() {
101 | err := run()
102 | if err != nil {
103 | fmt.Fprintf(os.Stderr, "Error: %v\n", err)
104 |
105 | os.Exit(1)
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/RedTeamPentesting/adauth
2 |
3 | go 1.23.3
4 |
5 | require (
6 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
7 | github.com/go-ldap/ldap/v3 v3.4.11
8 | github.com/jcmturner/gokrb5/v8 v8.4.4
9 | github.com/oiweiwei/go-msrpc v1.2.5
10 | github.com/oiweiwei/go-smb2.fork v1.0.0
11 | github.com/oiweiwei/gokrb5.fork/v9 v9.0.2
12 | github.com/spf13/pflag v1.0.6
13 | github.com/vadimi/go-ntlm v1.2.1
14 | golang.org/x/net v0.39.0
15 | software.sslmate.com/src/go-pkcs12 v0.5.0
16 | )
17 |
18 | require (
19 | github.com/geoffgarside/ber v1.1.0 // indirect
20 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
21 | github.com/google/uuid v1.6.0 // indirect
22 | github.com/hashicorp/go-uuid v1.0.3 // indirect
23 | github.com/indece-official/go-ebcdic v1.2.0 // indirect
24 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect
25 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
26 | github.com/jcmturner/gofork v1.7.6 // indirect
27 | github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
28 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect
29 | github.com/mattn/go-colorable v0.1.14 // indirect
30 | github.com/mattn/go-isatty v0.0.20 // indirect
31 | github.com/rs/zerolog v1.34.0 // indirect
32 | golang.org/x/crypto v0.37.0 // indirect
33 | golang.org/x/sys v0.32.0 // indirect
34 | golang.org/x/text v0.24.0 // indirect
35 | )
36 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
2 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
3 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
4 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
5 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9 | github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w=
10 | github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
11 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
12 | github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
13 | github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
14 | github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
15 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
16 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
17 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
18 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
19 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
20 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
21 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
22 | github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
23 | github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
24 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
25 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
26 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
27 | github.com/indece-official/go-ebcdic v1.2.0 h1:nKCubkNoXrGvBp3MSYuplOQnhANCDEY512Ry5Mwr4a0=
28 | github.com/indece-official/go-ebcdic v1.2.0/go.mod h1:RBddVJt0Ks0eDLRG5dhPwBDRiTNA7n+yv0dVFpSs46Q=
29 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
30 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
31 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
32 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
33 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
34 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
35 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
36 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
37 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
38 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
39 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
40 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
41 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
42 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
43 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
44 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
45 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
46 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
47 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
48 | github.com/oiweiwei/go-msrpc v1.2.5 h1:nIWoU7MWLk5l8vb0pgQ+D67GjDRPC4ybiR+OJtgDWdk=
49 | github.com/oiweiwei/go-msrpc v1.2.5/go.mod h1:WoWRPfm90vRNZDJCwOiUXy39vjyQMAFrFj0zkWTThwY=
50 | github.com/oiweiwei/go-smb2.fork v1.0.0 h1:xHq/eYPM8hQEO/nwCez8YwHWHC8mlcsgw/Neu52fPN4=
51 | github.com/oiweiwei/go-smb2.fork v1.0.0/go.mod h1:h0CzLVvGAmq39izdYVHKyI5cLv6aHdbQAMKEe4dz4N8=
52 | github.com/oiweiwei/gokrb5.fork/v9 v9.0.2 h1:JNkvXMuOEWNXJKzLiyROGfdK31/1RQWA9e5gJxAsl50=
53 | github.com/oiweiwei/gokrb5.fork/v9 v9.0.2/go.mod h1:KEnkAYUYqZ5VwzxLFbv3JHlRhCvdFahjrdjjssMJJkI=
54 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
57 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
58 | github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
59 | github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
60 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
61 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
62 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
63 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
64 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
65 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
66 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
67 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
68 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
69 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
70 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
71 | github.com/vadimi/go-ntlm v1.2.1 h1:y2xZf/a5+BJlYNJIIulP1q8F438H9bU7aGcYE53vghQ=
72 | github.com/vadimi/go-ntlm v1.2.1/go.mod h1:hPTY60eLSKGj9oUJAB+kZiLs2Cg5eKdH60aLczM9rMg=
73 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
74 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
75 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
76 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
77 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
78 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
79 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
80 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
81 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
82 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
83 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
84 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
85 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
86 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
87 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
88 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
89 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
90 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
91 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
92 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
93 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
94 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
95 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
96 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
98 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
100 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
101 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
102 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
103 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
104 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
105 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
106 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
107 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
108 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
109 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
110 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
111 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
112 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
113 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
114 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
115 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
116 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
117 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
118 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
119 | software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
120 | software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
121 |
--------------------------------------------------------------------------------
/ldapauth/gssapi.go:
--------------------------------------------------------------------------------
1 | package ldapauth
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "crypto/rsa"
7 | "crypto/x509"
8 | "encoding/binary"
9 | "encoding/hex"
10 | "errors"
11 | "fmt"
12 | "strings"
13 |
14 | "github.com/RedTeamPentesting/adauth"
15 | "github.com/RedTeamPentesting/adauth/compat"
16 | "github.com/RedTeamPentesting/adauth/pkinit"
17 | "github.com/jcmturner/gokrb5/v8/config"
18 | "github.com/oiweiwei/gokrb5.fork/v9/client"
19 | "github.com/oiweiwei/gokrb5.fork/v9/credentials"
20 | krb5Crypto "github.com/oiweiwei/gokrb5.fork/v9/crypto"
21 | krb5GSSAPI "github.com/oiweiwei/gokrb5.fork/v9/gssapi"
22 | "github.com/oiweiwei/gokrb5.fork/v9/iana/chksumtype"
23 | "github.com/oiweiwei/gokrb5.fork/v9/iana/etypeID"
24 | "github.com/oiweiwei/gokrb5.fork/v9/iana/flags"
25 | "github.com/oiweiwei/gokrb5.fork/v9/iana/keyusage"
26 | "github.com/oiweiwei/gokrb5.fork/v9/iana/nametype"
27 | "github.com/oiweiwei/gokrb5.fork/v9/krberror"
28 | "github.com/oiweiwei/gokrb5.fork/v9/messages"
29 | "github.com/oiweiwei/gokrb5.fork/v9/spnego"
30 | "github.com/oiweiwei/gokrb5.fork/v9/types"
31 | )
32 |
33 | type gssapiClient struct {
34 | *client.Client
35 | ccache *credentials.CCache
36 |
37 | ekey types.EncryptionKey
38 | Subkey types.EncryptionKey
39 |
40 | BindCertificate *x509.Certificate
41 | }
42 |
43 | func newClientFromCCache(
44 | username string, domain string, ccachePath string, krb5Conf *config.Config, dialer adauth.Dialer,
45 | ) (*gssapiClient, error) {
46 | ccache, err := credentials.LoadCCache(ccachePath)
47 | if err != nil {
48 | return nil, err
49 | }
50 |
51 | c, err := client.NewFromCCache(
52 | ccache, compat.Gokrb5ForkV9KerberosConfig(krb5Conf),
53 | client.DisablePAFXFAST(true), client.Dialer(dialer))
54 | if err != nil && strings.Contains(strings.ToLower(err.Error()), "tgt") {
55 | // client.NewFromCCache only accepts CCaches that contain at least one
56 | // TGT, however, we want to support CCaches that only contain a service
57 | // ticket. Therefore, we use a dummy client, and pull the service ticket
58 | // from the ccache ourselves instead of asking the client.
59 | return &gssapiClient{
60 | Client: client.NewWithPassword(
61 | username, domain, "", compat.Gokrb5ForkV9KerberosConfig(krb5Conf), client.DisablePAFXFAST(true)),
62 | ccache: ccache,
63 | }, nil
64 | }
65 |
66 | if err != nil {
67 | return nil, err
68 | }
69 |
70 | return &gssapiClient{Client: c}, nil
71 | }
72 |
73 | func newPKINITClient(
74 | ctx context.Context, username string, domain string, cert *x509.Certificate, key *rsa.PrivateKey,
75 | krb5Conf *config.Config, dialer adauth.Dialer,
76 | ) (*gssapiClient, error) {
77 | ctxDialer := adauth.AsContextDialer(dialer)
78 |
79 | ccache, err := pkinit.Authenticate(ctx, username, domain, cert, key, krb5Conf, pkinit.WithDialer(ctxDialer))
80 | if err != nil {
81 | return nil, fmt.Errorf("pkinit: %w", err)
82 | }
83 |
84 | c, err := client.NewFromCCache(
85 | compat.Gokrb5ForkV9CCache(ccache), compat.Gokrb5ForkV9KerberosConfig(krb5Conf),
86 | client.DisablePAFXFAST(true), client.Dialer(dialer))
87 | if err != nil {
88 | return nil, fmt.Errorf("initialize Kerberos client from PKINIT ccache: %w", err)
89 | }
90 |
91 | return &gssapiClient{Client: c}, nil
92 | }
93 |
94 | // Close deletes any established secure context and closes the client.
95 | func (client *gssapiClient) Close() error {
96 | client.Destroy()
97 |
98 | return nil
99 | }
100 |
101 | // DeleteSecContext destroys any established secure context.
102 | func (client *gssapiClient) DeleteSecContext() error {
103 | client.ekey = types.EncryptionKey{}
104 | client.Subkey = types.EncryptionKey{}
105 |
106 | return nil
107 | }
108 |
109 | func (client *gssapiClient) getServiceTicket(target string) (tkt messages.Ticket, key types.EncryptionKey, err error) {
110 | // ask our own copy of the ccache for a suitable service ticket before asking the client
111 | if client.ccache != nil {
112 | entry, ok := client.ccache.GetEntry(types.NewPrincipalName(nametype.KRB_NT_SRV_INST, target))
113 | if !ok {
114 | return tkt, key, fmt.Errorf("CCACHE does not contain service ticket for %q", target)
115 | }
116 |
117 | if entry.Key.KeyType == etypeID.RC4_HMAC {
118 | return tkt, key, fmt.Errorf("RC4 tickets from ccache are currently not supported " +
119 | "(see https://github.com/jcmturner/gokrb5/pull/498), but you should be able " +
120 | "to request an AES256 ticket instead (even with NT hash)")
121 | }
122 |
123 | return tkt, entry.Key, tkt.Unmarshal(entry.Ticket)
124 | }
125 |
126 | return client.GetServiceTicket(target)
127 | }
128 |
129 | func (client *gssapiClient) newKRB5TokenAPREQ(
130 | tkt messages.Ticket, ekey types.EncryptionKey, apOptions []int,
131 | ) (*spnego.KRB5Token, error) {
132 | gssapiFlags := []int{krb5GSSAPI.ContextFlagInteg, krb5GSSAPI.ContextFlagConf, krb5GSSAPI.ContextFlagMutual}
133 |
134 | // this actually does nothing important, we simply use it to obtain a dummy
135 | // KRB5Token with tokID set, which unfortunately is private, so we cannot
136 | // initialize it ourselves.
137 | token, err := spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, apOptions)
138 | if err != nil {
139 | return nil, err
140 | }
141 |
142 | // build a custom authenticator that supports channel binding
143 | authenticator, err := krb5TokenAuthenticator(
144 | client.Credentials.Realm(), client.Credentials.CName(), client.BindCertificate, gssapiFlags)
145 | if err != nil {
146 | return nil, fmt.Errorf("create authenticator: %w", err)
147 | }
148 |
149 | APReq, err := messages.NewAPReq(
150 | tkt,
151 | ekey,
152 | authenticator,
153 | )
154 | if err != nil {
155 | return nil, err
156 | }
157 |
158 | types.SetFlag(&APReq.APOptions, flags.APOptionMutualRequired)
159 |
160 | // put the APReq with custom authenticator into the dummy RKB5Token
161 | token.APReq = APReq
162 |
163 | return &token, nil
164 | }
165 |
166 | func (client *gssapiClient) InitSecContext(target string, input []byte) ([]byte, bool, error) {
167 | return client.InitSecContextWithOptions(target, input, []int{flags.APOptionMutualRequired})
168 | }
169 |
170 | func (client *gssapiClient) InitSecContextWithOptions(
171 | target string, input []byte, options []int,
172 | ) (outputToken []byte, needContinue bool, err error) {
173 | switch input {
174 | case nil:
175 | tkt, ekey, err := client.getServiceTicket(target)
176 | if err != nil {
177 | return nil, false, err
178 | }
179 |
180 | client.ekey = ekey
181 |
182 | token, err := client.newKRB5TokenAPREQ(tkt, ekey, options)
183 | if err != nil {
184 | return nil, false, err
185 | }
186 |
187 | output, err := token.Marshal()
188 | if err != nil {
189 | return nil, false, err
190 | }
191 |
192 | return output, true, nil
193 |
194 | default:
195 | var token spnego.KRB5Token
196 |
197 | err := token.Unmarshal(input)
198 | if err != nil {
199 | return nil, false, err
200 | }
201 |
202 | var completed bool
203 |
204 | if token.IsAPRep() {
205 | completed = true
206 |
207 | encpart, err := krb5Crypto.DecryptEncPart(token.APRep.EncPart, client.ekey, keyusage.AP_REP_ENCPART)
208 | if err != nil {
209 | return nil, false, err
210 | }
211 |
212 | part := &messages.EncAPRepPart{}
213 |
214 | if err = part.Unmarshal(encpart); err != nil {
215 | return nil, false, err
216 | }
217 |
218 | client.Subkey = part.Subkey
219 | }
220 |
221 | if token.IsKRBError() {
222 | return nil, !false, token.KRBError
223 | }
224 |
225 | return make([]byte, 0), !completed, nil
226 | }
227 | }
228 |
229 | // Fixed version of SASL authentication based on https://github.com/go-ldap/ldap/pull/537.
230 | func (client *gssapiClient) NegotiateSaslAuth(input []byte, authzid string) ([]byte, error) {
231 | token := &krb5GSSAPI.WrapToken{}
232 |
233 | err := unmarshalWrapToken(token, input, true)
234 | if err != nil {
235 | return nil, err
236 | }
237 |
238 | if (token.Flags & 0b1) == 0 {
239 | return nil, fmt.Errorf("got a Wrapped token that's not from the server")
240 | }
241 |
242 | key := client.ekey
243 | if (token.Flags & 0b100) != 0 {
244 | key = client.Subkey
245 | }
246 |
247 | _, err = token.Verify(key, keyusage.GSSAPI_ACCEPTOR_SEAL)
248 | if err != nil {
249 | return nil, err
250 | }
251 |
252 | pl := token.Payload
253 | if len(pl) != 4 {
254 | return nil, fmt.Errorf("server send bad final token for SASL GSSAPI Handshake")
255 | }
256 |
257 | // We never want a security layer
258 | payload := []byte{0, 0, 0, 0}
259 |
260 | encType, err := krb5Crypto.GetEtype(key.KeyType)
261 | if err != nil {
262 | return nil, err
263 | }
264 |
265 | token = &krb5GSSAPI.WrapToken{
266 | Flags: 0b100,
267 | EC: uint16(encType.GetHMACBitLength() / 8),
268 | RRC: 0,
269 | SndSeqNum: 1,
270 | Payload: payload,
271 | }
272 |
273 | if err := token.SetCheckSum(key, keyusage.GSSAPI_INITIATOR_SEAL); err != nil {
274 | return nil, err
275 | }
276 |
277 | output, err := token.Marshal()
278 | if err != nil {
279 | return nil, err
280 | }
281 |
282 | return output, nil
283 | }
284 |
285 | func unmarshalWrapToken(wt *krb5GSSAPI.WrapToken, data []byte, expectFromAcceptor bool) error {
286 | // Check if we can read a whole header
287 | if len(data) < 16 {
288 | return errors.New("bytes shorter than header length")
289 | }
290 |
291 | // Is the Token ID correct?
292 | expectedWrapTokenId := []byte{0x05, 0x04}
293 | if !bytes.Equal(expectedWrapTokenId, data[0:2]) {
294 | return fmt.Errorf("wrong Token ID. Expected %s, was %s",
295 | hex.EncodeToString(expectedWrapTokenId), hex.EncodeToString(data[0:2]))
296 | }
297 |
298 | // Check the acceptor flag
299 | flags := data[2]
300 | isFromAcceptor := flags&0x01 == 1
301 |
302 | if isFromAcceptor && !expectFromAcceptor {
303 | return errors.New("unexpected acceptor flag is set: not expecting a token from the acceptor")
304 | }
305 |
306 | if !isFromAcceptor && expectFromAcceptor {
307 | return errors.New("expected acceptor flag is not set: expecting a token from the acceptor, not the initiator")
308 | }
309 |
310 | // Check the filler byte
311 | if data[3] != krb5GSSAPI.FillerByte {
312 | return fmt.Errorf("unexpected filler byte: expecting 0xFF, was %s ", hex.EncodeToString(data[3:4]))
313 | }
314 |
315 | checksumL := binary.BigEndian.Uint16(data[4:6])
316 |
317 | // Sanity check on the checksum length
318 | if int(checksumL) > len(data)-krb5GSSAPI.HdrLen {
319 | return fmt.Errorf("inconsistent checksum length: %d bytes to parse, checksum length is %d",
320 | len(data), checksumL)
321 | }
322 |
323 | payloadStart := 16 + checksumL
324 |
325 | wt.Flags = flags
326 | wt.EC = checksumL
327 | wt.RRC = binary.BigEndian.Uint16(data[6:8])
328 | wt.SndSeqNum = binary.BigEndian.Uint64(data[8:16])
329 | wt.CheckSum = data[16:payloadStart]
330 | wt.Payload = data[payloadStart:]
331 |
332 | return nil
333 | }
334 |
335 | func krb5TokenAuthenticator(
336 | realm string, cname types.PrincipalName, cert *x509.Certificate, flags []int,
337 | ) (types.Authenticator, error) {
338 | // RFC 4121 Section 4.1.1
339 | auth, err := types.NewAuthenticator(realm, cname)
340 | if err != nil {
341 | return auth, krberror.Errorf(err, krberror.KRBMsgError, "error generating new authenticator")
342 | }
343 |
344 | // https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1
345 | checksum := make([]byte, 24)
346 |
347 | if cert != nil {
348 | hash := ChannelBindingHash(cert)
349 | if len(hash) != 16 {
350 | return auth, fmt.Errorf("unexpected channel binding hash size: %d, expected 16", len(hash))
351 | }
352 |
353 | binary.LittleEndian.PutUint32(checksum[:4], uint32(len(hash)))
354 | copy(checksum[4:20], hash)
355 | }
356 |
357 | binary.LittleEndian.PutUint32(checksum[:4], 16)
358 |
359 | for _, flag := range flags {
360 | if flag == krb5GSSAPI.ContextFlagDeleg {
361 | checksum = append(checksum, make([]byte, 28-len(checksum))...) //nolint:makezero
362 | }
363 |
364 | f := binary.LittleEndian.Uint32(checksum[20:24])
365 | f |= uint32(flag)
366 |
367 | binary.LittleEndian.PutUint32(checksum[20:24], f)
368 | }
369 |
370 | auth.Cksum = types.Checksum{
371 | CksumType: chksumtype.GSSAPI,
372 | Checksum: checksum,
373 | }
374 |
375 | return auth, nil
376 | }
377 |
--------------------------------------------------------------------------------
/ldapauth/ldap.go:
--------------------------------------------------------------------------------
1 | package ldapauth
2 |
3 | import (
4 | "context"
5 | "crypto"
6 | "crypto/ecdh"
7 | "crypto/ecdsa"
8 | "crypto/ed25519"
9 | "crypto/md5"
10 | "crypto/rsa"
11 | "crypto/tls"
12 | "crypto/x509"
13 | "encoding/binary"
14 | "encoding/hex"
15 | "encoding/pem"
16 | "fmt"
17 | "net"
18 | "os"
19 | "strings"
20 | "time"
21 |
22 | "github.com/RedTeamPentesting/adauth"
23 | "github.com/RedTeamPentesting/adauth/compat"
24 | "github.com/RedTeamPentesting/adauth/othername"
25 | "github.com/RedTeamPentesting/adauth/pkinit"
26 | "github.com/go-ldap/ldap/v3"
27 | "github.com/oiweiwei/gokrb5.fork/v9/client"
28 | "github.com/oiweiwei/gokrb5.fork/v9/iana/etypeID"
29 | "github.com/oiweiwei/gokrb5.fork/v9/iana/flags"
30 | "github.com/oiweiwei/gokrb5.fork/v9/types"
31 | "github.com/spf13/pflag"
32 | "software.sslmate.com/src/go-pkcs12"
33 | )
34 |
35 | // Options holds LDAP specific options.
36 | type Options struct {
37 | // LDAP scheme (ldap or ldaps).
38 | Scheme string
39 | // Verify indicates whether TLS verification should be performed.
40 | Verify bool
41 | // Timeout sets the request timeout for the LDAP connection.
42 | Timeout time.Duration
43 | // Debug can be set to enable debug output, for example with
44 | // adauth.NewDebugFunc(...).
45 | Debug func(string, ...any)
46 | // SimpleBind indicates that SimpleBind authentication should be used
47 | // instead of NTLM, Kerberos or mTLS. For this, a cleartext password is
48 | // required.
49 | SimpleBind bool
50 | // TLSConfig for LDAPS or LDAP+StartTLS. InsecureSkipVerify is ignored and
51 | // set according to Options.Verify. MaxVersion will be changed to 1.2 unless
52 | // Options.DisableChannelBinding is set.
53 | TLSConfig *tls.Config
54 | // DisableChannelBinding omits the TLS certificate hash in Kerberos and NTLM
55 | // authentication.
56 | DisableChannelBinding bool
57 | // StartTLS indicates that a TLS connection should be established even for
58 | // non-LDAPS connections before authenticating. For client-certificate
59 | // authentication on regular LDAP connections, StartTLS will be used even if
60 | // this option is disabled.
61 | StartTLS bool
62 | // DialOptions can be used to customize the connection. DialOptions is
63 | // ignored when a custom LDAPDialer is set.
64 | DialOptions []ldap.DialOpt
65 | // KerberosDialer is a custom dialer that is used to request Kerberos
66 | // tickets. DialContext is used if implemented.
67 | KerberosDialer adauth.Dialer
68 | // LDAPDialer is a custom dialer that is used to establish LDAP connections.
69 | // DialContext is used if implemented.
70 | LDAPDialer adauth.Dialer
71 | }
72 |
73 | // RegisterFlags registers LDAP specific flags to a pflag.FlagSet such as the
74 | // default flagset pflag.CommandLine.
75 | func (opts *Options) RegisterFlags(flagset *pflag.FlagSet) {
76 | flagset.StringVar(&opts.Scheme, "scheme", "ldaps", "Scheme (ldap or ldaps)")
77 | flagset.DurationVar(&opts.Timeout, "timeout", 5*time.Second, "LDAP connection timeout")
78 | flagset.BoolVar(&opts.SimpleBind, "simple-bind", false, "Authenticate with simple bind")
79 | flagset.BoolVar(&opts.Verify, "verify", false, "Verify LDAP TLS certificate")
80 | flagset.BoolVar(&opts.StartTLS, "start-tls", false,
81 | "Negotiate StartTLS before authenticating on regular LDAP connection")
82 | }
83 |
84 | // SetDialer configures a dialer for LDAP and Kerberos.
85 | func (opts *Options) SetDialer(dialer adauth.Dialer) {
86 | opts.KerberosDialer = dialer
87 | opts.LDAPDialer = dialer
88 | }
89 |
90 | // Connect returns an authenticated LDAP connection to the domain controller's
91 | // LDAP server.
92 | func Connect(ctx context.Context, authOpts *adauth.Options, ldapOpts *Options) (conn *ldap.Conn, err error) {
93 | creds, target, err := authOpts.WithDCTarget(ctx, ldapOpts.Scheme)
94 | if err != nil {
95 | return nil, err
96 | }
97 |
98 | return ConnectTo(ctx, creds, target, ldapOpts)
99 | }
100 |
101 | // Connect returns an authenticated LDAP connection to the specified target.
102 | func ConnectTo(
103 | ctx context.Context, creds *adauth.Credential, target *adauth.Target, ldapOpts *Options,
104 | ) (conn *ldap.Conn, err error) {
105 | opts := ldapOpts
106 | if opts.Debug == nil {
107 | opts.Debug = func(s string, a ...any) {}
108 | }
109 |
110 | opts.TLSConfig, err = TLSConfig(ldapOpts, creds.ClientCert, creds.ClientCertKey, creds.CACerts)
111 | if err != nil {
112 | return nil, fmt.Errorf("configure TLS: %w", err)
113 | }
114 |
115 | if !ldapOpts.TLSConfig.InsecureSkipVerify && net.ParseIP(target.AddressWithoutPort()) != nil {
116 | hostname, err := target.Hostname(ctx)
117 | if err != nil {
118 | return nil, fmt.Errorf("determine target hostname for TLS verification: %w", err)
119 | }
120 |
121 | opts.TLSConfig.ServerName = hostname
122 | }
123 |
124 | conn, err = connect(ctx, target, opts)
125 | if err != nil {
126 | return nil, err
127 | }
128 |
129 | if opts.Timeout == 0 {
130 | conn.SetTimeout(3 * time.Second)
131 | } else {
132 | conn.SetTimeout(opts.Timeout)
133 | }
134 |
135 | err = bind(ctx, conn, creds, target, opts)
136 | if err != nil {
137 | return nil, err
138 | }
139 |
140 | return conn, nil
141 | }
142 |
143 | func connect(ctx context.Context, target *adauth.Target, opts *Options) (conn *ldap.Conn, err error) {
144 | switch {
145 | case strings.EqualFold(opts.Scheme, "ldaps"):
146 | if target.Port == "" {
147 | target.Port = ldap.DefaultLdapsPort
148 | }
149 |
150 | if opts.LDAPDialer == nil {
151 | conn, err = ldap.DialURL("ldaps://"+target.Address(),
152 | append(opts.DialOptions, ldap.DialWithTLSConfig(opts.TLSConfig))...)
153 | if err != nil {
154 | return nil, fmt.Errorf("LDAPS dial: %w", err)
155 | }
156 | } else {
157 | tcpConn, err := adauth.AsContextDialer(opts.LDAPDialer).DialContext(ctx, "tcp", target.Address())
158 | if err != nil {
159 | return nil, fmt.Errorf("dial with custom dialer: %w", err)
160 | }
161 |
162 | tlsConn := tls.Client(tcpConn, opts.TLSConfig)
163 |
164 | err = tlsConn.Handshake()
165 | if err != nil {
166 | return nil, err
167 | }
168 |
169 | conn = ldap.NewConn(tlsConn, true)
170 | conn.Start()
171 | }
172 |
173 | opts.Debug("connected to LDAPS server %s", target.Address())
174 |
175 | return conn, nil
176 | case strings.EqualFold(opts.Scheme, "ldap"):
177 | if target.Port == "" {
178 | target.Port = ldap.DefaultLdapPort
179 | }
180 |
181 | if opts.LDAPDialer == nil {
182 | conn, err = ldap.DialURL("ldap://"+target.Address(), opts.DialOptions...)
183 | if err != nil {
184 | return nil, fmt.Errorf("LDAP dial: %w", err)
185 | }
186 | } else {
187 | tcpConn, err := adauth.AsContextDialer(opts.LDAPDialer).DialContext(ctx, "tcp", target.Address())
188 | if err != nil {
189 | return nil, fmt.Errorf("dial with custom dialer: %w", err)
190 | }
191 |
192 | conn = ldap.NewConn(tcpConn, false)
193 | conn.Start()
194 | }
195 |
196 | opts.Debug("connected to LDAP server %s", target.Address())
197 |
198 | if opts.StartTLS {
199 | opts.Debug("negotiating StartTLS")
200 |
201 | err = conn.StartTLS(opts.TLSConfig)
202 | if err != nil {
203 | _ = conn.Close()
204 |
205 | return nil, fmt.Errorf("StartTLS: %w", err)
206 | }
207 | }
208 |
209 | return conn, nil
210 | default:
211 | return nil, fmt.Errorf("invalid scheme: %q", opts.Scheme)
212 | }
213 | }
214 |
215 | func bind(
216 | ctx context.Context, conn *ldap.Conn, creds *adauth.Credential, target *adauth.Target, opts *Options,
217 | ) (err error) {
218 | switch {
219 | case opts.SimpleBind:
220 | switch {
221 | case creds.Password == "" && !creds.PasswordIsEmtpyString:
222 | return fmt.Errorf("specify a password for simple bind or -p '' for an unauthenticated simple bind")
223 | case creds.Password == "" && creds.PasswordIsEmtpyString:
224 | opts.Debug("using unauthenticated simple bind")
225 | default:
226 | opts.Debug("authenticating with simple bind")
227 | }
228 |
229 | _, err = conn.SimpleBind(&ldap.SimpleBindRequest{
230 | Username: creds.UPN(),
231 | Password: creds.Password,
232 | AllowEmptyPassword: creds.PasswordIsEmtpyString,
233 | })
234 | if err != nil {
235 | return fmt.Errorf("simple bind: %w", err)
236 | }
237 | case !target.UseKerberos && creds.ClientCert == nil:
238 | opts.Debug("authenticating using NTLM bind")
239 |
240 | if !creds.PasswordIsEmtpyString && (creds.Password == "" && creds.NTHash == "") {
241 | return fmt.Errorf("no credentials available for NTLM")
242 | }
243 |
244 | bindRequest := &ldap.NTLMBindRequest{
245 | Domain: creds.Domain,
246 | Username: creds.Username,
247 | Password: creds.Password,
248 | Hash: creds.NTHash,
249 | AllowEmptyPassword: creds.PasswordIsEmtpyString,
250 | }
251 |
252 | tlsState, ok := conn.TLSConnectionState()
253 | if ok && !opts.DisableChannelBinding {
254 | bindRequest.Negotiator = ntlmNegotiatorWithChannelBinding(tlsState.PeerCertificates[0], creds.Domain)
255 | } else {
256 | bindRequest.Negotiator = ntlmNegotiatorForDomain(creds.Domain)
257 | }
258 |
259 | _, err = conn.NTLMChallengeBind(bindRequest)
260 | if err != nil {
261 | return fmt.Errorf("NTLM bind: %w", err)
262 | }
263 | case target.UseKerberos:
264 | authClient, err := kerberosClient(ctx, conn, creds, opts)
265 | if err != nil {
266 | return err
267 | }
268 |
269 | spn, err := target.SPN(ctx)
270 | if err != nil {
271 | return fmt.Errorf("build SPN: %w", err)
272 | }
273 |
274 | err = conn.GSSAPIBindRequestWithAPOptions(authClient, &ldap.GSSAPIBindRequest{
275 | ServicePrincipalName: spn,
276 | AuthZID: creds.NTHash,
277 | }, []int{flags.APOptionMutualRequired})
278 | if err != nil {
279 | return fmt.Errorf("GSSAPI bind: %w", err)
280 | }
281 | case creds.ClientCert != nil && strings.EqualFold(opts.Scheme, "ldap"):
282 | opts.Debug("authenticating with client certificate via StartTLS")
283 |
284 | _, ok := conn.TLSConnectionState()
285 | if !ok {
286 | opts.Debug("negotiating StartTLS")
287 |
288 | err = conn.StartTLS(opts.TLSConfig)
289 | if err != nil {
290 | return fmt.Errorf("StartTLS: %w", err)
291 | }
292 | }
293 |
294 | err = conn.ExternalBind()
295 | if err != nil {
296 | if creds.ClientCert.Issuer.CommonName == "" ||
297 | strings.EqualFold(creds.ClientCert.Subject.CommonName, creds.ClientCert.Issuer.CommonName) {
298 | return fmt.Errorf("external bind: %w "+
299 | "(certificate likely belongs to a KeyCredentialLink, try PKINIT with -k instead)", err)
300 | }
301 |
302 | return fmt.Errorf("external bind: %w", err)
303 | }
304 | case creds.ClientCert != nil:
305 | opts.Debug("authenticating with client certificate")
306 |
307 | res, err := conn.WhoAmI(nil)
308 | if err != nil {
309 | return fmt.Errorf("send whoami to verify certificate authentication: %w", err)
310 | }
311 |
312 | if res.AuthzID == "" {
313 | if creds.ClientCert.Issuer.CommonName == "" ||
314 | strings.EqualFold(creds.ClientCert.Subject.CommonName, creds.ClientCert.Issuer.CommonName) {
315 | return fmt.Errorf("client certificate authentication failed " +
316 | "(certificate likely belongs to a KeyCredentialLink, try PKINIT with -k instead)")
317 | }
318 |
319 | return fmt.Errorf("client certificate authentication failed")
320 | }
321 | default:
322 | return fmt.Errorf("no credentials available")
323 | }
324 |
325 | return nil
326 | }
327 |
328 | func kerberosClient(
329 | ctx context.Context, conn *ldap.Conn, creds *adauth.Credential, opts *Options,
330 | ) (*gssapiClient, error) {
331 | krbConf, err := creds.KerberosConfig(ctx)
332 | if err != nil {
333 | return nil, fmt.Errorf("configure Kerberos: %w", err)
334 | }
335 |
336 | if opts.KerberosDialer == nil {
337 | opts.KerberosDialer = &net.Dialer{Timeout: pkinit.DefaultKerberosRoundtripDeadline}
338 | }
339 |
340 | var (
341 | authClient *gssapiClient
342 | cert *x509.Certificate
343 | )
344 |
345 | tlsState, ok := conn.TLSConnectionState()
346 | if ok && !opts.DisableChannelBinding {
347 | cert = tlsState.PeerCertificates[0]
348 | }
349 |
350 | switch {
351 | case creds.Password != "" || creds.PasswordIsEmtpyString:
352 | opts.Debug("authenticating using GSSAPI bind (password)")
353 |
354 | authClient = &gssapiClient{
355 | Client: client.NewWithPassword(
356 | creds.Username,
357 | strings.ToUpper(creds.Domain),
358 | creds.Password,
359 | compat.Gokrb5ForkV9KerberosConfig(krbConf),
360 | client.DisablePAFXFAST(true),
361 | client.Dialer(opts.KerberosDialer),
362 | ),
363 | }
364 |
365 | authClient.BindCertificate = cert
366 | case creds.NTHash != "":
367 | opts.Debug("authenticating using GSSAPI bind (NT hash)")
368 |
369 | ntHash, err := hex.DecodeString(creds.NTHash)
370 | if err != nil {
371 | return nil, fmt.Errorf("decode NT hash: %w", err)
372 | }
373 |
374 | authClient = &gssapiClient{
375 | Client: client.NewWithEncryptionKey(
376 | creds.Username,
377 | strings.ToUpper(creds.Domain),
378 | types.EncryptionKey{
379 | KeyType: etypeID.RC4_HMAC,
380 | KeyValue: ntHash,
381 | },
382 | compat.Gokrb5ForkV9KerberosConfig(krbConf),
383 | client.DisablePAFXFAST(true),
384 | client.Dialer(opts.KerberosDialer),
385 | ),
386 | BindCertificate: cert,
387 | }
388 |
389 | authClient.BindCertificate = cert
390 | case creds.AESKey != "":
391 | opts.Debug("authenticating using GSSAPI bind (AES key)")
392 |
393 | aesKey, err := hex.DecodeString(creds.AESKey)
394 | if err != nil {
395 | return nil, fmt.Errorf("decode AES key: %w", err)
396 | }
397 |
398 | var keyType int32
399 |
400 | switch len(aesKey) {
401 | case 32:
402 | keyType = etypeID.AES256_CTS_HMAC_SHA1_96
403 | case 16:
404 | keyType = etypeID.AES128_CTS_HMAC_SHA1_96
405 | default:
406 | return nil, fmt.Errorf("invalid AES128/AES256 key: key size is %d bytes", len(aesKey))
407 | }
408 |
409 | authClient = &gssapiClient{
410 | Client: client.NewWithEncryptionKey(
411 | creds.Username,
412 | strings.ToUpper(creds.Domain),
413 | types.EncryptionKey{
414 | KeyType: keyType,
415 | KeyValue: aesKey,
416 | },
417 | compat.Gokrb5ForkV9KerberosConfig(krbConf),
418 | client.DisablePAFXFAST(true),
419 | client.Dialer(opts.KerberosDialer),
420 | ),
421 | BindCertificate: cert,
422 | }
423 |
424 | authClient.BindCertificate = cert
425 | case creds.ClientCert != nil:
426 | opts.Debug("authenticating using GSSAPI bind (PKINIT)")
427 |
428 | rsaKey, ok := creds.ClientCertKey.(*rsa.PrivateKey)
429 | if !ok {
430 | return nil, fmt.Errorf("cannot use %T because PKINIT requires an RSA key", creds.ClientCertKey)
431 | }
432 |
433 | return newPKINITClient(ctx, creds.Username, strings.ToUpper(creds.Domain),
434 | creds.ClientCert, rsaKey, krbConf, opts.KerberosDialer)
435 | case creds.CCache != "":
436 | opts.Debug("authenticating using GSSAPI bind (ccache)")
437 |
438 | authClient, err = newClientFromCCache(
439 | creds.Username, strings.ToUpper(creds.Domain), creds.CCache, krbConf, opts.KerberosDialer)
440 | if err != nil {
441 | return nil, fmt.Errorf("create GSSAPI client from CCACHE: %w", err)
442 | }
443 |
444 | authClient.BindCertificate = cert
445 | default:
446 | return nil, fmt.Errorf("no credentials available for Kerberos")
447 | }
448 |
449 | return authClient, nil
450 | }
451 |
452 | // TLSConfig returns a TLS config based on the default config in the provided
453 | // LDAP options as well as PFX files.
454 | func TLSConfig(
455 | opts *Options, clientCert *x509.Certificate, clientCertKey crypto.PrivateKey, caCerts []*x509.Certificate,
456 | ) (*tls.Config, error) {
457 | tlsConfig := opts.TLSConfig
458 | if tlsConfig == nil {
459 | tlsConfig = &tls.Config{}
460 | }
461 |
462 | tlsConfig.InsecureSkipVerify = !opts.Verify
463 | if tlsConfig.MaxVersion == 0 && !opts.DisableChannelBinding {
464 | tlsConfig.MaxVersion = tls.VersionTLS12 // channel binding is not supported for TLS1.3
465 | }
466 |
467 | if clientCert == nil {
468 | return tlsConfig, nil
469 | }
470 |
471 | var (
472 | keyBytes []byte
473 | err error
474 | )
475 |
476 | switch v := clientCertKey.(type) {
477 | case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey, *ecdh.PrivateKey:
478 | keyBytes, err = x509.MarshalPKCS8PrivateKey(v)
479 | if err != nil {
480 | return nil, fmt.Errorf("marshal private key: %w", err)
481 | }
482 | default:
483 | return nil, fmt.Errorf("unsupported client certificate key type: %T", clientCertKey)
484 | }
485 |
486 | certPEM := pem.EncodeToMemory(&pem.Block{
487 | Type: "CERTIFICATE",
488 | Bytes: clientCert.Raw,
489 | })
490 | keyPEM := pem.EncodeToMemory(&pem.Block{
491 | Type: "PRIVATE KEY",
492 | Bytes: keyBytes,
493 | })
494 |
495 | clientCertificate, err := tls.X509KeyPair(certPEM, keyPEM)
496 | if err != nil {
497 | return nil, fmt.Errorf("load client certificate: %w", err)
498 | }
499 |
500 | tlsConfig.Certificates = append(tlsConfig.Certificates, clientCertificate)
501 |
502 | if len(caCerts) == 0 {
503 | return tlsConfig, nil
504 | }
505 |
506 | if tlsConfig.RootCAs == nil {
507 | tlsConfig.RootCAs = x509.NewCertPool()
508 | }
509 |
510 | for _, cert := range caCerts {
511 | tlsConfig.RootCAs.AppendCertsFromPEM(pem.EncodeToMemory(&pem.Block{
512 | Type: "CERTIFICATE",
513 | Bytes: cert.Raw,
514 | }))
515 | }
516 |
517 | return tlsConfig, nil
518 | }
519 |
520 | // UserAndDomainFromPFX extracts the username and domain from UPNs in the
521 | // certificate's otherName SAN extension.
522 | func UserAndDomainFromPFX(pfxFile string, password string) (user string, domain string, err error) {
523 | pfxData, err := os.ReadFile(pfxFile)
524 | if err != nil {
525 | return "", "", fmt.Errorf("read PFX: %w", err)
526 | }
527 |
528 | _, cert, _, err := pkcs12.DecodeChain(pfxData, password)
529 | if err != nil {
530 | return "", "", fmt.Errorf("decode PFX: %w", err)
531 | }
532 |
533 | user, domain = userAndDomainFromCert(cert)
534 |
535 | return user, domain, nil
536 | }
537 |
538 | func userAndDomainFromCert(cert *x509.Certificate) (user string, domain string) {
539 | upns, err := othername.UPNs(cert)
540 | if err != nil {
541 | return "", ""
542 | }
543 |
544 | for _, upn := range upns {
545 | if !strings.Contains(upn, "@") {
546 | continue
547 | }
548 |
549 | parts := strings.Split(upn, "@")
550 | if len(parts) != 2 {
551 | continue
552 | }
553 |
554 | return parts[0], parts[1]
555 | }
556 |
557 | return "", ""
558 | }
559 |
560 | // ChannelBinding hash computes the channel binding token that can be included
561 | // in the authentication handshake to make sure that the client has established
562 | // a TLS connection to the correct server.
563 | func ChannelBindingHash(cert *x509.Certificate) []byte {
564 | hashType := crypto.SHA256
565 |
566 | switch cert.SignatureAlgorithm {
567 | case x509.SHA384WithRSA, x509.ECDSAWithSHA384, x509.SHA384WithRSAPSS:
568 | hashType = crypto.SHA384
569 | case x509.SHA512WithRSA, x509.ECDSAWithSHA512, x509.SHA512WithRSAPSS:
570 | hashType = crypto.SHA512
571 | }
572 |
573 | certHasher := hashType.New()
574 | _, _ = certHasher.Write(cert.Raw)
575 | certHash := certHasher.Sum(nil)
576 | prefix := "tls-server-end-point:"
577 |
578 | // https://learn.microsoft.com/en-us/windows/win32/api/sspi/ns-sspi-sec_channel_bindings
579 | // https://github.com/jborean93/Mailozaurr/blob
580 | // /6b565c4a1debdf301a95b93674ff12acdf8c762c/Classes/Class.SaslMechanismWindowsAuth.ps1#L67C19-L68C1
581 | channelBindingStructure := []byte{
582 | 0, 0, 0, 0, // InitiatorAddrType
583 | 0, 0, 0, 0, // InitiatorLength
584 |
585 | 0, 0, 0, 0, // AcceptorAddrType,
586 | 0, 0, 0, 0, // AcceptorLength,
587 | }
588 |
589 | channelBindingStructure = binary.LittleEndian.AppendUint32(channelBindingStructure,
590 | uint32(len(prefix)+len(certHash))) // ApplicationDataLength
591 | channelBindingStructure = append(channelBindingStructure, []byte(prefix)...)
592 | channelBindingStructure = append(channelBindingStructure, certHash...)
593 |
594 | channelBindingHasher := md5.New()
595 | channelBindingHasher.Write(channelBindingStructure)
596 |
597 | hash := channelBindingHasher.Sum(nil)
598 |
599 | return hash
600 | }
601 |
--------------------------------------------------------------------------------
/ldapauth/ntlm.go:
--------------------------------------------------------------------------------
1 | package ldapauth
2 |
3 | import (
4 | "crypto/x509"
5 | "fmt"
6 |
7 | "github.com/Azure/go-ntlmssp"
8 | "github.com/go-ldap/ldap/v3"
9 | "github.com/vadimi/go-ntlm/ntlm"
10 | )
11 |
12 | type ntlmNegotiator struct {
13 | cert *x509.Certificate
14 | overrideTargetName string
15 | }
16 |
17 | var _ ldap.NTLMNegotiator = &ntlmNegotiator{}
18 |
19 | func ntlmNegotiatorWithChannelBinding(cert *x509.Certificate, domain string) ldap.NTLMNegotiator {
20 | return &ntlmNegotiator{cert: cert, overrideTargetName: domain}
21 | }
22 |
23 | func ntlmNegotiatorForDomain(domain string) ldap.NTLMNegotiator {
24 | return &ntlmNegotiator{overrideTargetName: domain}
25 | }
26 |
27 | func (n *ntlmNegotiator) Negotiate(domain string, worktation string) ([]byte, error) {
28 | return ntlmssp.NewNegotiateMessage(domain, worktation)
29 | }
30 |
31 | func (n *ntlmNegotiator) ChallengeResponse(challenge []byte, username string, hash string) ([]byte, error) {
32 | if n.cert == nil && n.overrideTargetName == "" {
33 | // no cert means no channel binding, so Azure/ntlmssp can handle
34 | // authentication alone.
35 | return ntlmssp.ProcessChallengeWithHash(challenge, username, hash)
36 | }
37 |
38 | // The authenticate message needs to include a channel binding hash, but the
39 | // Azure/ntlmssp library does not support channel binding and offers no API
40 | // to retrofit it. The only way to make it work is too parse the challenge,
41 | // add the channel bindings to the challenge's TargetInfo field which will
42 | // then be included in the authenticate message. Unfortunately we need
43 | // another NTLM library because message marshalling and unmarshalling is
44 | // also not exposed in Azure/ntlmssp.
45 | cm, err := ntlm.ParseChallengeMessage(challenge)
46 | if err != nil {
47 | return nil, fmt.Errorf("parse NTLM challenge before injecting channel binding AVPair: %w", err)
48 | }
49 |
50 | if n.cert != nil {
51 | // drop end-of-list marker if present because we want to add another entry
52 | if len(cm.TargetInfo.List) > 0 && cm.TargetInfo.List[len(cm.TargetInfo.List)-1].AvId == ntlm.MsvAvEOL {
53 | cm.TargetInfo.List = cm.TargetInfo.List[:len(cm.TargetInfo.List)-1]
54 | }
55 |
56 | // add channel bindings
57 | cm.TargetInfo.AddAvPair(ntlm.MsvChannelBindings, ChannelBindingHash(n.cert))
58 | cm.TargetInfo.AddAvPair(ntlm.MsvAvEOL, nil)
59 |
60 | cm.TargetInfoPayloadStruct, err = ntlm.CreateBytePayload(cm.TargetInfo.Bytes())
61 | if err != nil {
62 | return nil, fmt.Errorf("marshal AVPairs with injected channel binding AVPair: %w", err)
63 | }
64 | }
65 |
66 | // Authenticate with the domain name that was specified, not the domain that
67 | // the server advertises. This grants compatibility with the LDAP SOCKS
68 | // feature of ntlmrelayx.py which is sensitive to the exact domain name (DNS
69 | // vs NetBIOS name).
70 | if n.overrideTargetName != "" && n.overrideTargetName != "." {
71 | cm.TargetName, err = ntlm.CreateStringPayload(n.overrideTargetName)
72 | if err != nil {
73 | return nil, fmt.Errorf("override target name: create string payload: %w", err)
74 | }
75 | }
76 |
77 | // make sure that the server cannot make cm.Bytes() panic by omitting the
78 | // version.
79 | if cm.Version == nil {
80 | cm.Version = &ntlm.VersionStruct{}
81 | }
82 |
83 | return ntlmssp.ProcessChallengeWithHash(cm.Bytes(), username, hash)
84 | }
85 |
--------------------------------------------------------------------------------
/options.go:
--------------------------------------------------------------------------------
1 | package adauth
2 |
3 | import (
4 | "context"
5 | "crypto/x509"
6 | "encoding/hex"
7 | "encoding/pem"
8 | "fmt"
9 | "io"
10 | "net"
11 | "os"
12 | "strconv"
13 | "strings"
14 |
15 | "github.com/RedTeamPentesting/adauth/othername"
16 | "github.com/spf13/pflag"
17 | "software.sslmate.com/src/go-pkcs12"
18 | )
19 |
20 | // Options holds command line options that are used to determine authentication
21 | // credentials and target.
22 | type Options struct {
23 | // Username (with domain) in one of the following formats:
24 | // `UPN`, `domain\user`, `domain/user` or `user`
25 | User string
26 | Password string
27 | NTHash string
28 | AESKey string
29 | CCache string
30 | DomainController string
31 | ForceKerberos bool
32 |
33 | // It is possible to specify a cert/key pair directly, as PEM files or as a
34 | // single PFX file.
35 | Certificate *x509.Certificate
36 | CertificateKey any
37 | PFXFileName string
38 | PFXPassword string
39 | PEMCertFileName string
40 | PEMKeyFileName string
41 |
42 | credential *Credential
43 | flagset *pflag.FlagSet
44 |
45 | Debug func(fmt string, a ...any)
46 | Resolver Resolver
47 | }
48 |
49 | // RegisterFlags registers authentication flags to a pflag.FlagSet such as the
50 | // default flagset `pflag.CommandLine`.
51 | func (opts *Options) RegisterFlags(flagset *pflag.FlagSet) {
52 | defaultCCACHEFile := os.Getenv("KRB5CCNAME")
53 | ccacheHint := ""
54 |
55 | if defaultCCACHEFile == "" {
56 | ccacheHint = " (defaults to $KRB5CCNAME, currently unset)"
57 | }
58 |
59 | flagset.StringVarP(&opts.User, "user", "u", "",
60 | "Username ('`user@domain`', 'domain\\user', 'domain/user' or 'user')")
61 | flagset.StringVarP(&opts.Password, "password", "p", "", "Password")
62 | flagset.StringVarP(&opts.NTHash, "nt-hash", "H", "", "NT `hash` ('NT', ':NT' or 'LM:NT')")
63 | flagset.StringVar(&opts.AESKey, "aes-key", "", "Kerberos AES `hex key`")
64 | flagset.StringVar(&opts.PFXFileName, "pfx", "", "Client certificate and private key as PFX `file`")
65 | flagset.StringVar(&opts.PFXPassword, "pfx-password", "", "Password for PFX file")
66 | flagset.StringVar(&opts.CCache, "ccache", defaultCCACHEFile, "Kerberos CCache `file` name"+ccacheHint)
67 | flagset.StringVar(&opts.DomainController, "dc", "", "Domain controller")
68 | flagset.BoolVarP(&opts.ForceKerberos, "kerberos", "k", false, "Use Kerberos authentication")
69 | opts.flagset = flagset
70 | }
71 |
72 | func (opts *Options) debug(format string, a ...any) {
73 | if opts.Debug != nil {
74 | opts.Debug(format, a...)
75 | }
76 | }
77 |
78 | func portForProtocol(protocol string) string {
79 | switch strings.ToLower(protocol) {
80 | case "ldap":
81 | return "389"
82 | case "ldaps":
83 | return "636"
84 | case "http":
85 | return "80"
86 | case "https":
87 | return "443"
88 | case "smb":
89 | return "445"
90 | case "rdp":
91 | return "3389"
92 | case "kerberos":
93 | return "88"
94 | default:
95 | return ""
96 | }
97 | }
98 |
99 | func addPortForProtocolIfMissing(protocol string, addr string) string {
100 | host, port, err := net.SplitHostPort(addr)
101 | if err != nil || port != "" {
102 | return addr
103 | }
104 |
105 | port = portForProtocol(protocol)
106 | if port == "" {
107 | return addr
108 | }
109 |
110 | return net.JoinHostPort(host, port)
111 | }
112 |
113 | // WithDCTarget returns credentials and the domain controller for the
114 | // corresponding domain as the target.
115 | func (opts *Options) WithDCTarget(ctx context.Context, protocol string) (*Credential, *Target, error) {
116 | if opts.DomainController != "" {
117 | return opts.WithTarget(ctx, protocol, addPortForProtocolIfMissing(protocol, opts.DomainController))
118 | }
119 |
120 | cred, err := opts.preliminaryCredential()
121 | if err != nil {
122 | return nil, nil, err
123 | }
124 |
125 | if cred.Domain == "" {
126 | return nil, nil, fmt.Errorf("domain unknown")
127 | }
128 |
129 | resolver := ensureResolver(opts.Resolver, opts.debug)
130 |
131 | var dcAddr string
132 |
133 | host, port, err := resolver.LookupFirstService(context.Background(), protocol, cred.Domain)
134 | if err != nil {
135 | lookupSRVErr := fmt.Errorf("could not lookup %q service of domain %q: %w", protocol, cred.Domain, err)
136 |
137 | dcAddr, err = resolver.LookupDCByDomain(context.Background(), cred.Domain)
138 | if err != nil {
139 | return nil, nil, fmt.Errorf("could not find DC: %w and %w", lookupSRVErr, err)
140 | }
141 |
142 | port := portForProtocol(protocol)
143 | if port != "" {
144 | dcAddr = net.JoinHostPort(dcAddr, port)
145 | }
146 |
147 | opts.debug("using DC %s based on domain lookup for %s", dcAddr, cred.Domain)
148 | } else {
149 | dcAddr = net.JoinHostPort(host, strconv.Itoa(port))
150 | opts.debug("using DC %s based on SRV lookup for domain %s", dcAddr, cred.Domain)
151 | }
152 |
153 | return cred, newTarget(
154 | protocol, dcAddr, opts.ForceKerberos || cred.mustUseKerberos(), opts.CCache, opts.Resolver), nil
155 | }
156 |
157 | // WithTarget returns credentials and the specified target.
158 | func (opts *Options) WithTarget(ctx context.Context, protocol string, target string) (*Credential, *Target, error) {
159 | if protocol == "" {
160 | protocol = "host"
161 | }
162 |
163 | cred, err := opts.preliminaryCredential()
164 | if err != nil {
165 | return nil, nil, err
166 | }
167 |
168 | t := newTarget(protocol, target, opts.ForceKerberos || cred.mustUseKerberos(), opts.CCache, opts.Resolver)
169 |
170 | if cred.Domain == "" {
171 | hostname, err := t.Hostname(ctx)
172 | if err != nil {
173 | return nil, nil, fmt.Errorf("lookup target hostname to determine domain: %w", err)
174 | }
175 |
176 | parts := strings.SplitN(hostname, ".", 2)
177 | if len(parts) == 2 {
178 | switch {
179 | case strings.Contains(parts[1], "."):
180 | cred.Domain = parts[1]
181 | default:
182 | cred.Domain = hostname
183 | }
184 | }
185 | }
186 |
187 | return cred, t, nil
188 | }
189 |
190 | // Username returns the user's name. Username may return an empty string.
191 | func (opts *Options) Username() string {
192 | cred, err := opts.preliminaryCredential()
193 | if err != nil {
194 | return ""
195 | }
196 |
197 | return cred.Username
198 | }
199 |
200 | // UPN returns the user's domain. Domain may return an empty string.
201 | func (opts *Options) Domain() string {
202 | cred, err := opts.preliminaryCredential()
203 | if err != nil {
204 | return ""
205 | }
206 |
207 | return cred.Domain
208 | }
209 |
210 | // UPN returns the user's universal principal name. UPN may return an empty
211 | // string.
212 | func (opts *Options) UPN() string {
213 | cred, err := opts.preliminaryCredential()
214 | if err != nil {
215 | return ""
216 | }
217 |
218 | return cred.UPN()
219 | }
220 |
221 | // NoTarget returns the user credentials without supplementing it with
222 | // information from a target.
223 | func (opts *Options) NoTarget() (*Credential, error) {
224 | return opts.preliminaryCredential()
225 | }
226 |
227 | func (opts *Options) preliminaryCredential() (*Credential, error) {
228 | if opts.credential != nil {
229 | return opts.credential, nil
230 | }
231 |
232 | domain, username := splitUserIntoDomainAndUsername(opts.User)
233 |
234 | cleanedNTHash := cleanNTHash(opts.NTHash)
235 |
236 | var ntHash string
237 |
238 | if cleanedNTHash != "" {
239 | ntHashBytes, err := hex.DecodeString(cleanedNTHash)
240 | if err != nil {
241 | return nil, fmt.Errorf("invalid NT hash: parse hex: %w", err)
242 | } else if len(ntHashBytes) != 16 {
243 | return nil, fmt.Errorf("invalid NT hash: %d bytes instead of 16", len(ntHashBytes))
244 | }
245 |
246 | ntHash = cleanedNTHash
247 | }
248 |
249 | var aesKey string
250 |
251 | if opts.AESKey != "" {
252 | aesKeyBytes, err := hex.DecodeString(opts.AESKey)
253 | if err != nil {
254 | return nil, fmt.Errorf("invalid AES key: parse hex: %w", err)
255 | } else if len(aesKeyBytes) != 16 && len(aesKeyBytes) != 32 {
256 | return nil, fmt.Errorf("invalid AES key: %d bytes instead of 16 or 32", len(aesKeyBytes))
257 | }
258 |
259 | aesKey = opts.AESKey
260 | }
261 |
262 | var ccache string
263 |
264 | if opts.CCache != "" {
265 | s, err := os.Stat(opts.CCache)
266 | if err == nil && !s.IsDir() {
267 | ccache = opts.CCache
268 | }
269 | }
270 |
271 | cred := &Credential{
272 | Username: username,
273 | Password: opts.Password,
274 | Domain: domain,
275 | NTHash: cleanNTHash(ntHash),
276 | AESKey: aesKey,
277 | CCache: ccache,
278 | dc: opts.DomainController,
279 | PasswordIsEmtpyString: opts.Password == "" && (opts.flagset != nil && opts.flagset.Changed("password")),
280 | CCacheIsFromEnv: opts.CCache != "" && (opts.flagset != nil && !opts.flagset.Changed("ccache")),
281 | Resolver: opts.Resolver,
282 | }
283 |
284 | switch {
285 | case opts.Certificate != nil && opts.CertificateKey == nil:
286 | return nil, fmt.Errorf("specify a key file for the client certificate")
287 | case opts.Certificate != nil && opts.CertificateKey != nil:
288 | cred.ClientCert = opts.Certificate
289 | cred.ClientCertKey = opts.CertificateKey
290 | case opts.PFXFileName != "":
291 | cert, key, caCerts, err := readPFX(opts.PFXFileName, opts.PFXPassword)
292 | if err != nil {
293 | return nil, err
294 | }
295 |
296 | cred.ClientCert = cert
297 | cred.ClientCertKey = key
298 | cred.CACerts = caCerts
299 | case opts.PEMCertFileName != "" && opts.PEMKeyFileName == "":
300 | return nil, fmt.Errorf("specify a key file for the client certificate")
301 | case opts.PEMCertFileName != "" && opts.PEMKeyFileName != "":
302 | cert, key, err := readPEMCertAndKey(opts.PEMCertFileName, opts.PEMKeyFileName)
303 | if err != nil {
304 | return nil, err
305 | }
306 |
307 | cred.ClientCert = cert
308 | cred.ClientCertKey = key
309 | }
310 |
311 | //nolint:nestif
312 | if cred.ClientCert != nil {
313 | user, domain, err := othername.UserAndDomain(cred.ClientCert)
314 | if err == nil {
315 | if cred.Username == "" {
316 | cred.Username = user
317 | }
318 |
319 | if cred.Domain == "" {
320 | cred.Domain = domain
321 | }
322 | }
323 | }
324 |
325 | opts.credential = cred
326 |
327 | return cred, nil
328 | }
329 |
330 | func readPFX(fileName string, password string) (*x509.Certificate, any, []*x509.Certificate, error) {
331 | pfxData, err := os.ReadFile(fileName)
332 | if err != nil {
333 | return nil, nil, nil, fmt.Errorf("read PFX: %w", err)
334 | }
335 |
336 | key, cert, caCerts, err := pkcs12.DecodeChain(pfxData, password)
337 | if err != nil {
338 | return nil, nil, nil, fmt.Errorf("decode PFX: %w", err)
339 | }
340 |
341 | return cert, key, caCerts, nil
342 | }
343 |
344 | func readPEMCertAndKey(certFileName string, certKeyFileName string) (*x509.Certificate, any, error) {
345 | certData, err := os.ReadFile(certFileName)
346 | if err != nil {
347 | return nil, nil, fmt.Errorf("read cert file: %w", err)
348 | }
349 |
350 | block, _ := pem.Decode(certData)
351 | if block == nil {
352 | return nil, nil, fmt.Errorf("could not PEM-decode certificate")
353 | }
354 |
355 | if block.Type != "" && !strings.Contains(strings.ToLower(block.Type), "certificate") {
356 | return nil, nil, fmt.Errorf("unexpected block type for certificate: %q", block.Type)
357 | }
358 |
359 | cert, err := x509.ParseCertificate(block.Bytes)
360 | if err != nil {
361 | return nil, nil, fmt.Errorf("parse certificate: %w", err)
362 | }
363 |
364 | certKeyData, err := os.ReadFile(certKeyFileName)
365 | if err != nil {
366 | return nil, nil, fmt.Errorf("read cert key file: %w", err)
367 | }
368 |
369 | block, _ = pem.Decode(certKeyData)
370 | if block == nil {
371 | return nil, nil, fmt.Errorf("could not PEM-decode certificate key")
372 | }
373 |
374 | if block.Type != "" && !strings.Contains(strings.ToLower(block.Type), "key") {
375 | return nil, nil, fmt.Errorf("unexpected block type for key: %q", block.Type)
376 | }
377 |
378 | key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
379 | if err != nil {
380 | key, pkcs1Err := x509.ParsePKCS1PrivateKey(block.Bytes)
381 | if pkcs1Err == nil {
382 | return cert, key, nil
383 | }
384 |
385 | return nil, nil, fmt.Errorf("parse private key: %w", err)
386 | }
387 |
388 | return cert, key, nil
389 | }
390 |
391 | // NewDebugFunc creates a debug output handler.
392 | func NewDebugFunc(enabled *bool, writer io.Writer, colored bool) func(string, ...any) {
393 | return func(format string, a ...any) {
394 | if enabled == nil || !*enabled {
395 | return
396 | }
397 |
398 | format = strings.TrimRight(format, "\n")
399 | if colored {
400 | format = "\033[2m" + format + "\033[0m"
401 | }
402 |
403 | _, _ = fmt.Fprintf(writer, format+"\n", a...)
404 | }
405 | }
406 |
407 | func cleanNTHash(h string) string {
408 | if !strings.Contains(h, ":") {
409 | return h
410 | }
411 |
412 | parts := strings.Split(h, ":")
413 | if len(parts) != 2 {
414 | return h
415 | }
416 |
417 | return parts[1]
418 | }
419 |
--------------------------------------------------------------------------------
/options_test.go:
--------------------------------------------------------------------------------
1 | package adauth_test
2 |
3 | import (
4 | "context"
5 | "encoding/hex"
6 | "net"
7 | "strconv"
8 | "testing"
9 |
10 | "github.com/RedTeamPentesting/adauth"
11 | )
12 |
13 | const (
14 | testUser = "someuser"
15 | testDomain = "domain.tld"
16 | )
17 |
18 | func TestUsernameAndDomainParsing(t *testing.T) {
19 | expectedUPN := testUser + "@" + testDomain
20 | expectedLogonName := testDomain + `\` + testUser
21 | expectedImpacketLogonName := testDomain + "/" + testUser
22 |
23 | testCases := []struct {
24 | Name string
25 | Opts adauth.Options
26 | }{
27 | {
28 | Name: "UPN",
29 | Opts: adauth.Options{User: testUser + "@" + testDomain},
30 | },
31 | {
32 | Name: "logon-name",
33 | Opts: adauth.Options{User: testDomain + `\` + testUser},
34 | },
35 | {
36 | Name: "impacket-style",
37 | Opts: adauth.Options{User: testDomain + `/` + testUser},
38 | },
39 | {
40 | Name: "pfx",
41 | Opts: adauth.Options{PFXFileName: "testdata/someuser@domain.tld.pfx"},
42 | },
43 | }
44 |
45 | for _, testCase := range testCases {
46 | testCase := testCase
47 |
48 | t.Run(testCase.Name, func(t *testing.T) {
49 | creds, err := testCase.Opts.NoTarget()
50 | if err != nil {
51 | t.Fatalf("get credentials: %v", err)
52 | }
53 |
54 | if creds.Username != testUser {
55 | t.Errorf("username %q is not %q", creds.Username, testUser)
56 | }
57 |
58 | if creds.Domain != testDomain {
59 | t.Errorf("domain %q is not %q", creds.Domain, testDomain)
60 | }
61 |
62 | upn := creds.UPN()
63 | if upn != expectedUPN {
64 | t.Errorf("UPN %q is not %q", upn, expectedUPN)
65 | }
66 |
67 | logonName := creds.LogonName()
68 | if logonName != expectedLogonName {
69 | t.Errorf("logon name %q is not %q", logonName, expectedLogonName)
70 | }
71 |
72 | impacketLogonName := creds.ImpacketLogonName()
73 | if impacketLogonName != expectedImpacketLogonName {
74 | t.Errorf("impacket-style logon name %q is not %q", impacketLogonName, expectedImpacketLogonName)
75 | }
76 | })
77 | }
78 | }
79 |
80 | func TestDomainFromTargetHostname(t *testing.T) {
81 | opts := adauth.Options{
82 | User: testUser,
83 | }
84 |
85 | creds, _, err := opts.WithTarget(context.Background(), "cifs", "host."+testDomain)
86 | if err != nil {
87 | t.Fatalf("get credentials: %v", err)
88 | }
89 |
90 | if creds.Domain != testDomain {
91 | t.Errorf("domain is %q instead of %q", creds.Domain, testDomain)
92 | }
93 |
94 | // ignore domain in hostname for local authentication
95 | opts = adauth.Options{
96 | User: "./" + testUser,
97 | }
98 |
99 | creds, _, err = opts.WithTarget(context.Background(), "cifs", "host."+testDomain)
100 | if err != nil {
101 | t.Fatalf("get credentials: %v", err)
102 | }
103 |
104 | if creds.Domain != "." {
105 | t.Errorf("domain is %q instead of %q", creds.Domain, ".")
106 | }
107 | }
108 |
109 | func TestDCTarget(t *testing.T) {
110 | dcHost := "dc." + testDomain
111 | expectedSPN := "ldap/" + dcHost
112 |
113 | opts := adauth.Options{
114 | User: testUser + `@` + testDomain,
115 | Resolver: &testResolver{
116 | SRV: map[string]map[string]map[string]struct {
117 | Name string
118 | SRV []*net.SRV
119 | }{
120 | "ldap": {
121 | "tcp": {
122 | testDomain: {
123 | Name: dcHost,
124 | SRV: []*net.SRV{
125 | {Target: dcHost, Port: 389},
126 | },
127 | },
128 | },
129 | },
130 | },
131 | },
132 | }
133 |
134 | _, dc, err := opts.WithDCTarget(context.Background(), "ldap")
135 | if err != nil {
136 | t.Fatalf("get DC target: %v", err)
137 | }
138 |
139 | if dc.Address() != net.JoinHostPort(dcHost, "389") {
140 | t.Fatalf("DC address is %q instead of %q", dc.Address(), net.JoinHostPort(dcHost, "389"))
141 | }
142 |
143 | if dc.AddressWithoutPort() != dcHost {
144 | t.Fatalf("DC address without port is %q instead of %q", dc.Address(), dcHost)
145 | }
146 |
147 | if dc.Port != "389" {
148 | t.Errorf("DC port is %q instead of %q", dc.Port, "389")
149 | }
150 |
151 | spn, err := dc.SPN(context.Background())
152 | if err != nil {
153 | t.Fatalf("get DC SPN: %v", err)
154 | }
155 |
156 | if spn != expectedSPN {
157 | t.Errorf("DC SPN is %q instead of %q", spn, expectedSPN)
158 | }
159 | }
160 |
161 | func TestDCTargetWithoutSRVRecord(t *testing.T) {
162 | dcHost := "dc." + testDomain
163 | dcIP := net.ParseIP("10.0.0.1")
164 | expectedSPN := "ldap/" + dcHost
165 |
166 | opts := adauth.Options{
167 | User: testUser + `@` + testDomain,
168 | Resolver: &testResolver{
169 | HostToAddr: map[string][]net.IP{
170 | testDomain: {dcIP},
171 | },
172 | AddrToHost: map[string][]string{
173 | dcIP.String(): {dcHost},
174 | },
175 | },
176 | }
177 |
178 | _, dc, err := opts.WithDCTarget(context.Background(), "ldap")
179 | if err != nil {
180 | t.Fatalf("get DC target: %v", err)
181 | }
182 |
183 | if dc.Address() != net.JoinHostPort(dcHost, "389") {
184 | t.Fatalf("DC address is %q instead of %q", dc.Address(), net.JoinHostPort(dcHost, "389"))
185 | }
186 |
187 | if dc.AddressWithoutPort() != dcHost {
188 | t.Fatalf("DC address without port is %q instead of %q", dc.Address(), dcHost)
189 | }
190 |
191 | if dc.Port != "389" {
192 | t.Errorf("DC port is %q instead of %q", dc.Port, "389")
193 | }
194 |
195 | spn, err := dc.SPN(context.Background())
196 | if err != nil {
197 | t.Fatalf("get DC SPN: %v", err)
198 | }
199 |
200 | if spn != expectedSPN {
201 | t.Errorf("DC SPN is %q instead of %q", spn, expectedSPN)
202 | }
203 | }
204 |
205 | func TestDCTargetNoReverseLookup(t *testing.T) {
206 | dcIP := net.ParseIP("10.0.0.1")
207 |
208 | opts := adauth.Options{
209 | User: testUser + `@` + testDomain,
210 | Resolver: &testResolver{
211 | HostToAddr: map[string][]net.IP{
212 | testDomain: {dcIP},
213 | },
214 | },
215 | }
216 |
217 | _, dc, err := opts.WithDCTarget(context.Background(), "ldap")
218 | if err != nil {
219 | t.Fatalf("get DC target: %v", err)
220 | }
221 |
222 | if dc.Address() != net.JoinHostPort(dcIP.String(), "389") {
223 | t.Fatalf("DC address is %q instead of %q", dc.Address(), net.JoinHostPort(dcIP.String(), "389"))
224 | }
225 | }
226 |
227 | func TestKerberos(t *testing.T) {
228 | upn := testUser + `@` + testDomain
229 |
230 | testCases := []struct {
231 | Opts adauth.Options
232 | ShouldUseKerberos bool
233 | }{
234 | {
235 | Opts: adauth.Options{
236 | User: upn,
237 | Password: "pass",
238 | },
239 | ShouldUseKerberos: false,
240 | },
241 | {
242 | Opts: adauth.Options{
243 | User: upn,
244 | AESKey: hex.EncodeToString(make([]byte, 16)),
245 | },
246 | ShouldUseKerberos: true,
247 | },
248 | {
249 | Opts: adauth.Options{
250 | User: upn,
251 | Password: "test",
252 | AESKey: hex.EncodeToString(make([]byte, 16)),
253 | },
254 | ShouldUseKerberos: false,
255 | },
256 | {
257 | Opts: adauth.Options{
258 | User: upn,
259 | Password: "pass",
260 | ForceKerberos: true,
261 | },
262 | ShouldUseKerberos: true,
263 | },
264 | {
265 | Opts: adauth.Options{
266 | User: upn,
267 | PFXFileName: "testdata/someuser@domain.tld.pfx",
268 | },
269 | ShouldUseKerberos: false,
270 | },
271 | {
272 | Opts: adauth.Options{
273 | User: upn,
274 | // CCache indicates kerberos usage, but only if file is present
275 | // (even though it might be empty)
276 | CCache: "testdata/empty.ccache",
277 | },
278 | ShouldUseKerberos: true,
279 | },
280 | {
281 | Opts: adauth.Options{
282 | User: upn,
283 | // CCache does not matter if file is not even there
284 | CCache: "testdata/doesnotexist",
285 | },
286 | ShouldUseKerberos: false,
287 | },
288 | }
289 |
290 | for i, testCase := range testCases {
291 | testCase := testCase
292 |
293 | t.Run(strconv.Itoa(i), func(t *testing.T) {
294 | testCase.Opts.DomainController = "dc.domain.tld"
295 |
296 | _, target, err := testCase.Opts.WithDCTarget(context.Background(), "ldap")
297 | if err != nil {
298 | t.Fatalf("get target: %v", err)
299 | }
300 |
301 | switch {
302 | case testCase.ShouldUseKerberos && !target.UseKerberos:
303 | t.Errorf("target would not use Kerberos even though it should: %#v", testCase.Opts)
304 | case !testCase.ShouldUseKerberos && target.UseKerberos:
305 | t.Errorf("target would use Kerberos even though it should not: %#v", testCase.Opts)
306 | }
307 | })
308 | }
309 | }
310 |
311 | func TestCleanNTHash(t *testing.T) {
312 | ntHash := "31d6cfe0d16ae931b73c59d7e0c089c0"
313 |
314 | testCases := []string{
315 | ntHash,
316 | ":" + ntHash,
317 | "aad3b435b51404eeaad3b435b51404ee:" + ntHash,
318 | }
319 |
320 | for i, testCase := range testCases {
321 | testCase := testCase
322 |
323 | t.Run(strconv.Itoa(i), func(t *testing.T) {
324 | creds, err := (&adauth.Options{
325 | User: testUser + "@" + testDomain,
326 | NTHash: testCase,
327 | }).NoTarget()
328 | if err != nil {
329 | t.Fatalf("get credentials: %v", err)
330 | }
331 |
332 | if creds.NTHash != ntHash {
333 | t.Errorf("NT hash is %q instead of %q", creds.NTHash, ntHash)
334 | }
335 | })
336 | }
337 | }
338 |
--------------------------------------------------------------------------------
/othername/othername.go:
--------------------------------------------------------------------------------
1 | // Package othername is a minimal and incomplete implementation of the otherName SAN extension.
2 | package othername
3 |
4 | import (
5 | "crypto/x509"
6 | "crypto/x509/pkix"
7 | "encoding/asn1"
8 | "fmt"
9 | "strings"
10 | )
11 |
12 | var (
13 | UPNOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 20, 2, 3}
14 | SANOID = asn1.ObjectIdentifier{2, 5, 29, 17}
15 | )
16 |
17 | type OtherName struct {
18 | ID asn1.ObjectIdentifier
19 | Value asn1.RawValue
20 | }
21 |
22 | // Extension generates an otherName extension.
23 | func Extension(names ...*OtherName) (pkix.Extension, error) {
24 | ext := pkix.Extension{
25 | Id: SANOID,
26 | Critical: false,
27 | }
28 |
29 | rawValues := make([]asn1.RawValue, 0, len(names))
30 |
31 | for _, name := range names {
32 | v := asn1.RawValue{
33 | Class: asn1.ClassContextSpecific,
34 | IsCompound: true,
35 | }
36 |
37 | id, err := asn1.Marshal(name.ID)
38 | if err != nil {
39 | return ext, fmt.Errorf("marshal oid: %w", err)
40 | }
41 |
42 | v.Bytes = append(v.Bytes, id...)
43 |
44 | blob, err := asn1.Marshal(name.Value)
45 | if err != nil {
46 | return ext, fmt.Errorf("marshal raw name: %w", err)
47 | }
48 |
49 | v.Bytes = append(v.Bytes, blob...)
50 |
51 | rawValues = append(rawValues, v)
52 | }
53 |
54 | extBytes, err := asn1.Marshal(rawValues)
55 | if err != nil {
56 | return ext, fmt.Errorf("marshal extension bytes: %w", err)
57 | }
58 |
59 | ext.Value = append(ext.Value, extBytes...)
60 |
61 | return ext, nil
62 | }
63 |
64 | // ExtensionFromUPNs build an otherName extension based on the provided UPNs.
65 | func ExtensionFromUPNs(upns ...string) (ext pkix.Extension, err error) {
66 | otherNames := make([]*OtherName, 0, len(upns))
67 |
68 | for _, upn := range upns {
69 | utf8Name, err := asn1.MarshalWithParams(upn, "utf8")
70 | if err != nil {
71 | return ext, fmt.Errorf("marshal UTF8 name: %w", err)
72 | }
73 |
74 | otherNames = append(otherNames, &OtherName{
75 | ID: UPNOID,
76 | Value: asn1.RawValue{
77 | Class: asn1.ClassContextSpecific,
78 | IsCompound: true,
79 | Bytes: utf8Name,
80 | },
81 | })
82 | }
83 |
84 | return Extension(otherNames...)
85 | }
86 |
87 | // Names returns the names from the otherName extension of the provided
88 | // certificate. If it does not contain such an extension, it will return an
89 | // empty slice and no error.
90 | func Names(cert *x509.Certificate) ([]*OtherName, error) {
91 | oidSubjectAltName := asn1.ObjectIdentifier{2, 5, 29, 17}
92 |
93 | var otherNames []*OtherName
94 |
95 | for _, extension := range append(cert.Extensions, cert.ExtraExtensions...) {
96 | if !extension.Id.Equal(oidSubjectAltName) {
97 | continue
98 | }
99 |
100 | ons, err := otherNamesFromSANBytes(extension.Value)
101 | if err != nil {
102 | return nil, fmt.Errorf("parse otherName data: %w", err)
103 | }
104 |
105 | otherNames = append(otherNames, ons...)
106 | }
107 |
108 | return otherNames, nil
109 | }
110 |
111 | // UPNs returns all UPNs that are stored in certificates otherName extension.
112 | func UPNs(cert *x509.Certificate) (upns []string, err error) {
113 | otherNames, err := Names(cert)
114 | if err != nil {
115 | return nil, err
116 | }
117 |
118 | for _, otherName := range otherNames {
119 | if !otherName.ID.Equal(UPNOID) {
120 | continue
121 | }
122 |
123 | var upn string
124 |
125 | _, err = asn1.UnmarshalWithParams(otherName.Value.Bytes, &upn, "utf8")
126 | if err != nil {
127 | return nil, fmt.Errorf("unmarshal name: %w", err)
128 | }
129 |
130 | upns = append(upns, upn)
131 | }
132 |
133 | return upns, nil
134 | }
135 |
136 | // UserAndDomain returns the user and domain from the first valid UPN in the
137 | // certificate's otherName extension.
138 | func UserAndDomain(cert *x509.Certificate) (user string, domain string, err error) {
139 | upns, err := UPNs(cert)
140 | if err != nil {
141 | return "", "", err
142 | }
143 |
144 | for _, upn := range upns {
145 | parts := strings.Split(upn, "@")
146 | if len(parts) == 2 {
147 | return parts[0], parts[1], nil
148 | }
149 | }
150 |
151 | return "", "", fmt.Errorf("found no suitable UPN in certificate")
152 | }
153 |
154 | func otherNamesFromSANBytes(bytes []byte) ([]*OtherName, error) {
155 | values := []asn1.RawValue{}
156 |
157 | _, err := asn1.Unmarshal(bytes, &values)
158 | if err != nil {
159 | return nil, fmt.Errorf("unmarshal raw values: %w", err)
160 | }
161 |
162 | otherNames := make([]*OtherName, 0, len(values))
163 |
164 | for _, value := range values {
165 | if value.Tag != 0 {
166 | continue
167 | }
168 |
169 | otherName := &OtherName{}
170 |
171 | value.Bytes, err = asn1.Unmarshal(value.Bytes, &otherName.ID)
172 | if err != nil {
173 | return nil, fmt.Errorf("unmarshal ID: %w", err)
174 | }
175 |
176 | value.Bytes, err = asn1.UnmarshalWithParams(value.Bytes, &otherName.Value, "utf8")
177 | if err != nil {
178 | return nil, fmt.Errorf("unmarshal raw name: %w", err)
179 | }
180 |
181 | if len(value.Bytes) != 0 {
182 | return nil, fmt.Errorf("othername: entry contains trailing bytes")
183 | }
184 |
185 | otherNames = append(otherNames, otherName)
186 | }
187 |
188 | return otherNames, nil
189 | }
190 |
--------------------------------------------------------------------------------
/othername/othername_test.go:
--------------------------------------------------------------------------------
1 | package othername_test
2 |
3 | import (
4 | "crypto/x509"
5 | "crypto/x509/pkix"
6 | "testing"
7 |
8 | "github.com/RedTeamPentesting/adauth/othername"
9 | )
10 |
11 | func TestOtherName(t *testing.T) {
12 | t.Parallel()
13 |
14 | names := []string{"a", "b", "c"}
15 |
16 | ext, err := othername.ExtensionFromUPNs(names...)
17 | if err != nil {
18 | t.Fatalf("generate otherName extension: %v", err)
19 | }
20 |
21 | cert := &x509.Certificate{
22 | ExtraExtensions: []pkix.Extension{ext},
23 | }
24 |
25 | parsedNames, err := othername.UPNs(cert)
26 | if err != nil {
27 | t.Fatalf("parse otherNames: %v", err)
28 | }
29 |
30 | if len(names) != len(parsedNames) {
31 | t.Fatalf("got %d (%#v) names instead of %d (%#v)",
32 | len(parsedNames), parsedNames, len(names), names)
33 | }
34 |
35 | for i := 0; i < len(names); i++ {
36 | if names[i] != parsedNames[i] {
37 | t.Errorf("got %q instead of %q", parsedNames[i], names[i])
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/pkinit/asn1.go:
--------------------------------------------------------------------------------
1 | package pkinit
2 |
3 | import (
4 | "crypto/x509"
5 | "crypto/x509/pkix"
6 | "encoding/asn1"
7 | "fmt"
8 | "math/big"
9 | "time"
10 |
11 | "github.com/jcmturner/gokrb5/v8/types"
12 | )
13 |
14 | type SignerInfo struct {
15 | Version int `asn1:"default:1"`
16 | IssuerAndSerialNumber IssuerAndSerial
17 | DigestAlgorithm pkix.AlgorithmIdentifier
18 | AuthenticatedAttributes []Attribute `asn1:"optional,omitempty,tag:0"`
19 | DigestEncryptionAlgorithm pkix.AlgorithmIdentifier
20 | EncryptedDigest []byte
21 | UnauthenticatedAttributes []pkix.AttributeTypeAndValue `asn1:"optional,omitempty,tag:1"`
22 | }
23 |
24 | type Attribute struct {
25 | Type asn1.ObjectIdentifier
26 | Value asn1.RawValue `asn1:"set"`
27 | }
28 |
29 | type IssuerAndSerial struct {
30 | IssuerName asn1.RawValue
31 | SerialNumber *big.Int
32 | }
33 |
34 | type ContentInfo struct {
35 | ContentType asn1.ObjectIdentifier
36 | Content asn1.RawValue `asn1:"explicit,optional,tag:0"`
37 | }
38 |
39 | type SignedData struct {
40 | Version int `asn1:"default:1"`
41 | DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"`
42 | ContentInfo ContentInfo
43 | Certificates RawCertificates `asn1:"optional,tag:0"`
44 | CRLs []x509.RevocationList `asn1:"optional,tag:1"`
45 | SignerInfos []SignerInfo `asn1:"set"`
46 | }
47 |
48 | type RawCertificates struct {
49 | Raw asn1.RawContent
50 | }
51 |
52 | func RawCertificate(cert *x509.Certificate) (RawCertificates, error) {
53 | val := asn1.RawValue{Bytes: cert.Raw, Class: 2, Tag: 0, IsCompound: true}
54 |
55 | b, err := asn1.Marshal(val)
56 | if err != nil {
57 | return RawCertificates{}, err
58 | }
59 |
60 | return RawCertificates{Raw: b}, nil
61 | }
62 |
63 | type AuthPack struct {
64 | // AuthPack ::= SEQUENCE {
65 | // pkAuthenticator [0] PKAuthenticator,
66 | // clientPublicValue [1] SubjectPublicKeyInfo OPTIONAL,
67 | // supportedCMSTypes [2] SEQUENCE OF AlgorithmIdentifier OPTIONAL,
68 | // clientDHNonce [3] DHNonce OPTIONAL,
69 | // ...,
70 | // supportedKDFs [4] SEQUENCE OF KDFAlgorithmId OPTIONAL,
71 | // ...
72 | // }
73 | PKAuthenticator PKAuthenticator `asn1:"tag:0,explicit"`
74 | ClientPublicValue SubjectPublicKeyInfo `asn1:"tag:1,explicit,optional"`
75 | SupportedCMSTypes []pkix.AlgorithmIdentifier `asn1:"tag:2,explicit,optional"`
76 | ClientDHNonce []byte `asn1:"tag:3,explicit,optional"`
77 | }
78 |
79 | type PKAuthenticator struct {
80 | // PKAuthenticator ::= SEQUENCE {
81 | // cusec [0] INTEGER -- (0..999999) --,
82 | // ctime [1] KerberosTime,
83 | // nonce [2] INTEGER (0..4294967295),
84 | // paChecksum [3] OCTET STRING OPTIONAL,
85 | // ...
86 | // asn1
87 | CUSec int `asn1:"tag:0,explicit"`
88 | CTime time.Time `asn1:"tag:1,explicit,generalized"`
89 | Nonce int `asn1:"tag:2,explicit"`
90 | Checksum []byte `asn1:"tag:3,explicit,optional"`
91 | }
92 |
93 | type SubjectPublicKeyInfo struct {
94 | // SubjectPublicKeyInfo ::= SEQUENCE {
95 | // algorithm AlgorithmIdentifier{PUBLIC-KEY,
96 | // {PublicKeyAlgorithms}},
97 | // subjectPublicKey BIT STRING }
98 | Algorithm AlgorithmIdentifier
99 | PublicKey asn1.BitString
100 | }
101 |
102 | type AlgorithmIdentifier struct {
103 | Algorithm asn1.ObjectIdentifier `asn1:"implicit"`
104 | Parameters DomainParameters `asn1:"implicit,optional"`
105 | }
106 |
107 | type DomainParameters struct {
108 | // DomainParameters ::= SEQUENCE {
109 | // p INTEGER, -- odd prime, p=jq +1
110 | // g INTEGER, -- generator, g
111 | // q INTEGER, -- factor of p-1
112 | // j INTEGER OPTIONAL, -- subgroup factor
113 | // validationParams ValidationParams OPTIONAL }
114 | P *big.Int
115 | G int
116 | Q int
117 | }
118 |
119 | type PAPKASRep struct {
120 | DHInfo asn1.RawValue
121 | }
122 |
123 | type PAPACRequest struct {
124 | IncludePAC bool `asn1:"explicit,tag:0"`
125 | }
126 |
127 | func (p *PAPACRequest) AsPAData() types.PAData {
128 | // make sure we marshal the struct and not the pointer to the struct which
129 | // would cause an error and also make sure we don't panic when receiver is
130 | // nil.
131 | pacRequest := PAPACRequest{}
132 | if p != nil {
133 | pacRequest.IncludePAC = p.IncludePAC
134 | }
135 |
136 | pacRequestBytes, err := asn1.Marshal(pacRequest)
137 | if err != nil {
138 | panic(fmt.Sprintf("unexpected error marshalling PAPACRequest: %v", err))
139 | }
140 |
141 | const paDataTypePACRequest = 128
142 |
143 | return types.PAData{
144 | PADataType: paDataTypePACRequest,
145 | PADataValue: pacRequestBytes,
146 | }
147 | }
148 |
149 | type DHRepInfo struct {
150 | DHSignedData []byte `asn1:"tag:0"`
151 | ServerDHNonce []byte `asn1:"tag:1,explicit,optional"`
152 | }
153 |
154 | type KDCDHKeyInfo struct {
155 | SubjectPublicKey asn1.BitString `asn1:"tag:0,explicit"`
156 | Nonce *big.Int `asn1:"tag:1,explicit"`
157 | DHKeyExpication time.Time `asn1:"tag:2,explicit,optional,generalized"`
158 | }
159 |
160 | type AuthoirzationDataElement struct {
161 | ADType int `asn1:"tag:0"`
162 | ADData []byte `asn1:"tag:1"`
163 | }
164 |
165 | type AuthoirzationData []AuthoirzationDataElement
166 |
--------------------------------------------------------------------------------
/pkinit/asrep.go:
--------------------------------------------------------------------------------
1 | package pkinit
2 |
3 | import (
4 | "crypto/sha1"
5 | "encoding/asn1"
6 | "fmt"
7 | "math/big"
8 |
9 | krb5crypto "github.com/jcmturner/gokrb5/v8/crypto"
10 | "github.com/jcmturner/gokrb5/v8/iana/etypeID"
11 | "github.com/jcmturner/gokrb5/v8/iana/keyusage"
12 | "github.com/jcmturner/gokrb5/v8/messages"
13 | "github.com/jcmturner/gokrb5/v8/types"
14 | )
15 |
16 | // Decrypt decrypts the encrypted parts of an ASRep with the key derived during PKINIT.
17 | func Decrypt(asRep *messages.ASRep, dhKey *big.Int, dhClientNonce []byte) (pkinitKey types.EncryptionKey, err error) {
18 | ekey, err := ExtractNegotiatedKey(asRep, dhKey, dhClientNonce)
19 | if err != nil {
20 | return pkinitKey, fmt.Errorf("extract negotiated key: %w", err)
21 | }
22 |
23 | decrypted, err := krb5crypto.DecryptEncPart(asRep.EncPart, ekey, keyusage.AS_REP_ENCPART)
24 | if err != nil {
25 | return pkinitKey, fmt.Errorf("decrypt: %w", err)
26 | }
27 |
28 | err = asRep.DecryptedEncPart.Unmarshal(decrypted)
29 | if err != nil {
30 | return pkinitKey, fmt.Errorf("unmarshal encrypted part: %w", err)
31 | }
32 |
33 | return ekey, nil
34 | }
35 |
36 | // ExtractNegotiatedKey extracts the key derived during PKINIT.
37 | func ExtractNegotiatedKey(
38 | asRep *messages.ASRep, dhKey *big.Int, dhClientNonce []byte,
39 | ) (ekey types.EncryptionKey, err error) {
40 | var paPKASRepBytes []byte
41 |
42 | for _, paData := range asRep.PAData {
43 | if paData.PADataType == 17 {
44 | paPKASRepBytes = paData.PADataValue
45 | }
46 | }
47 |
48 | if paPKASRepBytes == nil {
49 | return ekey, fmt.Errorf("could not find pA-PK-AS-REP structure")
50 | }
51 |
52 | var paPKASRep DHRepInfo
53 |
54 | err = unmarshalFromRawValue(paPKASRepBytes, &paPKASRep)
55 | if err != nil {
56 | return ekey, fmt.Errorf("unmarshal PA-PK-AS-Rep: %w", err)
57 | }
58 |
59 | var contentInfo ContentInfo
60 |
61 | _, err = asn1.Unmarshal(paPKASRep.DHSignedData, &contentInfo)
62 | if err != nil {
63 | return ekey, fmt.Errorf("unmarshal signed data: %w", err)
64 | }
65 |
66 | if !contentInfo.ContentType.Equal(asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}) {
67 | return ekey, fmt.Errorf("unexpected outer content type: %s", contentInfo.ContentType)
68 | }
69 |
70 | var signedData SignedData
71 |
72 | _, err = asn1.Unmarshal(contentInfo.Content.Bytes, &signedData)
73 | if err != nil {
74 | return ekey, fmt.Errorf("unmarshal signed data: %w", err)
75 | }
76 |
77 | if !signedData.ContentInfo.ContentType.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 5, 2, 3, 2}) {
78 | return ekey, fmt.Errorf("unexpected inner content type: %s", signedData.ContentInfo.ContentType)
79 | }
80 |
81 | var keyInfo KDCDHKeyInfo
82 |
83 | err = unmarshalFromRawValue(signedData.ContentInfo.Content.Bytes, &keyInfo)
84 | if err != nil {
85 | return ekey, fmt.Errorf("unmarshal key info: %w", err)
86 | }
87 |
88 | if len(keyInfo.SubjectPublicKey.Bytes) < 7 {
89 | return ekey, fmt.Errorf("public key is too short")
90 | }
91 |
92 | pubkeyData, err := asn1.Marshal(keyInfo.SubjectPublicKey)
93 | if err != nil {
94 | return ekey, fmt.Errorf("marshal public key: %w", err)
95 | }
96 |
97 | pubKey := big.NewInt(0)
98 | pubKey.SetBytes(pubkeyData[7:])
99 |
100 | sharedSecret := DiffieHellmanSharedSecret(dhKey, pubKey)
101 | sharedKey := sharedSecret.Bytes()
102 | sharedKey = append(sharedKey, dhClientNonce...)
103 | sharedKey = append(sharedKey, paPKASRep.ServerDHNonce...)
104 |
105 | var keyType int32
106 |
107 | switch asRep.EncPart.EType {
108 | case etypeID.AES256_CTS_HMAC_SHA1_96:
109 | keyType = etypeID.AES256_CTS_HMAC_SHA1_96
110 | sharedKey = truncateKey(sharedKey, 32)
111 | case etypeID.AES128_CTS_HMAC_SHA1_96:
112 | keyType = etypeID.AES128_CTS_HMAC_SHA1_96
113 | sharedKey = truncateKey(sharedKey, 16)
114 | default:
115 | return ekey, fmt.Errorf("PKInit is not implemented for EType %d", asRep.EncPart.EType)
116 | }
117 |
118 | return types.EncryptionKey{
119 | KeyType: keyType,
120 | KeyValue: sharedKey,
121 | }, nil
122 | }
123 |
124 | func unmarshalFromRawValue(data []byte, v any) error {
125 | var raw asn1.RawValue
126 |
127 | rest, err := asn1.Unmarshal(data, &raw)
128 | if err != nil {
129 | return fmt.Errorf("unmarshal raw: %w", err)
130 | }
131 |
132 | if len(rest) != 0 {
133 | return fmt.Errorf("remaining data found after unmarshalling raw value")
134 | }
135 |
136 | rest, err = asn1.Unmarshal(raw.Bytes, v)
137 | if err != nil {
138 | return err
139 | }
140 |
141 | if len(rest) != 0 {
142 | return fmt.Errorf("remaining data found after unmarshalling")
143 | }
144 |
145 | return nil
146 | }
147 |
148 | func truncateKey(key []byte, size int) []byte {
149 | var output []byte
150 |
151 | idx := byte(0)
152 |
153 | for len(output) < size {
154 | digest := sha1.Sum(append([]byte{idx}, key...))
155 | if len(output)+len(digest) > size {
156 | output = append(output, digest[:size-len(output)]...)
157 |
158 | break
159 | }
160 |
161 | output = append(output, digest[:]...)
162 |
163 | idx++
164 | }
165 |
166 | return output
167 | }
168 |
--------------------------------------------------------------------------------
/pkinit/asreq.go:
--------------------------------------------------------------------------------
1 | package pkinit
2 |
3 | import (
4 | cryptRand "crypto/rand"
5 | "crypto/rsa"
6 | "crypto/sha1"
7 | "crypto/x509"
8 | "encoding/asn1"
9 | "fmt"
10 | "math/big"
11 | mathRand "math/rand"
12 | "time"
13 |
14 | "github.com/jcmturner/gokrb5/v8/config"
15 | "github.com/jcmturner/gokrb5/v8/iana/nametype"
16 | "github.com/jcmturner/gokrb5/v8/messages"
17 | "github.com/jcmturner/gokrb5/v8/types"
18 | )
19 |
20 | // NewASReq generates an ASReq configured for PKINIT.
21 | func NewASReq(
22 | username string, domain string, cert *x509.Certificate, key *rsa.PrivateKey, dhKey *big.Int, config *config.Config,
23 | ) (asReq messages.ASReq, dhClientNonce []byte, err error) {
24 | asReq, err = messages.NewASReqForTGT(domain, config, types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, username))
25 | if err != nil {
26 | return asReq, nil, fmt.Errorf("build generic asreq: %w", err)
27 | }
28 |
29 | dhClientNonce = NewDiffieHellmanNonce()
30 |
31 | return asReq, dhClientNonce, ConfigureASReq(&asReq, cert, key, dhKey, dhClientNonce)
32 | }
33 |
34 | // ConfigureASReq configures an ASReq for PKINIT.
35 | func ConfigureASReq(
36 | asReq *messages.ASReq, cert *x509.Certificate, key *rsa.PrivateKey, dhKey *big.Int, dhClientNonce []byte,
37 | ) error {
38 | pkAuthenticatorChecksum, err := calculatePKAuthenticatorChecksum(asReq.ReqBody)
39 | if err != nil {
40 | return fmt.Errorf("calculate checksum: %w", err)
41 | }
42 |
43 | publicKey := DiffieHellmanPublicKey(dhKey)
44 |
45 | publicKeyBytes, err := asn1.MarshalWithParams(publicKey, "int")
46 | if err != nil {
47 | return err
48 | }
49 |
50 | now := time.Now().UTC()
51 |
52 | authPack := AuthPack{
53 | PKAuthenticator: PKAuthenticator{
54 | CUSec: int(now.UnixMicro() - now.Truncate(time.Millisecond).UnixMicro()),
55 | CTime: now,
56 | Nonce: mathRand.Intn(4294967295), //nolint:gosec
57 | Checksum: pkAuthenticatorChecksum,
58 | },
59 | ClientPublicValue: SubjectPublicKeyInfo{
60 | Algorithm: AlgorithmIdentifier{
61 | Algorithm: asn1.ObjectIdentifier{1, 2, 840, 10046, 2, 1},
62 | Parameters: DomainParameters{
63 | P: DiffieHellmanPrime,
64 | G: 2,
65 | Q: 0,
66 | },
67 | },
68 | PublicKey: asn1.BitString{
69 | Bytes: publicKeyBytes,
70 | BitLength: len(publicKeyBytes) * 8,
71 | },
72 | },
73 | SupportedCMSTypes: nil,
74 | ClientDHNonce: dhClientNonce,
75 | }
76 |
77 | authPackBytes, err := asn1.Marshal(authPack)
78 | if err != nil {
79 | return fmt.Errorf("marshal auth pack: %w", err)
80 | }
81 |
82 | signedAuthPack, err := PKCS7Sign(authPackBytes, key, cert)
83 | if err != nil {
84 | return fmt.Errorf("create signed authpack: %w", err)
85 | }
86 |
87 | pkASReq := struct {
88 | SignedAuthPack []byte `asn1:"tag:0"`
89 | TrustedIdentifiers types.PrincipalName `asn1:"tag:1,explicit,optional"`
90 | KDCPKID []byte `asn1:"tag:2,optional"`
91 | }{
92 | SignedAuthPack: signedAuthPack,
93 | }
94 |
95 | pkASReqBytes, err := asn1.Marshal(pkASReq)
96 | if err != nil {
97 | return fmt.Errorf("marshal ASReq: %w", err)
98 | }
99 |
100 | const paDataTypePKASReq = 16
101 |
102 | asReq.PAData = append(asReq.PAData, types.PAData{
103 | PADataType: paDataTypePKASReq,
104 | PADataValue: pkASReqBytes,
105 | })
106 |
107 | return nil
108 | }
109 |
110 | // NewDiffieHellmanNonce generates a nonce for the Diffie Hellman key exchange.
111 | func NewDiffieHellmanNonce() []byte {
112 | return randomBytes(32)
113 | }
114 |
115 | func calculatePKAuthenticatorChecksum(asReqBody messages.KDCReqBody) ([]byte, error) {
116 | bodyBytes, err := asReqBody.Marshal()
117 | if err != nil {
118 | return nil, fmt.Errorf("marshal ASReq body: %w", err)
119 | }
120 |
121 | hash := sha1.Sum(bodyBytes)
122 |
123 | return hash[:], nil
124 | }
125 |
126 | func randomBytes(n int) []byte {
127 | b := make([]byte, n)
128 |
129 | _, err := cryptRand.Read(b)
130 | if err != nil {
131 | panic(err.Error())
132 | }
133 |
134 | return b
135 | }
136 |
--------------------------------------------------------------------------------
/pkinit/diffie_hellman.go:
--------------------------------------------------------------------------------
1 | package pkinit
2 |
3 | import (
4 | "encoding/hex"
5 | "fmt"
6 | "math/big"
7 | )
8 |
9 | var (
10 | // DiffieHellmanPrime is the Diffie Hellman prime (P) that is acccepted by PKINIT.
11 | DiffieHellmanPrime = big.NewInt(0)
12 | // DiffieHellmanPrime is the Diffie Hellman base (G) that is acccepted by PKINIT.
13 | DiffieHellmanBase = big.NewInt(2)
14 | )
15 |
16 | func init() {
17 | pBytes, err := hex.DecodeString(
18 | "00ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020b" +
19 | "bea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe135" +
20 | "6d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb" +
21 | "5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff")
22 | if err != nil {
23 | panic(fmt.Sprintf("decode Diffie-Hellman parameter p: %v", err))
24 | }
25 |
26 | DiffieHellmanPrime.SetBytes(pBytes)
27 | }
28 |
29 | // DiffieHellmanPublicKey derives the Diffie Hellman public key from the
30 | // provided private key with the parameters that are accepted by PKINIT.
31 | func DiffieHellmanPublicKey(privateKey *big.Int) *big.Int {
32 | publicKey := new(big.Int).Exp(DiffieHellmanBase, privateKey, DiffieHellmanPrime)
33 |
34 | return publicKey
35 | }
36 |
37 | // DiffieHellmanSharedSecret derives the Diffie Hellman shared secret with the
38 | // parameters that are accepted by PKINIT.
39 | func DiffieHellmanSharedSecret(privateKey *big.Int, publicKey *big.Int) *big.Int {
40 | sharedSecret := new(big.Int).Exp(publicKey, privateKey, DiffieHellmanPrime)
41 |
42 | return sharedSecret
43 | }
44 |
--------------------------------------------------------------------------------
/pkinit/exchange.go:
--------------------------------------------------------------------------------
1 | package pkinit
2 |
3 | import (
4 | "context"
5 | "crypto/rsa"
6 | "crypto/x509"
7 | "encoding/binary"
8 | "fmt"
9 | "io"
10 | "net"
11 | "time"
12 |
13 | "github.com/RedTeamPentesting/adauth"
14 | "github.com/RedTeamPentesting/adauth/ccachetools"
15 | "github.com/jcmturner/gokrb5/v8/config"
16 | "github.com/jcmturner/gokrb5/v8/credentials"
17 | "github.com/jcmturner/gokrb5/v8/messages"
18 | )
19 |
20 | // DefaultKerberosRoundtripDeadline is the maximum time a roundtrip with the KDC
21 | // can take before it is aborted. This deadline is for each KDC that is
22 | // considered.
23 | var DefaultKerberosRoundtripDeadline = 5 * time.Second
24 |
25 | // Authenticate obtains a ticket granting ticket using PKINIT and returns it in
26 | // a CCache which can be serialized using ccachetools.MarshalCCache.
27 | func Authenticate(
28 | ctx context.Context, user string, domain string, cert *x509.Certificate, key *rsa.PrivateKey,
29 | krbConfig *config.Config, opts ...Option,
30 | ) (*credentials.CCache, error) {
31 | if user == "" {
32 | return nil, fmt.Errorf("username is empty")
33 | }
34 |
35 | if domain == "" {
36 | return nil, fmt.Errorf("domain is empty")
37 | }
38 |
39 | dialer, roundtripDeadline, err := processOptions(opts)
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | asReq, dhClientNonce, err := NewASReq(user, domain, cert, key, key.D, krbConfig)
45 | if err != nil {
46 | return nil, fmt.Errorf("build ASReq: %w", err)
47 | }
48 |
49 | asRep, err := ASExchange(ctx, asReq, domain, krbConfig, dialer, roundtripDeadline)
50 | if err != nil {
51 | return nil, fmt.Errorf("exchange: %w", err)
52 | }
53 |
54 | _, err = Decrypt(&asRep, key.D, dhClientNonce)
55 | if err != nil {
56 | return nil, fmt.Errorf("decrypt ASRep: %w", err)
57 | }
58 |
59 | return ccachetools.NewCCacheFromASRep(asRep)
60 | }
61 |
62 | // ASExchange sends a ASReq to the KDC for the provided domain and returns the
63 | // ASRep.
64 | func ASExchange(
65 | ctx context.Context, asReq messages.ASReq, domain string, config *config.Config,
66 | dialer adauth.ContextDialer, roundtripDeadline time.Duration,
67 | ) (asRep messages.ASRep, err error) {
68 | asReqBytes, err := asReq.Marshal()
69 | if err != nil {
70 | return asRep, fmt.Errorf("marshal ASReq: %w", err)
71 | }
72 |
73 | asRepBytes, err := roundtrip(ctx, asReqBytes, config, domain, dialer, roundtripDeadline)
74 | if err != nil {
75 | return asRep, fmt.Errorf("roundtrip: %w", err)
76 | }
77 |
78 | err = asRep.Unmarshal(asRepBytes)
79 | if err != nil {
80 | return asRep, fmt.Errorf("unmarshal ASRep: %w", err)
81 | }
82 |
83 | return asRep, nil
84 | }
85 |
86 | // TGSExchange sends a TGSReq to the KDC for the provided domain and returns the
87 | // TGSRep.
88 | func TGSExchange(
89 | ctx context.Context, tgsReq messages.TGSReq, config *config.Config, domain string,
90 | dialer adauth.ContextDialer, roundtripDeadline time.Duration,
91 | ) (tgsRep messages.TGSRep, err error) {
92 | asReqBytes, err := tgsReq.Marshal()
93 | if err != nil {
94 | return tgsRep, fmt.Errorf("marshal ASReq: %w", err)
95 | }
96 |
97 | asRepBytes, err := roundtrip(ctx, asReqBytes, config, domain, dialer, roundtripDeadline)
98 | if err != nil {
99 | return tgsRep, fmt.Errorf("roundtrip: %w", err)
100 | }
101 |
102 | err = tgsRep.Unmarshal(asRepBytes)
103 | if err != nil {
104 | return tgsRep, fmt.Errorf("unmarshal ASRep: %w", err)
105 | }
106 |
107 | return tgsRep, nil
108 | }
109 |
110 | func roundtrip(
111 | ctx context.Context, request []byte, config *config.Config, domain string,
112 | dialer adauth.ContextDialer, roundtripDeadline time.Duration,
113 | ) (response []byte, err error) {
114 | _, kdcs, err := config.GetKDCs(domain, true)
115 | if err != nil {
116 | return nil, fmt.Errorf("get KDCs from config: %w", err)
117 | } else if len(kdcs) == 0 {
118 | return nil, fmt.Errorf("no KDCs found in config")
119 | }
120 |
121 | for i := 1; i <= len(kdcs); i++ {
122 | if ctx.Err() != nil {
123 | return nil, context.Cause(ctx)
124 | }
125 |
126 | response, err = roundtripForSingleKDC(ctx, request, kdcs[i], dialer, roundtripDeadline)
127 | if err == nil {
128 | return response, nil
129 | }
130 | }
131 |
132 | switch {
133 | case err != nil:
134 | return nil, err
135 | case ctx.Err() != nil:
136 | return nil, context.Cause(ctx)
137 | default:
138 | return nil, fmt.Errorf("unknown error")
139 | }
140 | }
141 |
142 | func roundtripForSingleKDC(
143 | ctx context.Context, request []byte, address string,
144 | dialer adauth.ContextDialer, roundtripDeadline time.Duration,
145 | ) ([]byte, error) {
146 | if dialer == nil {
147 | dialer = &net.Dialer{Timeout: roundtripDeadline}
148 | }
149 |
150 | ctx, cancel := context.WithTimeout(ctx, roundtripDeadline)
151 | defer cancel()
152 |
153 | conn, err := dialer.DialContext(ctx, "tcp", address)
154 | if err != nil {
155 | return nil, fmt.Errorf("dial: %w", err)
156 | }
157 |
158 | var (
159 | responseChan = make(chan []byte)
160 | errChan = make(chan error)
161 | )
162 |
163 | go func() {
164 | _ = conn.SetDeadline(time.Now().Add(roundtripDeadline))
165 |
166 | response, err := sendRecv(conn, request)
167 |
168 | _ = conn.Close()
169 |
170 | switch {
171 | case err != nil:
172 | errChan <- err
173 | default:
174 | responseChan <- response
175 | }
176 | }()
177 |
178 | select {
179 | case response := <-responseChan:
180 | return response, nil
181 | case err := <-errChan:
182 | return nil, err
183 | case <-ctx.Done():
184 | conn.Close() //nolint:gosec
185 |
186 | return nil, context.Cause(ctx)
187 | }
188 | }
189 |
190 | func sendRecv(conn net.Conn, request []byte) ([]byte, error) {
191 | requestLengthBytes := make([]byte, 4)
192 | binary.BigEndian.PutUint32(requestLengthBytes, uint32(len(request)))
193 |
194 | request = append(requestLengthBytes, request...) //nolint:makezero
195 |
196 | _, err := conn.Write(request)
197 | if err != nil {
198 | return nil, fmt.Errorf("error sending to KDC (%s): %w", conn.RemoteAddr().String(), err)
199 | }
200 |
201 | responseLengthBytes := make([]byte, 4)
202 |
203 | _, err = conn.Read(responseLengthBytes)
204 | if err != nil {
205 | return nil, fmt.Errorf("error reading response size header: %w", err)
206 | }
207 |
208 | responseLength := binary.BigEndian.Uint32(responseLengthBytes)
209 |
210 | responseBytes := make([]byte, responseLength)
211 |
212 | _, err = io.ReadFull(conn, responseBytes)
213 | if err != nil {
214 | return nil, fmt.Errorf("error reading response: %w", err)
215 | }
216 |
217 | if len(responseBytes) < 1 {
218 | return nil, fmt.Errorf("no response data from KDC %s", conn.RemoteAddr().String())
219 | }
220 |
221 | return responseBytes, nil
222 | }
223 |
224 | // Option can be passed to a function to modify the default behavior.
225 | type Option interface {
226 | isPKINITOption()
227 | }
228 |
229 | type option struct{}
230 |
231 | func (option) isPKINITOption() {}
232 |
233 | type dialerOption struct {
234 | option
235 | ContextDialer adauth.ContextDialer
236 | }
237 |
238 | // WithDialer can be used to set a custom dialer for communication with a DC.
239 | func WithDialer(dialer adauth.ContextDialer) Option {
240 | return dialerOption{ContextDialer: dialer}
241 | }
242 |
243 | type deadlineOption struct {
244 | option
245 | Deadline time.Duration
246 | }
247 |
248 | // WithRoundtripDeadline can be used to set a deadline for a single
249 | // request-response roundtrip with a single KDC.
250 | func WithRoundtripDeadline(deadline time.Duration) Option {
251 | return deadlineOption{Deadline: deadline}
252 | }
253 |
254 | func processOptions(opts []Option) (dialer adauth.ContextDialer, roundtripDeadline time.Duration, err error) {
255 | roundtripDeadline = DefaultKerberosRoundtripDeadline
256 |
257 | for _, opt := range opts {
258 | switch o := opt.(type) {
259 | case dialerOption:
260 | dialer = o.ContextDialer
261 | case deadlineOption:
262 | roundtripDeadline = o.Deadline
263 | default:
264 | return nil, 0, fmt.Errorf("unknown option: %T", o)
265 | }
266 | }
267 |
268 | if dialer == nil {
269 | dialer = &net.Dialer{Timeout: roundtripDeadline}
270 | }
271 |
272 | return dialer, roundtripDeadline, nil
273 | }
274 |
--------------------------------------------------------------------------------
/pkinit/pkcs7.go:
--------------------------------------------------------------------------------
1 | package pkinit
2 |
3 | import (
4 | "crypto"
5 | "crypto/rsa"
6 | "crypto/sha1"
7 | "crypto/x509"
8 | "crypto/x509/pkix"
9 | "encoding/asn1"
10 | "fmt"
11 | )
12 |
13 | // PKCS7Sign signs the data according to PKCS#7.
14 | func PKCS7Sign(data []byte, key *rsa.PrivateKey, cert *x509.Certificate) ([]byte, error) {
15 | serializedData, err := asn1.Marshal(data)
16 | if err != nil {
17 | return nil, fmt.Errorf("marshal data: %w", err)
18 | }
19 |
20 | rawCert, err := RawCertificate(cert)
21 | if err != nil {
22 | return nil, fmt.Errorf("marshal certificate: %w", err)
23 | }
24 |
25 | digest := sha1.Sum(data)
26 |
27 | serializedDigest, err := asn1.Marshal(digest[:])
28 | if err != nil {
29 | return nil, fmt.Errorf("marshal digest: %w", err)
30 | }
31 |
32 | serializedPKInitOID, err := asn1.Marshal(asn1.ObjectIdentifier{1, 3, 6, 1, 5, 2, 3, 1})
33 | if err != nil {
34 | return nil, fmt.Errorf("marshal PKInit OID: %w", err)
35 | }
36 |
37 | sha1AlgorithmIdentifier := pkix.AlgorithmIdentifier{
38 | Algorithm: asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26},
39 | }
40 |
41 | rsaWithSHA1AlgorithmIdentifier := pkix.AlgorithmIdentifier{
42 | Algorithm: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5},
43 | }
44 |
45 | authenticatedAttributes := []Attribute{
46 | {
47 | // ContentType
48 | Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3},
49 | // id-pkinit-authData
50 | Value: asn1.RawValue{Tag: 17, IsCompound: true, Bytes: serializedPKInitOID},
51 | },
52 | {
53 | // MessageDigest
54 | Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4},
55 | Value: asn1.RawValue{Tag: 17, IsCompound: true, Bytes: serializedDigest},
56 | },
57 | }
58 |
59 | signature, err := signAuthenicatedAttributes(authenticatedAttributes, key)
60 | if err != nil {
61 | return nil, fmt.Errorf("sign authenticated data: %w", err)
62 | }
63 |
64 | signedDataBytes, err := asn1.Marshal(SignedData{
65 | Version: 3,
66 | DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{sha1AlgorithmIdentifier},
67 | ContentInfo: ContentInfo{
68 | // id-pkinit-authData
69 | ContentType: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 2, 3, 1},
70 | Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: serializedData, IsCompound: true},
71 | },
72 | Certificates: rawCert,
73 | SignerInfos: []SignerInfo{
74 | {
75 | Version: 1,
76 | IssuerAndSerialNumber: IssuerAndSerial{
77 | IssuerName: asn1.RawValue{FullBytes: cert.RawIssuer},
78 | SerialNumber: cert.SerialNumber,
79 | },
80 | DigestAlgorithm: sha1AlgorithmIdentifier,
81 | AuthenticatedAttributes: authenticatedAttributes,
82 | DigestEncryptionAlgorithm: rsaWithSHA1AlgorithmIdentifier,
83 | EncryptedDigest: signature,
84 | },
85 | },
86 | })
87 | if err != nil {
88 | return nil, fmt.Errorf("marshal signed data: %w", err)
89 | }
90 |
91 | contentInfo := ContentInfo{
92 | // signed data
93 | ContentType: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2},
94 | Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: signedDataBytes, IsCompound: true},
95 | }
96 |
97 | signedContent, err := asn1.Marshal(contentInfo)
98 | if err != nil {
99 | return nil, fmt.Errorf("marshal signed content: %w", err)
100 | }
101 |
102 | return signedContent, nil
103 | }
104 |
105 | func signAuthenicatedAttributes(attrs []Attribute, key *rsa.PrivateKey) ([]byte, error) {
106 | rawAuthenticatedAttributesAsSequence, err := asn1.Marshal(struct {
107 | A []Attribute `asn1:"set"`
108 | }{A: attrs})
109 | if err != nil {
110 | return nil, fmt.Errorf("marshal authenticated data: %w", err)
111 | }
112 |
113 | // Remove the leading sequence octets
114 | var rawAuthenticatedAttributes asn1.RawValue
115 |
116 | _, err = asn1.Unmarshal(rawAuthenticatedAttributesAsSequence, &rawAuthenticatedAttributes)
117 | if err != nil {
118 | return nil, fmt.Errorf("remove sequence bytes: %w", err)
119 | }
120 |
121 | hash := sha1.Sum(rawAuthenticatedAttributes.Bytes)
122 |
123 | return rsa.SignPKCS1v15(nil, key, crypto.SHA1, hash[:])
124 | }
125 |
--------------------------------------------------------------------------------
/pkinit/unpacthehash.go:
--------------------------------------------------------------------------------
1 | package pkinit
2 |
3 | import (
4 | "context"
5 | "crypto/rsa"
6 | "crypto/x509"
7 | "encoding/asn1"
8 | "encoding/hex"
9 | "fmt"
10 | "os"
11 | "strings"
12 |
13 | "github.com/RedTeamPentesting/adauth"
14 | "github.com/RedTeamPentesting/adauth/ccachetools"
15 | "github.com/jcmturner/gokrb5/v8/config"
16 | "github.com/jcmturner/gokrb5/v8/credentials"
17 | "github.com/jcmturner/gokrb5/v8/iana/nametype"
18 | "github.com/jcmturner/gokrb5/v8/iana/patype"
19 | "github.com/jcmturner/gokrb5/v8/krberror"
20 | "github.com/jcmturner/gokrb5/v8/messages"
21 | "github.com/jcmturner/gokrb5/v8/types"
22 | "github.com/oiweiwei/go-msrpc/msrpc/pac"
23 | "github.com/oiweiwei/go-msrpc/ndr"
24 | v9_types "github.com/oiweiwei/gokrb5.fork/v9/types"
25 | )
26 |
27 | // UnPACTheHash retrieves the user's NT hash via PKINIT using the provided PFX
28 | // file. The DC argument is optional.
29 | func UnPACTheHashFromPFX(
30 | ctx context.Context, username string, domain string, pfxFile string, pfxPassword string,
31 | dc string, opts ...Option,
32 | ) (*credentials.CCache, *Hash, error) {
33 | pfxData, err := os.ReadFile(pfxFile)
34 | if err != nil {
35 | return nil, nil, fmt.Errorf("read PFX file: %w", err)
36 | }
37 |
38 | return UnPACTheHashFromPFXData(ctx, username, domain, pfxData, pfxPassword, dc, opts...)
39 | }
40 |
41 | // UnPACTheHash retrieves the user's NT hash via PKINIT using the provided PFX
42 | // data. The DC argument is optional.
43 | func UnPACTheHashFromPFXData(
44 | ctx context.Context, username string, domain string, pfxData []byte, pfxPassword string,
45 | dc string, opts ...Option,
46 | ) (*credentials.CCache, *Hash, error) {
47 | cred, err := adauth.CredentialFromPFXBytes(username, domain, pfxData, pfxPassword)
48 | if err != nil {
49 | return nil, nil, fmt.Errorf("build credentials from PFX: %w", err)
50 | }
51 |
52 | if dc != "" {
53 | cred.SetDC(dc)
54 | }
55 |
56 | krbConf, err := cred.KerberosConfig(ctx)
57 | if err != nil {
58 | return nil, nil, fmt.Errorf("configure Kerberos: %w", err)
59 | }
60 |
61 | rsaKey, ok := cred.ClientCertKey.(*rsa.PrivateKey)
62 | if !ok {
63 | return nil, nil, fmt.Errorf("cannot use %T because PKINIT requires an RSA key", cred.ClientCertKey)
64 | }
65 |
66 | return UnPACTheHash(ctx, cred.Username, cred.Domain, cred.ClientCert, rsaKey, krbConf, opts...)
67 | }
68 |
69 | // UnPACTheHash retrieves the user's NT hash via PKINIT using the provided
70 | // certificates.
71 | func UnPACTheHash(
72 | ctx context.Context, user string, domain string, cert *x509.Certificate, key *rsa.PrivateKey,
73 | krbConfig *config.Config, opts ...Option,
74 | ) (*credentials.CCache, *Hash, error) {
75 | dialer, roundtripDeadline, err := processOptions(opts)
76 | if err != nil {
77 | return nil, nil, err
78 | }
79 |
80 | krbConfig = &config.Config{
81 | LibDefaults: krbConfig.LibDefaults,
82 | Realms: krbConfig.Realms,
83 | DomainRealm: krbConfig.DomainRealm,
84 | }
85 |
86 | krbConfig.LibDefaults.Proxiable = false
87 |
88 | asReq, dhClientNonce, err := NewASReq(user, domain, cert, key, key.D, krbConfig)
89 | if err != nil {
90 | return nil, nil, fmt.Errorf("build ASReq: %w", err)
91 | }
92 |
93 | asReq.PAData = append(asReq.PAData, (&PAPACRequest{IncludePAC: true}).AsPAData())
94 |
95 | asRep, err := ASExchange(ctx, asReq, domain, krbConfig, dialer, roundtripDeadline)
96 | if err != nil {
97 | return nil, nil, fmt.Errorf("AS exchange: %w", err)
98 | }
99 |
100 | pkinitKey, err := Decrypt(&asRep, key.D, dhClientNonce)
101 | if err != nil {
102 | return nil, nil, fmt.Errorf("decrypt ASRep: %w", err)
103 | }
104 |
105 | ccache, err := ccachetools.NewCCacheFromASRep(asRep)
106 | if err != nil {
107 | return nil, nil, fmt.Errorf("generate CCache: %w", err)
108 | }
109 |
110 | tgsReq, err := messages.NewUser2UserTGSReq(
111 | types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, user), strings.ToUpper(asRep.CRealm), krbConfig,
112 | asRep.Ticket, asRep.DecryptedEncPart.Key, types.NewPrincipalName(nametype.KRB_NT_UNKNOWN, user),
113 | false, asRep.Ticket)
114 | if err != nil {
115 | return ccache, nil, fmt.Errorf("generate TGSReq: %w", err)
116 | }
117 |
118 | tgsReq.ReqBody.CName = types.PrincipalName{}
119 |
120 | tgsReq.PAData, err = generatePAData(user, asRep.Ticket, asRep.DecryptedEncPart.Key)
121 | if err != nil {
122 | return ccache, nil, fmt.Errorf("generate PAData sequence: %w", err)
123 | }
124 |
125 | tgsRep, err := TGSExchange(ctx, tgsReq, krbConfig, domain, dialer, roundtripDeadline)
126 | if err != nil {
127 | return ccache, nil, fmt.Errorf("TGS exchange: %w", err)
128 | }
129 |
130 | err = tgsRep.DecryptEncPart(asRep.DecryptedEncPart.Key)
131 | if err != nil {
132 | return ccache, nil, fmt.Errorf("decrypt TGSRep: %w", err)
133 | }
134 |
135 | err = tgsRep.Ticket.Decrypt(asRep.DecryptedEncPart.Key)
136 | if err != nil {
137 | return ccache, nil, fmt.Errorf("decrypt service ticket: %w", err)
138 | }
139 |
140 | ntHash, err := extractNTHash(tgsRep.Ticket, pkinitKey)
141 | if err != nil {
142 | return nil, nil, fmt.Errorf("extract NT hash: %w", err)
143 | }
144 |
145 | return ccache, ntHash, nil
146 | }
147 |
148 | func extractNTHash(
149 | tkt messages.Ticket, pkinitKey types.EncryptionKey,
150 | ) (hash *Hash, err error) {
151 | ifRelevant := types.ADIfRelevant{}
152 |
153 | for _, authData := range tkt.DecryptedEncPart.AuthorizationData {
154 | if authData.ADType != 1 {
155 | continue
156 | }
157 |
158 | _, err = asn1.Unmarshal(authData.ADData, &ifRelevant)
159 | if err != nil {
160 | return nil, fmt.Errorf("unmarshal ADIfRelevant container: %w", err)
161 | }
162 | }
163 |
164 | if len(ifRelevant) == 0 {
165 | return nil, fmt.Errorf("no ADIfRelevant container present")
166 | }
167 |
168 | var pacData []byte
169 |
170 | for _, authData := range ifRelevant {
171 | if authData.ADType != 128 {
172 | continue
173 | }
174 |
175 | pacData = authData.ADData
176 | }
177 |
178 | if len(pacData) == 0 {
179 | return nil, fmt.Errorf("no PACTYPE container present: %w", err)
180 | }
181 |
182 | pacType := pac.PACType{}
183 |
184 | err = ndr.Unmarshal(pacData, &pacType, ndr.Opaque)
185 | if err != nil {
186 | return nil, fmt.Errorf("unmarshal PACTYPE: %w", err)
187 | }
188 |
189 | var pacCredentialInfoBuffer []byte
190 |
191 | for _, buffer := range pacType.Buffers {
192 | if buffer.Type != 2 {
193 | continue
194 | }
195 |
196 | if buffer.Offset+uint64(buffer.BufferLength) > uint64(len(pacData)) {
197 | return nil, fmt.Errorf("PAC_CREDENTIAL_INFO buffer offset and length are outside of data range")
198 | }
199 |
200 | pacCredentialInfoBuffer = pacData[buffer.Offset : buffer.Offset+uint64(buffer.BufferLength)]
201 | }
202 |
203 | if len(pacCredentialInfoBuffer) == 0 {
204 | return nil, fmt.Errorf("could not find PAC_CREDENTIAL_INFO buffer")
205 | }
206 |
207 | credInfo := pac.PACCredentialInfo{}
208 |
209 | err = ndr.Unmarshal(pacCredentialInfoBuffer, &credInfo, ndr.Opaque)
210 | if err != nil {
211 | return nil, fmt.Errorf("unmarshal PAC_CREDENTIAL_INFO: %w", err)
212 | }
213 |
214 | credData, err := credInfo.DecryptCredentialData((v9_types.EncryptionKey)(pkinitKey))
215 | if err != nil {
216 | return nil, fmt.Errorf("decrypt PAC_CREDENTIAL_DATA: %w", err)
217 | }
218 |
219 | for _, cred := range credData.Credentials {
220 | if cred.NTLMSupplementalCredential == nil {
221 | continue
222 | }
223 |
224 | return NewHash(cred.NTLMSupplementalCredential)
225 | }
226 |
227 | return nil, fmt.Errorf("could not find NTLM_SUPPLEMENTAL_CREDENTIAL in SECPKG_SUPPLEMENTAL_CRED array")
228 | }
229 |
230 | func generatePAData(
231 | username string, tgt messages.Ticket, sessionKey types.EncryptionKey,
232 | ) (paData types.PADataSequence, err error) {
233 | auth, err := types.NewAuthenticator(tgt.Realm, types.NewPrincipalName(nametype.KRB_NT_UNKNOWN, username))
234 | if err != nil {
235 | return paData, krberror.Errorf(err, krberror.KRBMsgError, "error generating new authenticator")
236 | }
237 |
238 | auth.SeqNumber = 0
239 |
240 | apReq, err := messages.NewAPReq(tgt, sessionKey, auth)
241 | if err != nil {
242 | return paData, krberror.Errorf(err, krberror.KRBMsgError, "error generating new AP_REQ")
243 | }
244 |
245 | apb, err := apReq.Marshal()
246 | if err != nil {
247 | return paData, krberror.Errorf(err, krberror.EncodingError,
248 | "error marshaling AP_REQ for pre-authentication data",
249 | )
250 | }
251 |
252 | return types.PADataSequence{types.PAData{
253 | PADataType: patype.PA_TGS_REQ,
254 | PADataValue: apb,
255 | }}, nil
256 | }
257 |
258 | // Hash represents LM and NT password hashes.
259 | type Hash struct {
260 | nt []byte
261 | lm []byte
262 | }
263 |
264 | func NewHash(ntlmSupplementalCredential *pac.NTLMSupplementalCredential) (*Hash, error) {
265 | hash := &Hash{}
266 |
267 | if ntlmSupplementalCredential.Flags&1 > 0 {
268 | hash.lm = ntlmSupplementalCredential.LMPassword
269 | }
270 |
271 | if ntlmSupplementalCredential.Flags&2 > 0 {
272 | hash.nt = ntlmSupplementalCredential.NTPassword
273 | }
274 |
275 | // according to the flags, there are no hashes, but we better check for ourselves
276 | if hash.Empty() {
277 | if len(ntlmSupplementalCredential.NTPassword) == 16 && nonZero(ntlmSupplementalCredential.NTPassword) {
278 | hash.nt = ntlmSupplementalCredential.NTPassword
279 | }
280 |
281 | if len(ntlmSupplementalCredential.LMPassword) == 16 && nonZero(ntlmSupplementalCredential.LMPassword) {
282 | hash.lm = ntlmSupplementalCredential.LMPassword
283 | }
284 | }
285 |
286 | if hash.Empty() {
287 | return nil, fmt.Errorf("NTLM_SUPPLEMENTAL_CREDENTIAL does not contain hashes "+
288 | "(Flags: 0x%x, NTPassword: 0x%x, LMPassword 0x%x)",
289 | ntlmSupplementalCredential.Flags,
290 | ntlmSupplementalCredential.NTPassword, ntlmSupplementalCredential.LMPassword)
291 | }
292 |
293 | return hash, nil
294 | }
295 |
296 | // NT returns the hex-encoded NT hash or an empty string if no NT hash is
297 | // present.
298 | func (h *Hash) NT() string {
299 | return hex.EncodeToString(h.nt)
300 | }
301 |
302 | // NTBytes returns the binary NT hash or an empty slice if no NT hash is
303 | // present.
304 | func (h *Hash) NTBytes() []byte {
305 | return h.nt
306 | }
307 |
308 | // NTPresent indicates whether or not an NT hash is present.
309 | func (h *Hash) NTPresent() bool {
310 | return h.nt != nil
311 | }
312 |
313 | // LM returns the hex-encoded LM hash or an empty string if no LM hash is
314 | // present.
315 | func (h *Hash) LM() string {
316 | return hex.EncodeToString(h.lm)
317 | }
318 |
319 | // LMPresent indicates whether or not an LM hash is present.
320 | func (h *Hash) LMPresent() bool {
321 | return h.lm != nil
322 | }
323 |
324 | // LMBytes returns the binary LM hash or an empty slice if no LM hash is
325 | // present.
326 | func (h *Hash) LMBytes() []byte {
327 | return h.lm
328 | }
329 |
330 | // Empty returns true if the structure contains neither LM nor NT hash data.
331 | func (h *Hash) Empty() bool {
332 | return h.nt == nil && h.lm == nil
333 | }
334 |
335 | // Combined returns the hex-encoded hashes in LM:NT format. If any of these
336 | // hashes is not present, they are replaced by their respective empty hash
337 | // value.
338 | func (h *Hash) Combined() string {
339 | lm := h.LM()
340 | if lm == "" {
341 | lm = "aad3b435b51404eeaad3b435b51404ee"
342 | }
343 |
344 | nt := h.NT()
345 | if nt == "" {
346 | nt = "31d6cfe0d16ae931b73c59d7e0c089c0"
347 | }
348 |
349 | return lm + ":" + nt
350 | }
351 |
352 | func nonZero(d []byte) bool {
353 | for i := 0; i < len(d); i++ {
354 | if d[i] != 0 {
355 | return true
356 | }
357 | }
358 |
359 | return false
360 | }
361 |
--------------------------------------------------------------------------------
/resolver.go:
--------------------------------------------------------------------------------
1 | package adauth
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "strings"
8 | )
9 |
10 | type Resolver interface {
11 | LookupAddr(ctx context.Context, addr string) ([]string, error)
12 | LookupIP(ctx context.Context, network string, host string) ([]net.IP, error)
13 | LookupSRV(ctx context.Context, service string, proto string, name string) (string, []*net.SRV, error)
14 | }
15 |
16 | var _ Resolver = net.DefaultResolver
17 |
18 | type resolver struct {
19 | Resolver
20 | debugFn func(string, ...any)
21 | }
22 |
23 | func ensureResolver(r Resolver, debug func(string, ...any)) *resolver {
24 | if r == nil {
25 | return &resolver{Resolver: net.DefaultResolver, debugFn: debug}
26 | }
27 |
28 | return &resolver{Resolver: r, debugFn: debug}
29 | }
30 |
31 | func (r *resolver) debug(format string, a ...any) {
32 | if r.debugFn == nil {
33 | return
34 | }
35 |
36 | r.debugFn(format, a...)
37 | }
38 |
39 | func (r *resolver) LookupFirstService(ctx context.Context, protocol string, domain string) (string, int, error) {
40 | _, addrs, err := r.LookupSRV(ctx, protocol, "tcp", domain)
41 | if err != nil {
42 | if strings.EqualFold(protocol, "ldaps") {
43 | host, _, srvLDAPErr := r.LookupFirstService(ctx, "ldap", domain)
44 | if srvLDAPErr == nil {
45 | return host, 636, nil
46 | }
47 | }
48 |
49 | return "", 0, fmt.Errorf("lookup %q service of domain %q: %w", protocol, domain, err)
50 | }
51 |
52 | if len(addrs) == 0 {
53 | return "", 0, fmt.Errorf("no %q services were discovered for domain %q", protocol, domain)
54 | }
55 |
56 | return strings.TrimRight(addrs[0].Target, "."), int(addrs[0].Port), nil
57 | }
58 |
59 | func (r *resolver) LookupDCByDomain(ctx context.Context, domain string) (string, error) {
60 | // Unfortunately, Go does not implement SOA lookups, so we lookup the domain
61 | // for DC IPs and reverse lookup their hostnames instead.
62 | dcAddrs, err := r.LookupIP(context.Background(), "ip", domain)
63 | if err != nil {
64 | return "", fmt.Errorf("lookup domain itself: %w", err)
65 | }
66 |
67 | if len(dcAddrs) == 0 {
68 | return "", fmt.Errorf("looking up domain itself returned no results")
69 | }
70 |
71 | dcAddr := dcAddrs[0].String()
72 |
73 | names, err := r.LookupAddr(context.Background(), dcAddr)
74 | if err == nil {
75 | domain, names = splitResultsInDomainAndHostname(names, domain)
76 |
77 | switch {
78 | case len(names) > 0:
79 | dcAddr = strings.TrimRight(names[0], ".")
80 | case domain != "":
81 | dcAddr = domain
82 |
83 | r.debug("Warning: reverse lookup of DC only returned domain name, using domain name %q as DC hostname",
84 | domain)
85 | default:
86 | r.debug("Warning: reverse lookup of DC did not contain a hostname")
87 | }
88 | } else {
89 | r.debug("Warning: could not reverse lookup DC IP: %v", err)
90 | }
91 |
92 | return dcAddr, nil
93 | }
94 |
95 | // A reverse lookup of the DC IP returns both the DCs hostname and the name of
96 | // the domain. This function splits the results in these categories.
97 | func splitResultsInDomainAndHostname(
98 | hostnames []string, domain string,
99 | ) (domainFromHostnames string, filtered []string) {
100 | var d string
101 |
102 | for _, hostname := range hostnames {
103 | hostname = strings.TrimRight(hostname, ".")
104 |
105 | if strings.EqualFold(hostname, domain) {
106 | d = hostname
107 | } else {
108 | filtered = append(filtered, hostname)
109 | }
110 | }
111 |
112 | return d, filtered
113 | }
114 |
--------------------------------------------------------------------------------
/resolver_test.go:
--------------------------------------------------------------------------------
1 | package adauth_test
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 |
8 | "github.com/RedTeamPentesting/adauth"
9 | )
10 |
11 | type testResolver struct {
12 | HostToAddr map[string][]net.IP
13 | AddrToHost map[string][]string
14 | SRV map[string]map[string]map[string]struct {
15 | Name string
16 | SRV []*net.SRV
17 | }
18 | Error error
19 | }
20 |
21 | func (r *testResolver) LookupAddr(ctx context.Context, addr string) ([]string, error) {
22 | if r.Error != nil {
23 | return nil, r.Error
24 | }
25 |
26 | if r.AddrToHost == nil {
27 | return nil, nil
28 | }
29 |
30 | return r.AddrToHost[addr], nil
31 | }
32 |
33 | func (r *testResolver) LookupIP(ctx context.Context, network string, host string) ([]net.IP, error) {
34 | if r.Error != nil {
35 | return nil, r.Error
36 | }
37 |
38 | if r.HostToAddr == nil {
39 | return nil, nil
40 | }
41 |
42 | addrs := r.HostToAddr[host]
43 |
44 | switch network {
45 | case "ip":
46 | return addrs, nil
47 | case "ip4":
48 | var ipv4s []net.IP
49 |
50 | for _, addr := range addrs {
51 | if addr.To4() != nil {
52 | ipv4s = append(ipv4s, addr)
53 | }
54 | }
55 |
56 | return ipv4s, nil
57 | case "ip6":
58 | var ipv6s []net.IP
59 |
60 | for _, addr := range addrs {
61 | if addr.To4() == nil {
62 | ipv6s = append(ipv6s, addr)
63 | }
64 | }
65 |
66 | return ipv6s, nil
67 | default:
68 | return nil, fmt.Errorf("invalid network: %q", network)
69 | }
70 | }
71 |
72 | func (r *testResolver) LookupSRV(
73 | ctx context.Context, service string, proto string, name string,
74 | ) (string, []*net.SRV, error) {
75 | if r.Error != nil {
76 | return "", nil, r.Error
77 | }
78 |
79 | if r.SRV == nil {
80 | return "", nil, nil
81 | }
82 |
83 | srvForService := r.SRV[service]
84 | if srvForService == nil {
85 | return "", nil, nil
86 | }
87 |
88 | srvForServiceAndProto := srvForService[proto]
89 | if srvForServiceAndProto == nil {
90 | return "", nil, nil
91 | }
92 |
93 | record := srvForServiceAndProto[name]
94 |
95 | return record.Name, record.SRV, nil
96 | }
97 |
98 | var _ adauth.Resolver = &testResolver{}
99 |
--------------------------------------------------------------------------------
/smbauth/smbauth.go:
--------------------------------------------------------------------------------
1 | package smbauth
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/RedTeamPentesting/adauth"
8 | "github.com/RedTeamPentesting/adauth/dcerpcauth"
9 | msrpcSMB2 "github.com/oiweiwei/go-msrpc/smb2"
10 | "github.com/oiweiwei/go-msrpc/ssp"
11 | "github.com/oiweiwei/go-msrpc/ssp/gssapi"
12 | "github.com/oiweiwei/go-msrpc/ssp/krb5"
13 | "github.com/oiweiwei/go-smb2.fork"
14 | )
15 |
16 | // Options holds options that modify the behavior of the Dialer function.
17 | type Options struct {
18 | // SMBOptions holds options for the SMB dialer. If SMBOptions is nil,
19 | // encryption/sealing will be enabled. Specify an empty slice to disable
20 | // this default.
21 | SMBOptions []msrpcSMB2.DialerOption
22 |
23 | // KerberosDialer is a custom dialer that is used to request Kerberos
24 | // tickets.
25 | KerberosDialer adauth.Dialer
26 |
27 | // Debug can be set to enable debug output, for example with
28 | // adauth.NewDebugFunc(...).
29 | Debug func(string, ...any)
30 | }
31 |
32 | func (opts *Options) debug(format string, a ...any) {
33 | if opts == nil || opts.Debug == nil {
34 | return
35 | }
36 |
37 | opts.Debug(format, a...)
38 | }
39 |
40 | // Dialer returns an SMB dialer which is prepared for authentication with the
41 | // given credentials. The dialer can be further customized with
42 | // options.SMBDialerOptions.
43 | func Dialer(
44 | ctx context.Context, creds *adauth.Credential, target *adauth.Target, options *Options,
45 | ) (*smb2.Dialer, error) {
46 | smbCreds, err := dcerpcauth.DCERPCCredentials(ctx, creds, &dcerpcauth.Options{
47 | Debug: options.debug,
48 | KerberosDialer: options.KerberosDialer,
49 | })
50 | if err != nil {
51 | return nil, err
52 | }
53 |
54 | dialerOptions := options.SMBOptions
55 | if dialerOptions == nil {
56 | dialerOptions = append(dialerOptions, msrpcSMB2.WithSeal())
57 | }
58 |
59 | switch {
60 | case target.UseKerberos || creds.ClientCert != nil:
61 | spn, err := target.SPN(ctx)
62 | if err != nil {
63 | return nil, fmt.Errorf("build SPN: %w", err)
64 | }
65 |
66 | options.debug("Using Kerberos with SPN %q", spn)
67 |
68 | krbConf, err := creds.KerberosConfig(ctx)
69 | if err != nil {
70 | return nil, fmt.Errorf("generate Kerberos config: %w", err)
71 | }
72 |
73 | dialerOptions = append(dialerOptions, msrpcSMB2.WithSecurity(
74 | gssapi.WithTargetName(spn),
75 | gssapi.WithCredential(smbCreds),
76 | gssapi.WithMechanismFactory(ssp.KRB5, &krb5.Config{
77 | KRB5Config: krbConf,
78 | CCachePath: creds.CCache,
79 | DisablePAFXFAST: true,
80 | KDCDialer: options.KerberosDialer,
81 | }),
82 | ))
83 |
84 | return msrpcSMB2.NewDialer(dialerOptions...), nil
85 | default:
86 | options.debug("Using NTLM")
87 |
88 | secOptions := []gssapi.ContextOption{
89 | gssapi.WithCredential(smbCreds),
90 | gssapi.WithMechanismFactory(ssp.NTLM),
91 | }
92 |
93 | spn, err := target.SPN(ctx)
94 | if err == nil {
95 | secOptions = append(secOptions, gssapi.WithTargetName(spn))
96 | }
97 |
98 | dialerOptions = append(dialerOptions, msrpcSMB2.WithSecurity(secOptions...))
99 |
100 | return msrpcSMB2.NewDialer(dialerOptions...), nil
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/target.go:
--------------------------------------------------------------------------------
1 | package adauth
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "strings"
8 |
9 | "github.com/jcmturner/gokrb5/v8/credentials"
10 | "github.com/jcmturner/gokrb5/v8/iana/nametype"
11 | "github.com/jcmturner/gokrb5/v8/types"
12 | )
13 |
14 | // Target holds information about the authentication target.
15 | type Target struct {
16 | // Port holds the target's port which may be empty.
17 | Port string
18 | addr string
19 | hostname string
20 | ip net.IP
21 | // UseKerberos indicated that Kerberos authentication should be used to
22 | // authenticate to this target.
23 | //
24 | // Warning: `UseKerberos` is false when the only credential available is a
25 | // client certificate because in this case mTLS may also be used to
26 | // authenticate depending on the protocol (e.g. LDAP/HTTPS). If the protocol
27 | // that is used does not support using client certificates directly, you
28 | // should decide for Kerberos authentication if `target.UserKerberos &&
29 | // creds.ClientCert != nil` is `true`. In this case, Kerberos with PKINIT
30 | // will be used.
31 | UseKerberos bool
32 | // Protocol is a string that represents the protocol that is used when
33 | // communicating with this target. It is used to construct the SPN, however,
34 | // some protocol name corrections may be applied in this case, such as 'smb'
35 | // -> 'cifs'.
36 | Protocol string
37 | ccache string
38 |
39 | // Resolver can be used to set an alternative DNS resolver. If empty,
40 | // net.DefaultResolver is used.
41 | Resolver Resolver
42 | }
43 |
44 | // NewTarget creates a new target. The provided protocol is used to construct
45 | // the SPN, however, some protocol name corrections may be applied in this case,
46 | // such as 'smb' -> 'cifs'. The target parameter may or may not contain a port
47 | // and the protocol string will *not* influence the port of the resulting
48 | // Target.
49 | func NewTarget(protocol string, target string) *Target {
50 | return newTarget(protocol, target, false, "", nil)
51 | }
52 |
53 | func newTarget(protocol string, target string, useKerberos bool, ccache string, resolver Resolver) *Target {
54 | host, port, err := net.SplitHostPort(target)
55 | if err != nil {
56 | host = target
57 | }
58 |
59 | t := &Target{
60 | addr: host,
61 | Port: port,
62 | Protocol: protocol,
63 | UseKerberos: useKerberos,
64 | ccache: ccache,
65 | Resolver: resolver,
66 | }
67 |
68 | ip := net.ParseIP(host)
69 | if ip == nil {
70 | t.hostname = host
71 | } else {
72 | t.ip = ip
73 | }
74 |
75 | return t
76 | }
77 |
78 | // Address returns the address including the port if available. It will contain
79 | // either a hostname or an IP address depending on how the target was
80 | // constructed.
81 | func (t *Target) Address() string {
82 | if t.Port != "" {
83 | return net.JoinHostPort(t.addr, t.Port)
84 | } else {
85 | return t.addr
86 | }
87 | }
88 |
89 | // AddressWithoutPort is like Address but without the port.
90 | func (t *Target) AddressWithoutPort() string {
91 | return t.addr
92 | }
93 |
94 | // IP returns the target's IP address. If only the hostname is known, a lookup
95 | // will be performed.
96 | func (t *Target) IP(ctx context.Context) (net.IP, error) {
97 | if t.ip != nil {
98 | return t.ip, nil
99 | }
100 |
101 | if t.hostname == "" {
102 | return nil, fmt.Errorf("no IP or hostname known")
103 | }
104 |
105 | addrs, err := ensureResolver(t.Resolver, nil).LookupIP(ctx, "ip", t.hostname)
106 |
107 | switch {
108 | case err != nil:
109 | return nil, fmt.Errorf("lookup %s: %w", t.hostname, err)
110 | case len(addrs) > 1:
111 | return nil, fmt.Errorf("lookup of %s returned multiple names", t.hostname)
112 | case len(addrs) == 0:
113 | return nil, fmt.Errorf("lookup of %s returned no names", t.hostname)
114 | }
115 |
116 | t.ip = addrs[0]
117 |
118 | return addrs[0], nil
119 | }
120 |
121 | // Hostname returns the target's hostname. If only the IP address is known, a
122 | // lookup will be performed.
123 | func (t *Target) Hostname(ctx context.Context) (string, error) {
124 | if t.hostname != "" {
125 | return t.hostname, nil
126 | }
127 |
128 | if t.ip == nil {
129 | return "", fmt.Errorf("no IP or hostname known")
130 | }
131 |
132 | names, err := ensureResolver(t.Resolver, nil).LookupAddr(ctx, t.ip.String())
133 |
134 | switch {
135 | case err != nil:
136 | return "", fmt.Errorf("reverse lookup %s: %w", t.ip, err)
137 | case len(names) > 1:
138 | return "", fmt.Errorf("reverse lookup of %s returned multiple names", t.ip)
139 | case len(names) == 0:
140 | return "", fmt.Errorf("reverse lookup of %s returned no names", t.ip)
141 | }
142 |
143 | t.hostname = strings.TrimRight(names[0], ".")
144 |
145 | return t.hostname, nil
146 | }
147 |
148 | // SPN returns the target's service principal name. The protocol part of the SPN
149 | // *may* be changed to the generic 'host' in order to align with the SPN of
150 | // service tickets in a CCACHE file. Some protocol name translations will also
151 | // be applied such as 'smb' -> 'cifs'.
152 | func (t *Target) SPN(ctx context.Context) (string, error) {
153 | hostname, err := t.Hostname(ctx)
154 | if err != nil {
155 | return "", err
156 | }
157 |
158 | var spn string
159 |
160 | switch strings.ToLower(t.Protocol) {
161 | case "smb":
162 | spn = "cifs/" + hostname
163 | case "ldap", "ldaps":
164 | spn = "ldap/" + hostname
165 | case "http", "https":
166 | spn = "http/" + hostname
167 | case "kerberos":
168 | spn = "krbtgt"
169 | case "":
170 | spn = "host/" + hostname
171 | default:
172 | spn = t.Protocol + `/` + hostname
173 | }
174 |
175 | if t.ccache == "" || strings.HasPrefix(spn, "host/") {
176 | return spn, nil
177 | }
178 |
179 | ccache, err := credentials.LoadCCache(t.ccache)
180 | if err != nil {
181 | return spn, nil //nolint:nilerr
182 | }
183 |
184 | _, ok := ccache.GetEntry(types.NewPrincipalName(nametype.KRB_NT_SRV_INST, spn))
185 | if ok {
186 | return spn, nil
187 | }
188 |
189 | // change SPN to generic host SPN if a ticket exists ONLY for the generic host SPN
190 | _, ok = ccache.GetEntry(types.NewPrincipalName(nametype.KRB_NT_SRV_INST, "host/"+hostname))
191 | if ok {
192 | return "host/" + hostname, nil
193 | }
194 |
195 | return spn, nil
196 | }
197 |
--------------------------------------------------------------------------------
/target_test.go:
--------------------------------------------------------------------------------
1 | package adauth_test
2 |
3 | import (
4 | "context"
5 | "net"
6 | "strconv"
7 | "testing"
8 |
9 | "github.com/RedTeamPentesting/adauth"
10 | )
11 |
12 | func TestNewTarget(t *testing.T) {
13 | targetIP := net.ParseIP("10.0.0.1")
14 | targetPort := "1234"
15 | targetHostname := "computer.tld"
16 | resolver := &testResolver{
17 | HostToAddr: map[string][]net.IP{
18 | targetHostname: {targetIP},
19 | },
20 | AddrToHost: map[string][]string{
21 | targetIP.String(): {targetHostname},
22 | },
23 | }
24 |
25 | t.Run("hostname_from_ip", func(t *testing.T) {
26 | target := adauth.NewTarget("", targetIP.String())
27 | target.Resolver = resolver
28 |
29 | if target.Address() != targetIP.String() {
30 | t.Errorf("target address is %q instead of %q", target.Address(), targetIP.String())
31 | }
32 |
33 | hostname, err := target.Hostname(context.Background())
34 | if err != nil {
35 | t.Errorf("get hostname: %v", err)
36 | }
37 |
38 | if hostname != targetHostname {
39 | t.Errorf("hostname is %q instead of %q", hostname, targetHostname)
40 | }
41 | })
42 |
43 | t.Run("ip_from_hostname", func(t *testing.T) {
44 | target := adauth.NewTarget("", targetHostname)
45 | target.Resolver = resolver
46 |
47 | if target.Address() != targetHostname {
48 | t.Errorf("target address is %q instead of %q", target.Address(), targetIP.String())
49 | }
50 |
51 | ip, err := target.IP(context.Background())
52 | if err != nil {
53 | t.Errorf("get hostname: %v", err)
54 | }
55 |
56 | if ip.String() != targetIP.String() {
57 | t.Errorf("hostname is %q instead of %q", ip.String(), targetIP.String())
58 | }
59 | })
60 |
61 | t.Run("hostname_and_port", func(t *testing.T) {
62 | target := adauth.NewTarget("", net.JoinHostPort(targetHostname, targetPort))
63 | target.Resolver = resolver
64 |
65 | if target.Port != targetPort {
66 | t.Errorf("target port is %q instead of %q", target.Port, targetPort)
67 | }
68 |
69 | _, port, err := net.SplitHostPort(target.Address())
70 | if err != nil {
71 | t.Fatalf("split target.Address(): %v", err)
72 | }
73 |
74 | if port != targetPort {
75 | t.Errorf("target.Address() contains port %q instead of %q", port, targetPort)
76 | }
77 | })
78 | t.Run("ip_and_port", func(t *testing.T) {
79 | target := adauth.NewTarget("", net.JoinHostPort(targetIP.String(), targetPort))
80 | target.Resolver = resolver
81 |
82 | if target.Port != targetPort {
83 | t.Errorf("target port is %q instead of %q", target.Port, targetPort)
84 | }
85 |
86 | _, port, err := net.SplitHostPort(target.Address())
87 | if err != nil {
88 | t.Fatalf("split target.Address(): %v", err)
89 | }
90 |
91 | if port != targetPort {
92 | t.Errorf("target.Address() contains port %q instead of %q", port, targetPort)
93 | }
94 | })
95 | }
96 |
97 | func TestTargetAddressWithAndWithoutPort(t *testing.T) {
98 | targetHostname := "computer.tld"
99 |
100 | t.Run("with_port", func(t *testing.T) {
101 | target := adauth.NewTarget("ldap", targetHostname+":389")
102 |
103 | if target.Address() != targetHostname+":389" {
104 | t.Errorf("target.Address() is %q instead of %q", target.Address(), targetHostname)
105 | }
106 |
107 | if target.AddressWithoutPort() != targetHostname {
108 | t.Errorf("target.AddressWithoutPort() is %q instead of %q", target.AddressWithoutPort(), targetHostname)
109 | }
110 | })
111 | t.Run("without_port", func(t *testing.T) {
112 | target := adauth.NewTarget("ldap", targetHostname)
113 |
114 | if target.Address() != targetHostname {
115 | t.Errorf("target.Address() is %q instead of %q", target.Address(), targetHostname)
116 | }
117 |
118 | if target.AddressWithoutPort() != targetHostname {
119 | t.Errorf("target.AddressWithoutPort() is %q instead of %q", target.AddressWithoutPort(), targetHostname)
120 | }
121 | })
122 | }
123 |
124 | func TestNewTargetSPN(t *testing.T) {
125 | hostname := "computer.tld"
126 |
127 | testCases := []struct {
128 | InputProtocol string
129 | SPNProtocol string
130 | }{
131 | {
132 | InputProtocol: "",
133 | SPNProtocol: "host",
134 | },
135 | {
136 | InputProtocol: "ldaps",
137 | SPNProtocol: "ldap",
138 | },
139 | {
140 | InputProtocol: "smb",
141 | SPNProtocol: "cifs",
142 | },
143 | {
144 | InputProtocol: "foo",
145 | SPNProtocol: "foo",
146 | },
147 | {
148 | InputProtocol: "http",
149 | SPNProtocol: "http",
150 | },
151 |
152 | {
153 | InputProtocol: "https",
154 | SPNProtocol: "http",
155 | },
156 | }
157 |
158 | for i, testCase := range testCases {
159 | t.Run(strconv.Itoa(i), func(t *testing.T) {
160 | spn, err := adauth.NewTarget(testCase.InputProtocol, hostname).SPN(context.Background())
161 | if err != nil {
162 | t.Fatalf("SPN: %v", err)
163 | }
164 |
165 | expectedSPN := testCase.SPNProtocol + `/` + hostname
166 |
167 | if spn != expectedSPN {
168 | t.Fatalf("SPN for %q is %q instead of %q", testCase.InputProtocol, spn, expectedSPN)
169 | }
170 | })
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/testdata/empty.ccache:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedTeamPentesting/adauth/3a2d02c432b7af302263e6ec7f18c5c7b4bce55b/testdata/empty.ccache
--------------------------------------------------------------------------------
/testdata/someuser@domain.tld.pfx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RedTeamPentesting/adauth/3a2d02c432b7af302263e6ec7f18c5c7b4bce55b/testdata/someuser@domain.tld.pfx
--------------------------------------------------------------------------------
/workspace.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | }
6 | ],
7 | "settings": {
8 | "go.useLanguageServer": true,
9 | "go.lintTool": "golangci-lint",
10 | "gopls": {
11 | "gofumpt": true,
12 | },
13 | "go.formatTool": "gofumpt",
14 | "[go]": {
15 | "editor.formatOnSave": true,
16 | "editor.codeActionsOnSave": {
17 | "source.organizeImports": "explicit",
18 | },
19 | },
20 | "[go.mod]": {
21 | "editor.formatOnSave": true,
22 | "editor.codeActionsOnSave": {
23 | "source.organizeImports": "explicit",
24 | },
25 | }
26 | },
27 | "extensions": {
28 | "recommendations": [
29 | "golang.go"
30 | ]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------