package adauth import ( "bytes" "context" "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" "crypto/x509" "fmt" "net" "os" "strings" "time" "github.com/RedTeamPentesting/adauth/x509ext" "github.com/jcmturner/gokrb5/v8/config" "github.com/jcmturner/gokrb5/v8/iana/etypeID" "software.sslmate.com/src/go-pkcs12" ) // Credential represents Active Directory credentials. type Credential struct { // Username is the username without the domain. Username string // Password contains the users cleartext password if available. Password string // Domain holds the user's domain. Domain string // NTHash holds the user's NT hash or Kerberos RC4 key if available. NTHash string // AESKey holds the user's Kerberos AES128 or AES256 key if available. AESKey string // CCache contains the path to the user's CCache file. CCache string // ClientCert holds a client certificate for Kerberos or LDAP authentication if available. ClientCert *x509.Certificate // ClientCertKey holds the private key that corresponds to ClientCert. ClientCertKey any // CACerts holds CA certificates that were loaded alongside the ClientCert. CACerts []*x509.Certificate dc string // PasswordIsEmptyString is true when an empty Password field should not be // interpreted as a missing password but as a password that happens to be // empty. PasswordIsEmptyString bool // CCacheIsFromEnv indicates whether the CCache was set explicitly or // implicitly through an environment variable. CCacheIsFromEnv bool // Resolver can be used to set an alternative DNS resolver. If empty, // net.DefaultResolver is used. Resolver Resolver } // CredentialFromPFX creates a Credential structure for certificate-based // authentication based on a PFX file. func CredentialFromPFX( username string, domain string, pfxFile string, pfxPassword string, ) (*Credential, error) { pfxData, err := os.ReadFile(pfxFile) if err != nil { return nil, fmt.Errorf("read PFX: %w", err) } return CredentialFromPFXBytes(username, domain, pfxData, pfxPassword) } // CredentialFromPFX creates a Credential structure for certificate-based // authentication based on PFX data. func CredentialFromPFXBytes( username string, domain string, pfxData []byte, pfxPassword string, ) (*Credential, error) { cred := &Credential{ Username: username, Domain: domain, } key, cert, caCerts, err := DecodePFX(pfxData, pfxPassword) if err != nil { return nil, fmt.Errorf("decode PFX: %w", err) } rsaKey, ok := key.(*rsa.PrivateKey) if !ok { return nil, fmt.Errorf("PFX key is not an RSA private key but %T", key) } cred.ClientCert = cert cred.ClientCertKey = rsaKey cred.CACerts = caCerts user, domain, err := x509ext.UserAndDomainFromOtherNames(cert) if err == nil { if cred.Username == "" { cred.Username = user } if cred.Domain == "" { cred.Domain = domain } } return cred, nil } // UPN is the user principal name (username@domain). If the credential does not // contain a domain, only the username is returned. If username and domain are // empty, the UPN will be empty, too. func (c *Credential) UPN() string { switch { case c.Username == "" && c.Domain == "": return "" case c.Domain == "": return c.Username default: return c.Username + "@" + c.Domain } } // LogonName is the legacy logon name (domain\username). func (c *Credential) LogonName() string { return c.Domain + `\` + c.Username } // LogonNameWithUpperCaseDomain is like LogonName with the domain capitalized // for compatibility with the Kerberos library (DOMAIN\username). func (c *Credential) LogonNameWithUpperCaseDomain() string { return strings.ToUpper(c.Domain) + `\` + c.Username } // ImpacketLogonName is the Impacket-style logon name (domain/username). func (c *Credential) ImpacketLogonName() string { return c.Domain + "/" + c.Username } // SetDC configures a specific domain controller for this credential. func (c *Credential) SetDC(dc string) { c.dc = dc } // DC returns the domain controller of the credential's domain as a target. func (c *Credential) DC(ctx context.Context, protocol string) (*Target, error) { if c.dc != "" { return newTarget(protocol, c.dc, true, c.CCache, c.Resolver), nil } if c.Domain == "" { return nil, fmt.Errorf("domain unknown") } _, addrs, err := ensureResolver(c.Resolver, nil).LookupSRV(ctx, "kerberos", "tcp", c.Domain) if err != nil { return nil, fmt.Errorf("lookup %q service of domain %q: %w", "kerberos", c.Domain, err) } if len(addrs) == 0 { return nil, fmt.Errorf("no %q services were discovered for domain %q", "kerberos", c.Domain) } return newTarget(protocol, strings.TrimRight(addrs[0].Target, "."), true, c.CCache, c.Resolver), nil } func (c *Credential) mustUseKerberos() bool { return c.Password == "" && c.NTHash == "" && (c.CCache != "" || c.AESKey != "") } // KerberosConfig returns the Kerberos configuration for the credential's // domain. For compatibility with other Kerberos libraries, see the `compat` // package. func (c *Credential) KerberosConfig(ctx context.Context) (*config.Config, error) { dc, err := c.DC(ctx, "krbtgt") if err != nil { return nil, fmt.Errorf("find DC: %w", err) } krbConf := config.New() krbConf.LibDefaults.DefaultRealm = strings.ToUpper(c.Domain) krbConf.LibDefaults.AllowWeakCrypto = true krbConf.LibDefaults.DNSLookupRealm = false krbConf.LibDefaults.DNSLookupKDC = false krbConf.LibDefaults.TicketLifetime = time.Duration(24) * time.Hour krbConf.LibDefaults.RenewLifetime = time.Duration(24*7) * time.Hour krbConf.LibDefaults.Forwardable = true krbConf.LibDefaults.Proxiable = true krbConf.LibDefaults.RDNS = false krbConf.LibDefaults.UDPPreferenceLimit = 1 // Force use of tcp if c.NTHash != "" { // use RC4 for pre-auth but AES256 for ephemeral keys, otherwise we get // unsupported GSSAPI tokens during LDAP SASL handshake krbConf.LibDefaults.DefaultTGSEnctypeIDs = []int32{etypeID.AES256_CTS_HMAC_SHA1_96} krbConf.LibDefaults.DefaultTktEnctypeIDs = []int32{etypeID.RC4_HMAC} krbConf.LibDefaults.PermittedEnctypeIDs = []int32{etypeID.AES256_CTS_HMAC_SHA1_96} krbConf.LibDefaults.PreferredPreauthTypes = []int{int(etypeID.RC4_HMAC)} } else { krbConf.LibDefaults.DefaultTGSEnctypeIDs = []int32{ etypeID.AES256_CTS_HMAC_SHA1_96, etypeID.AES128_CTS_HMAC_SHA1_96, etypeID.RC4_HMAC, } krbConf.LibDefaults.DefaultTktEnctypeIDs = []int32{ etypeID.AES256_CTS_HMAC_SHA1_96, etypeID.AES128_CTS_HMAC_SHA1_96, etypeID.RC4_HMAC, } krbConf.LibDefaults.PermittedEnctypeIDs = []int32{ etypeID.AES256_CTS_HMAC_SHA1_96, etypeID.AES128_CTS_HMAC_SHA1_96, etypeID.RC4_HMAC, } krbConf.LibDefaults.PreferredPreauthTypes = []int{ int(etypeID.AES256_CTS_HMAC_SHA1_96), int(etypeID.AES128_CTS_HMAC_SHA1_96), int(etypeID.RC4_HMAC), } } krbConf.Realms = []config.Realm{ { Realm: strings.ToUpper(c.Domain), DefaultDomain: strings.ToUpper(c.Domain), AdminServer: []string{dc.AddressWithoutPort()}, KDC: []string{net.JoinHostPort(dc.AddressWithoutPort(), "88")}, KPasswdServer: []string{net.JoinHostPort(dc.AddressWithoutPort(), "464")}, MasterKDC: []string{dc.AddressWithoutPort()}, }, { Realm: c.Domain, DefaultDomain: c.Domain, AdminServer: []string{dc.AddressWithoutPort()}, KDC: []string{net.JoinHostPort(dc.AddressWithoutPort(), "88")}, KPasswdServer: []string{net.JoinHostPort(dc.AddressWithoutPort(), "464")}, MasterKDC: []string{dc.AddressWithoutPort()}, }, } krbConf.DomainRealm = map[string]string{ "." + c.Domain: strings.ToUpper(c.Domain), c.Domain: strings.ToUpper(c.Domain), } return krbConf, nil } func splitUserIntoDomainAndUsername(user string) (domain string, username string) { switch { case strings.Contains(user, "@"): parts := strings.Split(user, "@") if len(parts) == 2 { return parts[1], parts[0] } return "", user case strings.Contains(user, `\`): parts := strings.Split(user, `\`) if len(parts) == 2 { return parts[0], parts[1] } return "", user case strings.Contains(user, "/"): parts := strings.Split(user, "/") if len(parts) == 2 { return parts[0], parts[1] } return "", user default: return "", user } } // DecodePFX loads the private key, certificate and certificate chain from PFX // bytes that may or may not be protected by a password. func DecodePFX(pfxData []byte, password string) (privateKey any, cert *x509.Certificate, chain []*x509.Certificate, err error) { // In some PFXs, especially those create by Microsoft tools, the cert and // chain order is reversed such that pkcs12.DecodeChain returns the CA cert // as "cert" and the leaf certificate in the chain (see // https://github.com/SSLMate/go-pkcs12/issues/54). Our strategy is that we // swap certifiates such that "cert" is the certificate that belongs to the // private key and "chain" contains all other certificates. privateKey, cert, chain, err = pkcs12.DecodeChain(pfxData, password) if err != nil || certMatchesKey(privateKey, cert) { return privateKey, cert, chain, err } for i := range chain { if !certMatchesKey(privateKey, chain[i]) { continue } newCert := chain[i] chain[i] = cert return privateKey, newCert, chain, nil } return privateKey, cert, chain, fmt.Errorf("private key does not match any of the %d certificates in PFX", len(chain)+1) } func certMatchesKey(key any, cert *x509.Certificate) bool { switch pub := cert.PublicKey.(type) { case *rsa.PublicKey: priv, ok := key.(*rsa.PrivateKey) if !ok { return false } if pub.N.Cmp(priv.N) != 0 { return false } return true case *ecdsa.PublicKey: priv, ok := key.(*ecdsa.PrivateKey) if !ok { return false } if pub.X.Cmp(priv.X) != 0 || pub.Y.Cmp(priv.Y) != 0 { return false } return true case ed25519.PublicKey: priv, ok := key.(ed25519.PrivateKey) if !ok { return false } privPublicKey, ok := priv.Public().(ed25519.PublicKey) if !ok { return false } if !bytes.Equal(privPublicKey, pub) { return false } return true default: return false } }