├── .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 | Release 6 | Go Doc 7 | GitHub Action: Check 8 | Software License 9 | Go Report Card 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 | --------------------------------------------------------------------------------