From df6496cfa4a84d6cc9606410fe93219e1d017578 Mon Sep 17 00:00:00 2001 From: Erik Geiser Date: Mon, 5 May 2025 15:57:51 +0200 Subject: [PATCH] Remove keytab because KVNO may not match --- compat/compat.go | 26 ---------- credentials.go | 104 ------------------------------------- credentials_test.go | 124 -------------------------------------------- ldapauth/ldap.go | 53 ++++++++++++++++--- 4 files changed, 47 insertions(+), 260 deletions(-) diff --git a/compat/compat.go b/compat/compat.go index 2c28a47..10b624a 100644 --- a/compat/compat.go +++ b/compat/compat.go @@ -5,10 +5,8 @@ package compat import ( "github.com/jcmturner/gokrb5/v8/config" "github.com/jcmturner/gokrb5/v8/credentials" - "github.com/jcmturner/gokrb5/v8/keytab" gokrb5ForkConfig "github.com/oiweiwei/gokrb5.fork/v9/config" gokrb5ForkCredentials "github.com/oiweiwei/gokrb5.fork/v9/credentials" - gokrb5ForkKeytab "github.com/oiweiwei/gokrb5.fork/v9/keytab" gokrb5ForkTypes "github.com/oiweiwei/gokrb5.fork/v9/types" "github.com/jcmturner/gokrb5/v8/types" @@ -75,27 +73,3 @@ func Gokrb5ForkV9Principal(realm string, principalName types.PrincipalName) gokr PrincipalName: gokrb5ForkTypes.PrincipalName(principalName), } } - -func Gokrb5ForkV9Keytab(keytab *keytab.Keytab) *gokrb5ForkKeytab.Keytab { - entries := make([]gokrb5ForkKeytab.Entry, 0, len(keytab.Entries)) - - for _, entry := range keytab.Entries { - entries = append(entries, gokrb5ForkKeytab.Entry{ - Principal: gokrb5ForkKeytab.Principal{ - NumComponents: entry.Principal.NumComponents, - Realm: entry.Principal.Realm, - Components: entry.Principal.Components, - NameType: entry.Principal.NameType, - }, - Timestamp: entry.Timestamp, - KVNO8: entry.KVNO8, - Key: gokrb5ForkTypes.EncryptionKey(entry.Key), - KVNO: entry.KVNO, - }) - } - - return &gokrb5ForkKeytab.Keytab{ - Version: 2, - Entries: entries, - } -} diff --git a/credentials.go b/credentials.go index 676a4ab..0a2a246 100644 --- a/credentials.go +++ b/credentials.go @@ -4,7 +4,6 @@ import ( "context" "crypto/rsa" "crypto/x509" - "encoding/hex" "fmt" "net" "os" @@ -13,9 +12,7 @@ import ( "github.com/RedTeamPentesting/adauth/othername" "github.com/jcmturner/gokrb5/v8/config" - "github.com/jcmturner/gokrb5/v8/credentials" "github.com/jcmturner/gokrb5/v8/iana/etypeID" - "github.com/jcmturner/gokrb5/v8/keytab" "software.sslmate.com/src/go-pkcs12" ) @@ -156,30 +153,6 @@ func (c *Credential) mustUseKerberos() bool { return c.Password == "" && c.NTHash == "" && (c.CCache != "" || c.AESKey != "") } -// Keytab returns the Kerberos keytab containing the AES key and/or NT hash if -// they were supplied. If a password is supplied, the keys/hashes are not -// derived and the keytab will be empty. For compatibility with other Kerberos -// libraries, see the `compat` package. -func (c *Credential) Keytab() (*keytab.Keytab, error) { - kt := newKeytab() - - if c.AESKey != "" { - err := addKeyToKeytab(kt, c.Username, c.Domain, c.AESKey, true, 1) - if err != nil { - return nil, fmt.Errorf("add AES key: %w", err) - } - } - - if c.NTHash != "" { - err := addKeyToKeytab(kt, c.Username, c.Domain, c.NTHash, false, 1) - if err != nil { - return nil, fmt.Errorf("add RC4 key: %w", err) - } - } - - return kt, nil -} - // KerberosConfig returns the Kerberos configuration for the credential's // domain. For compatibility with other Kerberos libraries, see the `compat` // package. @@ -272,80 +245,3 @@ func splitUserIntoDomainAndUsername(user string) (domain string, username string return "", user } } - -func newKeytab() *keytab.Keytab { - kt := &keytab.Keytab{} - - err := kt.Unmarshal([]byte{ - // header. - 0x05, // first-byte - 0x02, // version - 0x00, 0x00, 0x00, 0x00, // entry-length - }) - if err != nil { - panic(err.Error()) - } - - return kt -} - -func addKeyToKeytab(kt *keytab.Keytab, username string, domain string, key string, aes bool, kvno uint32) error { - keyBytes, err := hex.DecodeString(key) - if err != nil { - return fmt.Errorf("decode AES key: %w", err) - } - - var keyType int32 - - switch len(keyBytes) { - case 32: - keyType = etypeID.AES256_CTS_HMAC_SHA1_96 - case 16: - if aes { - keyType = etypeID.AES128_CTS_HMAC_SHA1_96 - } else { - keyType = etypeID.RC4_HMAC - } - default: - return fmt.Errorf("invalid AES128/AES256 key") - } - - tmp := &keytab.Keytab{} - - err = tmp.Unmarshal([]byte{ - // header - 0x05, // first-byte - 0x02, // version - 0x00, 0x00, 0x00, 0x11, // entry-length - // principal - 0x00, 0x00, // num components - 0x00, 0x00, // realm length - 0x00, 0x00, 0x00, 0x00, // name type - // key - 0x00, 0x00, 0x00, 0x00, // timestamp - 0x00, // kvno8 - 0x00, 0x00, // key type - 0x00, 0x00, // key length - }) - if err != nil { - return fmt.Errorf("invalid dummy data: %w", err) - } - - e := tmp.Entries[0] - - krbCreds := credentials.New(username, domain) - e.Principal.NumComponents = int16(len(krbCreds.CName().NameString)) - e.Principal.Components = krbCreds.CName().NameString - e.Principal.Realm = strings.ToUpper(krbCreds.Realm()) - e.Principal.NameType = krbCreds.CName().NameType - - e.Timestamp = time.Now() - e.KVNO8 = 0 - e.Key.KeyType = keyType - e.Key.KeyValue = keyBytes - e.KVNO = kvno - - kt.Entries = append(kt.Entries, e) - - return nil -} diff --git a/credentials_test.go b/credentials_test.go index 74a58bf..2f290bb 100644 --- a/credentials_test.go +++ b/credentials_test.go @@ -2,136 +2,12 @@ package adauth_test import ( "context" - "encoding/hex" "net" - "strconv" - "strings" "testing" "github.com/RedTeamPentesting/adauth" - "github.com/jcmturner/gokrb5/v8/iana/etypeID" - "github.com/jcmturner/gokrb5/v8/iana/nametype" - "github.com/jcmturner/gokrb5/v8/types" ) -func TestKeytab(t *testing.T) { - expectedNTHash := hex.EncodeToString(make([]byte, 16)) - expectedAES256Key := hex.EncodeToString(make([]byte, 32)) - expectedAES128Key := hex.EncodeToString(make([]byte, 16)) - principal := types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, testUser) - - testCases := []struct { - Cred adauth.Credential - ShouldHaveRC4Key bool - ShouldHaveAES128Key bool - ShouldHaveAES256Key bool - }{ - { - Cred: adauth.Credential{}, - ShouldHaveRC4Key: false, - ShouldHaveAES128Key: false, - ShouldHaveAES256Key: false, - }, - { - Cred: adauth.Credential{ - NTHash: expectedNTHash, - }, - ShouldHaveRC4Key: true, - ShouldHaveAES128Key: false, - ShouldHaveAES256Key: false, - }, - { - Cred: adauth.Credential{ - NTHash: expectedNTHash, - AESKey: expectedAES128Key, - }, - ShouldHaveRC4Key: true, - ShouldHaveAES128Key: true, - ShouldHaveAES256Key: false, - }, - { - Cred: adauth.Credential{ - NTHash: expectedNTHash, - AESKey: expectedAES256Key, - }, - ShouldHaveRC4Key: true, - ShouldHaveAES128Key: false, - ShouldHaveAES256Key: true, - }, - { - Cred: adauth.Credential{ - AESKey: expectedAES128Key, - }, - ShouldHaveRC4Key: false, - ShouldHaveAES128Key: true, - ShouldHaveAES256Key: false, - }, - { - Cred: adauth.Credential{ - AESKey: expectedAES256Key, - }, - ShouldHaveRC4Key: false, - ShouldHaveAES128Key: false, - ShouldHaveAES256Key: true, - }, - } - - for i, testCase := range testCases { - testCase := testCase - - t.Run(strconv.Itoa(i), func(t *testing.T) { - testCase.Cred.Username = testUser - testCase.Cred.Domain = testDomain - - keyTab, err := testCase.Cred.Keytab() - if err != nil { - t.Fatalf("create keytab: %v", err) - } - - rc4Key, _, rc4Err := keyTab.GetEncryptionKey(principal, strings.ToUpper(testDomain), 0, etypeID.RC4_HMAC) - - switch { - case testCase.ShouldHaveRC4Key && rc4Err != nil: - t.Errorf("expected RC4 key but got error: %v", rc4Err) - case testCase.ShouldHaveRC4Key && len(rc4Key.KeyValue) != 16: - t.Errorf("RC4 key has %d bytes instead of %d", len(rc4Key.KeyValue), 16) - case testCase.ShouldHaveRC4Key && rc4Key.KeyType != etypeID.RC4_HMAC: - t.Errorf("RC4 key type is %d instead of %d", rc4Key.KeyType, etypeID.RC4_HMAC) - case !testCase.ShouldHaveRC4Key && (rc4Err == nil || len(rc4Key.KeyValue) > 0): - t.Errorf("RC4 key should not exist") - } - - aes128Key, _, aes128Err := keyTab.GetEncryptionKey( - principal, strings.ToUpper(testDomain), 0, etypeID.AES128_CTS_HMAC_SHA1_96) - - switch { - case testCase.ShouldHaveAES128Key && aes128Err != nil: - t.Errorf("expected AES128 key but got error: %v:\n%#v\n", aes128Err, keyTab.Entries) - case testCase.ShouldHaveAES128Key && len(aes128Key.KeyValue) != 16: - t.Errorf("AES128 key has %d bytes instead of %d", len(aes128Key.KeyValue), 16) - case testCase.ShouldHaveAES128Key && aes128Key.KeyType != etypeID.AES128_CTS_HMAC_SHA1_96: - t.Errorf("AES128 key type is %d instead of %d", aes128Key.KeyType, etypeID.AES128_CTS_HMAC_SHA1_96) - case !testCase.ShouldHaveAES128Key && (aes128Err == nil || len(aes128Key.KeyValue) > 0): - t.Errorf("AES128 key should not exist") - } - - aes256Key, _, aes256Err := keyTab.GetEncryptionKey( - principal, strings.ToUpper(testDomain), 0, etypeID.AES256_CTS_HMAC_SHA1_96) - - switch { - case testCase.ShouldHaveAES256Key && aes256Err != nil: - t.Errorf("expected AES256 key but got error: %v", aes256Err) - case testCase.ShouldHaveAES256Key && len(aes256Key.KeyValue) != 32: - t.Errorf("AES256 key has %d bytes instead of %d", len(aes256Key.KeyValue), 32) - case testCase.ShouldHaveAES256Key && aes256Key.KeyType != etypeID.AES256_CTS_HMAC_SHA1_96: - t.Errorf("AES256 key type is %d instead of %d", aes256Key.KeyType, etypeID.AES256_CTS_HMAC_SHA1_96) - case !testCase.ShouldHaveAES256Key && (aes256Err == nil || len(aes256Key.KeyValue) > 0): - t.Errorf("AES256 key should not exist") - } - }) - } -} - func TestSetDC(t *testing.T) { creds := adauth.Credential{ Username: testUser, diff --git a/ldapauth/ldap.go b/ldapauth/ldap.go index 3a2fbfb..bd83e33 100644 --- a/ldapauth/ldap.go +++ b/ldapauth/ldap.go @@ -11,6 +11,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/binary" + "encoding/hex" "encoding/pem" "fmt" "net" @@ -26,7 +27,9 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/oiweiwei/gokrb5.fork/v9/client" + "github.com/oiweiwei/gokrb5.fork/v9/iana/etypeID" "github.com/oiweiwei/gokrb5.fork/v9/iana/flags" + "github.com/oiweiwei/gokrb5.fork/v9/types" "github.com/spf13/pflag" ) @@ -353,19 +356,57 @@ func kerberosClient( } authClient.BindCertificate = cert - case creds.NTHash != "" || creds.AESKey != "": - opts.Debug("authenticating using GSSAPI bind (key)") + case creds.NTHash != "": + opts.Debug("authenticating using GSSAPI bind (NT hash)") - keyTab, err := creds.Keytab() + ntHash, err := hex.DecodeString(creds.NTHash) if err != nil { - return nil, fmt.Errorf("create keytab: %w", err) + return nil, fmt.Errorf("decode NT hash: %w", err) } authClient = &gssapiClient{ - Client: client.NewWithKeytab( + Client: client.NewWithEncryptionKey( creds.Username, strings.ToUpper(creds.Domain), - compat.Gokrb5ForkV9Keytab(keyTab), + types.EncryptionKey{ + KeyType: etypeID.RC4_HMAC, + KeyValue: ntHash, + }, + compat.Gokrb5ForkV9KerberosConfig(krbConf), + client.DisablePAFXFAST(true), + client.Dialer(opts.KerberosDialer), + ), + BindCertificate: cert, + } + + authClient.BindCertificate = cert + case creds.AESKey != "": + opts.Debug("authenticating using GSSAPI bind (AES key)") + + aesKey, err := hex.DecodeString(creds.AESKey) + if err != nil { + return nil, fmt.Errorf("decode AES key: %w", err) + } + + var keyType int32 + + switch len(aesKey) { + case 32: + keyType = etypeID.AES256_CTS_HMAC_SHA1_96 + case 16: + keyType = etypeID.AES128_CTS_HMAC_SHA1_96 + default: + return nil, fmt.Errorf("invalid AES128/AES256 key: key size is %d bytes", len(aesKey)) + } + + authClient = &gssapiClient{ + Client: client.NewWithEncryptionKey( + creds.Username, + strings.ToUpper(creds.Domain), + types.EncryptionKey{ + KeyType: keyType, + KeyValue: aesKey, + }, compat.Gokrb5ForkV9KerberosConfig(krbConf), client.DisablePAFXFAST(true), client.Dialer(opts.KerberosDialer),