diff --git a/packages/go/analysis/ad/adcs.go b/packages/go/analysis/ad/adcs.go index e62f1c79f..53d34987f 100644 --- a/packages/go/analysis/ad/adcs.go +++ b/packages/go/analysis/ad/adcs.go @@ -20,7 +20,6 @@ import ( "context" "errors" "fmt" - "github.com/specterops/bloodhound/analysis" "github.com/specterops/bloodhound/analysis/impact" "github.com/specterops/bloodhound/dawgs/cardinality" @@ -30,7 +29,6 @@ import ( "github.com/specterops/bloodhound/dawgs/util/channels" "github.com/specterops/bloodhound/graphschema/ad" "github.com/specterops/bloodhound/log" - "github.com/specterops/bloodhound/slices" ) var ( @@ -154,228 +152,6 @@ func validatePublishedCertTemplateForEsc1(properties PublishedCertTemplateValida } } -func PostEnrollOnBehalfOf(certTemplates []*graph.Node, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob]) error { - versionOneTemplates := make([]*graph.Node, 0) - versionTwoTemplates := make([]*graph.Node, 0) - - for _, node := range certTemplates { - if version, err := node.Properties.Get(ad.SchemaVersion.String()).Float64(); err != nil { - log.Errorf("Error getting schema version for cert template %d: %v", node.ID, err) - } else { - if version == 1 { - versionOneTemplates = append(versionOneTemplates, node) - } else if version >= 2 { - versionTwoTemplates = append(versionTwoTemplates, node) - } else { - log.Warnf("Got cert template %d with an invalid version %d", node.ID, version) - } - } - } - - operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { - if results, err := EnrollOnBehalfOfVersionTwo(tx, versionTwoTemplates, certTemplates); err != nil { - return err - } else { - for _, result := range results { - if !channels.Submit(ctx, outC, result) { - return nil - } - } - - return nil - } - }) - - operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { - if results, err := EnrollOnBehalfOfVersionOne(tx, versionOneTemplates, certTemplates); err != nil { - return err - } else { - for _, result := range results { - if !channels.Submit(ctx, outC, result) { - return nil - } - } - - return nil - } - }) - - operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { - if results, err := EnrollOnBehalfOfSelfControl(tx, versionOneTemplates); err != nil { - return err - } else { - for _, result := range results { - if !channels.Submit(ctx, outC, result) { - return nil - } - } - - return nil - } - }) - - return nil -} - -func EnrollOnBehalfOfVersionTwo(tx graph.Transaction, versionTwoCertTemplates, allCertTemplates []*graph.Node) ([]analysis.CreatePostRelationshipJob, error) { - results := make([]analysis.CreatePostRelationshipJob, 0) - for _, certTemplateOne := range allCertTemplates { - if hasBadEku, err := certTemplateHasEku(certTemplateOne, EkuAnyPurpose); err != nil { - log.Errorf("error getting ekus for cert template %d: %w", certTemplateOne.ID, err) - } else if hasBadEku { - continue - } else if hasEku, err := certTemplateHasEku(certTemplateOne, EkuCertRequestAgent); err != nil { - log.Errorf("error getting ekus for cert template %d: %w", certTemplateOne.ID, err) - } else if !hasEku { - continue - } else if domainNode, err := getDomainForCertTemplate(tx, certTemplateOne); err != nil { - log.Errorf("error getting domain node for cert template %d: %w", certTemplateOne.ID, err) - } else if isLinked, err := DoesCertTemplateLinkToDomain(tx, certTemplateOne, domainNode); err != nil { - log.Errorf("error fetching paths from cert template %d to domain: %w", certTemplateOne.ID, err) - } else if !isLinked { - continue - } else { - for _, certTemplateTwo := range versionTwoCertTemplates { - if certTemplateOne.ID == certTemplateTwo.ID { - continue - } else if authorizedSignatures, err := certTemplateTwo.Properties.Get(ad.AuthorizedSignatures.String()).Float64(); err != nil { - log.Errorf("Error getting authorized signatures for cert template %d: %w", certTemplateTwo.ID, err) - } else if authorizedSignatures < 1 { - continue - } else if applicationPolicies, err := certTemplateTwo.Properties.Get(ad.ApplicationPolicies.String()).StringSlice(); err != nil { - log.Errorf("Error getting application policies for cert template %d: %w", certTemplateTwo.ID, err) - } else if !slices.Contains(applicationPolicies, EkuCertRequestAgent) { - continue - } else if isLinked, err := DoesCertTemplateLinkToDomain(tx, certTemplateTwo, domainNode); err != nil { - log.Errorf("error fetch paths from cert template %d to domain: %w", certTemplateTwo.ID, err) - } else if !isLinked { - continue - } else { - results = append(results, analysis.CreatePostRelationshipJob{ - FromID: certTemplateOne.ID, - ToID: certTemplateTwo.ID, - Kind: ad.EnrollOnBehalfOf, - }) - } - } - } - } - - return results, nil -} - -func EnrollOnBehalfOfVersionOne(tx graph.Transaction, versionOneCertTemplates []*graph.Node, allCertTemplates []*graph.Node) ([]analysis.CreatePostRelationshipJob, error) { - results := make([]analysis.CreatePostRelationshipJob, 0) - - for _, certTemplateOne := range allCertTemplates { - //prefilter as much as we can first - if slices.Contains(versionOneCertTemplates, certTemplateOne) { - continue - } else if hasEku, err := certTemplateHasEkuOrAll(certTemplateOne, EkuCertRequestAgent, EkuAnyPurpose); err != nil { - log.Errorf("Error checking ekus for certtemplate %d: %w", certTemplateOne.ID, err) - } else if !hasEku { - continue - } else if domainNode, err := getDomainForCertTemplate(tx, certTemplateOne); err != nil { - log.Errorf("Error getting domain node for certtemplate %d: %w", certTemplateOne.ID, err) - } else if hasPath, err := DoesCertTemplateLinkToDomain(tx, certTemplateOne, domainNode); err != nil { - log.Errorf("Error fetching paths from certtemplate %d to domain: %w", certTemplateOne.ID, err) - } else if !hasPath { - continue - } else { - for _, certTemplateTwo := range versionOneCertTemplates { - if certTemplateTwo.ID == certTemplateOne.ID { - continue - } else if hasPath, err := DoesCertTemplateLinkToDomain(tx, certTemplateTwo, domainNode); err != nil { - log.Errorf("Error getting domain node for certtemplate %d: %w", certTemplateTwo.ID, err) - } else if !hasPath { - continue - } else { - results = append(results, analysis.CreatePostRelationshipJob{ - FromID: certTemplateOne.ID, - ToID: certTemplateTwo.ID, - Kind: ad.EnrollOnBehalfOf, - }) - } - } - } - } - - return results, nil -} - -func getDomainForCertTemplate(tx graph.Transaction, certTemplate *graph.Node) (*graph.Node, error) { - if domainSid, err := certTemplate.Properties.Get(ad.DomainSID.String()).String(); err != nil { - return &graph.Node{}, err - } else if domainNode, err := analysis.FetchNodeByObjectID(tx, domainSid); err != nil { - return &graph.Node{}, err - } else { - return domainNode, nil - } -} - -func EnrollOnBehalfOfSelfControl(tx graph.Transaction, versionOneCertTemplates []*graph.Node) ([]analysis.CreatePostRelationshipJob, error) { - results := make([]analysis.CreatePostRelationshipJob, 0) - for _, certTemplate := range versionOneCertTemplates { - if hasEku, err := certTemplateHasEkuOrAll(certTemplate, EkuAnyPurpose); err != nil { - log.Errorf("Error checking ekus for certtemplate %d: %w", certTemplate.ID, err) - } else if !hasEku { - continue - } else if subjectRequireUpn, err := certTemplate.Properties.Get(ad.SubjectAltRequireUPN.String()).Bool(); err != nil { - log.Errorf("Error getting subjectAltRequireUPN for certtemplate %d: %w", certTemplate.ID, err) - } else if !subjectRequireUpn { - continue - } else if domainNode, err := getDomainForCertTemplate(tx, certTemplate); err != nil { - log.Errorf("Error getting domain for certtemplate %d: %w", certTemplate.ID, err) - } else if doesLink, err := DoesCertTemplateLinkToDomain(tx, certTemplate, domainNode); err != nil { - log.Errorf("Error fetching paths from certtemplate %d to domain: %w", certTemplate.ID, err) - } else if !doesLink { - continue - } else { - results = append(results, analysis.CreatePostRelationshipJob{ - FromID: certTemplate.ID, - ToID: certTemplate.ID, - Kind: ad.EnrollOnBehalfOf, - }) - } - } - - return results, nil -} - -func certTemplateHasEkuOrAll(certTemplate *graph.Node, targetEkus ...string) (bool, error) { - if ekus, err := certTemplate.Properties.Get(ad.EKUs.String()).StringSlice(); err != nil { - return false, err - } else if len(ekus) == 0 { - return true, nil - } else { - for _, eku := range ekus { - for _, targetEku := range targetEkus { - if eku == targetEku { - return true, nil - } - } - } - - return false, nil - } -} - -func certTemplateHasEku(certTemplate *graph.Node, targetEkus ...string) (bool, error) { - if ekus, err := certTemplate.Properties.Get(ad.EKUs.String()).StringSlice(); err != nil { - return false, err - } else { - for _, eku := range ekus { - for _, targetEku := range targetEkus { - if eku == targetEku { - return true, nil - } - } - } - - return false, nil - } -} - func PostADCS(ctx context.Context, db graph.Database, groupExpansions impact.PathAggregator, adcsEnabled bool) (*analysis.AtomicPostProcessingStats, error) { if adcsEnabled { operation := analysis.NewPostRelationshipOperation(ctx, db, "ADCS Post Processing") @@ -429,20 +205,24 @@ func PostADCS(ctx context.Context, db graph.Database, groupExpansions impact.Pat } } +// postADCSPreProcessStep1 processes the edges that are not dependent on any other post-processed edges func postADCSPreProcessStep1(ctx context.Context, db graph.Database, enterpriseCertAuthorities, rootCertAuthorities []*graph.Node) (*analysis.AtomicPostProcessingStats, error) { operation := analysis.NewPostRelationshipOperation(ctx, db, "ADCS Post Processing Step 1") if err := PostTrustedForNTAuth(ctx, db, operation); err != nil { return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed post processing for %s: %w", ad.TrustedForNTAuth.String(), err) - } else if err := PostIssuedSignedBy(ctx, db, operation, enterpriseCertAuthorities, rootCertAuthorities); err != nil { + } else if err = PostIssuedSignedBy(ctx, db, operation, enterpriseCertAuthorities, rootCertAuthorities); err != nil { return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed post processing for %s: %w", ad.IssuedSignedBy.String(), err) - } else if err := PostEnterpriseCAFor(ctx, db, operation, enterpriseCertAuthorities); err != nil { + } else if err = PostEnterpriseCAFor(ctx, db, operation, enterpriseCertAuthorities); err != nil { return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed post processing for %s: %w", ad.EnterpriseCAFor.String(), err) + } else if err = PostCanAbuseUPNCertMapping(ctx, db, operation, enterpriseCertAuthorities); err != nil { + return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed post processing for %s: %w", ad.CanAbuseUPNCertMapping.String(), err) } else { return &operation.Stats, operation.Done() } } +// postADCSPreProcessStep2 Processes the edges that are dependent on those processed in postADCSPreProcessStep1 func postADCSPreProcessStep2(ctx context.Context, db graph.Database, certTemplates []*graph.Node) (*analysis.AtomicPostProcessingStats, error) { operation := analysis.NewPostRelationshipOperation(ctx, db, "ADCS Post Processing Step 2") @@ -469,144 +249,3 @@ func PostGoldenCert(ctx context.Context, tx graph.Transaction, outC chan<- analy } return nil } - -func PostTrustedForNTAuth(ctx context.Context, db graph.Database, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob]) error { - - if ntAuthStoreNodes, err := FetchNodesByKind(ctx, db, ad.NTAuthStore); err != nil { - return err - } else { - for _, node := range ntAuthStoreNodes { - innerNode := node - - operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { - if thumbprints, err := innerNode.Properties.Get(ad.CertThumbprints.String()).StringSlice(); err != nil { - return err - } else { - for _, thumbprint := range thumbprints { - if thumbprint != "" { - if sourceNodeIDs, err := findNodesByCertThumbprint(thumbprint, tx, ad.EnterpriseCA); err != nil { - return err - } else { - for _, sourceNodeID := range sourceNodeIDs { - if !channels.Submit(ctx, outC, analysis.CreatePostRelationshipJob{ - FromID: sourceNodeID, - ToID: innerNode.ID, - Kind: ad.TrustedForNTAuth, - }) { - return nil - } - } - } - } - } - } - return nil - }) - } - } - - return nil -} - -func PostIssuedSignedBy(ctx context.Context, db graph.Database, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob], enterpriseCertAuthorities []*graph.Node, rootCertAuthorities []*graph.Node) error { - - operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { - for _, node := range enterpriseCertAuthorities { - if postRels, err := processCertChainParent(node, tx); err != nil && !errors.Is(err, ErrNoCertParent) { - return err - } else if errors.Is(err, ErrNoCertParent) { - continue - } else { - for _, rel := range postRels { - if !channels.Submit(ctx, outC, rel) { - return nil - } - } - } - } - - return nil - }) - - operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { - for _, node := range rootCertAuthorities { - if postRels, err := processCertChainParent(node, tx); err != nil && !errors.Is(err, ErrNoCertParent) { - return err - } else if errors.Is(err, ErrNoCertParent) { - continue - } else { - for _, rel := range postRels { - if !channels.Submit(ctx, outC, rel) { - return nil - } - } - } - } - - return nil - }) - - return nil -} - -func PostEnterpriseCAFor(ctx context.Context, db graph.Database, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob], enterpriseCertAuthorities []*graph.Node) error { - - operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { - for _, ecaNode := range enterpriseCertAuthorities { - if thumbprint, err := ecaNode.Properties.Get(ad.CertThumbprint.String()).String(); err != nil { - return err - } else if thumbprint != "" { - if rootCAIDs, err := findNodesByCertThumbprint(thumbprint, tx, ad.RootCA); err != nil { - return err - } else { - for _, rootCANodeID := range rootCAIDs { - if !channels.Submit(ctx, outC, analysis.CreatePostRelationshipJob{ - FromID: ecaNode.ID, - ToID: rootCANodeID, - Kind: ad.EnterpriseCAFor, - }) { - return nil - } - } - } - } - } - - return nil - }) - - return nil -} - -func processCertChainParent(node *graph.Node, tx graph.Transaction) ([]analysis.CreatePostRelationshipJob, error) { - if certChain, err := node.Properties.Get(ad.CertChain.String()).StringSlice(); err != nil { - return []analysis.CreatePostRelationshipJob{}, err - } else if len(certChain) > 1 { - parentCert := certChain[1] - if targetNodes, err := findNodesByCertThumbprint(parentCert, tx, ad.EnterpriseCA, ad.RootCA); err != nil { - return []analysis.CreatePostRelationshipJob{}, err - } else { - return slices.Map(targetNodes, func(nodeId graph.ID) analysis.CreatePostRelationshipJob { - return analysis.CreatePostRelationshipJob{ - FromID: node.ID, - ToID: nodeId, - Kind: ad.IssuedSignedBy, - } - }), nil - } - } else { - return []analysis.CreatePostRelationshipJob{}, ErrNoCertParent - } -} - -func findNodesByCertThumbprint(certThumbprint string, tx graph.Transaction, kinds ...graph.Kind) ([]graph.ID, error) { - return ops.FetchNodeIDs(tx.Nodes().Filterf(func() graph.Criteria { - return query.And( - query.KindIn(query.Node(), kinds...), - query.Equals( - query.NodeProperty(ad.CertThumbprint.String()), - certThumbprint, - ), - ) - })) -} diff --git a/packages/go/analysis/ad/esc3.go b/packages/go/analysis/ad/esc3.go new file mode 100644 index 000000000..9f3e8adfc --- /dev/null +++ b/packages/go/analysis/ad/esc3.go @@ -0,0 +1,233 @@ +package ad + +import ( + "context" + "github.com/specterops/bloodhound/analysis" + "github.com/specterops/bloodhound/dawgs/graph" + "github.com/specterops/bloodhound/dawgs/util/channels" + "github.com/specterops/bloodhound/graphschema/ad" + "github.com/specterops/bloodhound/log" + "github.com/specterops/bloodhound/slices" +) + +func PostEnrollOnBehalfOf(certTemplates []*graph.Node, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob]) error { + versionOneTemplates := make([]*graph.Node, 0) + versionTwoTemplates := make([]*graph.Node, 0) + + for _, node := range certTemplates { + if version, err := node.Properties.Get(ad.SchemaVersion.String()).Float64(); err != nil { + log.Errorf("Error getting schema version for cert template %d: %v", node.ID, err) + } else { + if version == 1 { + versionOneTemplates = append(versionOneTemplates, node) + } else if version >= 2 { + versionTwoTemplates = append(versionTwoTemplates, node) + } else { + log.Warnf("Got cert template %d with an invalid version %d", node.ID, version) + } + } + } + + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + if results, err := EnrollOnBehalfOfVersionTwo(tx, versionTwoTemplates, certTemplates); err != nil { + return err + } else { + for _, result := range results { + if !channels.Submit(ctx, outC, result) { + return nil + } + } + + return nil + } + }) + + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + if results, err := EnrollOnBehalfOfVersionOne(tx, versionOneTemplates, certTemplates); err != nil { + return err + } else { + for _, result := range results { + if !channels.Submit(ctx, outC, result) { + return nil + } + } + + return nil + } + }) + + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + if results, err := EnrollOnBehalfOfSelfControl(tx, versionOneTemplates); err != nil { + return err + } else { + for _, result := range results { + if !channels.Submit(ctx, outC, result) { + return nil + } + } + + return nil + } + }) + + return nil +} + +func EnrollOnBehalfOfVersionTwo(tx graph.Transaction, versionTwoCertTemplates, allCertTemplates []*graph.Node) ([]analysis.CreatePostRelationshipJob, error) { + results := make([]analysis.CreatePostRelationshipJob, 0) + for _, certTemplateOne := range allCertTemplates { + if hasBadEku, err := certTemplateHasEku(certTemplateOne, EkuAnyPurpose); err != nil { + log.Errorf("error getting ekus for cert template %d: %w", certTemplateOne.ID, err) + } else if hasBadEku { + continue + } else if hasEku, err := certTemplateHasEku(certTemplateOne, EkuCertRequestAgent); err != nil { + log.Errorf("error getting ekus for cert template %d: %w", certTemplateOne.ID, err) + } else if !hasEku { + continue + } else if domainNode, err := getDomainForCertTemplate(tx, certTemplateOne); err != nil { + log.Errorf("error getting domain node for cert template %d: %w", certTemplateOne.ID, err) + } else if isLinked, err := DoesCertTemplateLinkToDomain(tx, certTemplateOne, domainNode); err != nil { + log.Errorf("error fetching paths from cert template %d to domain: %w", certTemplateOne.ID, err) + } else if !isLinked { + continue + } else { + for _, certTemplateTwo := range versionTwoCertTemplates { + if certTemplateOne.ID == certTemplateTwo.ID { + continue + } else if authorizedSignatures, err := certTemplateTwo.Properties.Get(ad.AuthorizedSignatures.String()).Float64(); err != nil { + log.Errorf("Error getting authorized signatures for cert template %d: %w", certTemplateTwo.ID, err) + } else if authorizedSignatures < 1 { + continue + } else if applicationPolicies, err := certTemplateTwo.Properties.Get(ad.ApplicationPolicies.String()).StringSlice(); err != nil { + log.Errorf("Error getting application policies for cert template %d: %w", certTemplateTwo.ID, err) + } else if !slices.Contains(applicationPolicies, EkuCertRequestAgent) { + continue + } else if isLinked, err := DoesCertTemplateLinkToDomain(tx, certTemplateTwo, domainNode); err != nil { + log.Errorf("error fetch paths from cert template %d to domain: %w", certTemplateTwo.ID, err) + } else if !isLinked { + continue + } else { + results = append(results, analysis.CreatePostRelationshipJob{ + FromID: certTemplateOne.ID, + ToID: certTemplateTwo.ID, + Kind: ad.EnrollOnBehalfOf, + }) + } + } + } + } + + return results, nil +} + +func certTemplateHasEku(certTemplate *graph.Node, targetEkus ...string) (bool, error) { + if ekus, err := certTemplate.Properties.Get(ad.EKUs.String()).StringSlice(); err != nil { + return false, err + } else { + for _, eku := range ekus { + for _, targetEku := range targetEkus { + if eku == targetEku { + return true, nil + } + } + } + + return false, nil + } +} + +func EnrollOnBehalfOfVersionOne(tx graph.Transaction, versionOneCertTemplates []*graph.Node, allCertTemplates []*graph.Node) ([]analysis.CreatePostRelationshipJob, error) { + results := make([]analysis.CreatePostRelationshipJob, 0) + + for _, certTemplateOne := range allCertTemplates { + //prefilter as much as we can first + if slices.Contains(versionOneCertTemplates, certTemplateOne) { + continue + } else if hasEku, err := certTemplateHasEkuOrAll(certTemplateOne, EkuCertRequestAgent, EkuAnyPurpose); err != nil { + log.Errorf("Error checking ekus for certtemplate %d: %w", certTemplateOne.ID, err) + } else if !hasEku { + continue + } else if domainNode, err := getDomainForCertTemplate(tx, certTemplateOne); err != nil { + log.Errorf("Error getting domain node for certtemplate %d: %w", certTemplateOne.ID, err) + } else if hasPath, err := DoesCertTemplateLinkToDomain(tx, certTemplateOne, domainNode); err != nil { + log.Errorf("Error fetching paths from certtemplate %d to domain: %w", certTemplateOne.ID, err) + } else if !hasPath { + continue + } else { + for _, certTemplateTwo := range versionOneCertTemplates { + if certTemplateTwo.ID == certTemplateOne.ID { + continue + } else if hasPath, err := DoesCertTemplateLinkToDomain(tx, certTemplateTwo, domainNode); err != nil { + log.Errorf("Error getting domain node for certtemplate %d: %w", certTemplateTwo.ID, err) + } else if !hasPath { + continue + } else { + results = append(results, analysis.CreatePostRelationshipJob{ + FromID: certTemplateOne.ID, + ToID: certTemplateTwo.ID, + Kind: ad.EnrollOnBehalfOf, + }) + } + } + } + } + + return results, nil +} + +func certTemplateHasEkuOrAll(certTemplate *graph.Node, targetEkus ...string) (bool, error) { + if ekus, err := certTemplate.Properties.Get(ad.EKUs.String()).StringSlice(); err != nil { + return false, err + } else if len(ekus) == 0 { + return true, nil + } else { + for _, eku := range ekus { + for _, targetEku := range targetEkus { + if eku == targetEku { + return true, nil + } + } + } + + return false, nil + } +} + +func getDomainForCertTemplate(tx graph.Transaction, certTemplate *graph.Node) (*graph.Node, error) { + if domainSid, err := certTemplate.Properties.Get(ad.DomainSID.String()).String(); err != nil { + return &graph.Node{}, err + } else if domainNode, err := analysis.FetchNodeByObjectID(tx, domainSid); err != nil { + return &graph.Node{}, err + } else { + return domainNode, nil + } +} + +func EnrollOnBehalfOfSelfControl(tx graph.Transaction, versionOneCertTemplates []*graph.Node) ([]analysis.CreatePostRelationshipJob, error) { + results := make([]analysis.CreatePostRelationshipJob, 0) + for _, certTemplate := range versionOneCertTemplates { + if hasEku, err := certTemplateHasEkuOrAll(certTemplate, EkuAnyPurpose); err != nil { + log.Errorf("Error checking ekus for certtemplate %d: %w", certTemplate.ID, err) + } else if !hasEku { + continue + } else if subjectRequireUpn, err := certTemplate.Properties.Get(ad.SubjectAltRequireUPN.String()).Bool(); err != nil { + log.Errorf("Error getting subjectAltRequireUPN for certtemplate %d: %w", certTemplate.ID, err) + } else if !subjectRequireUpn { + continue + } else if domainNode, err := getDomainForCertTemplate(tx, certTemplate); err != nil { + log.Errorf("Error getting domain for certtemplate %d: %w", certTemplate.ID, err) + } else if doesLink, err := DoesCertTemplateLinkToDomain(tx, certTemplate, domainNode); err != nil { + log.Errorf("Error fetching paths from certtemplate %d to domain: %w", certTemplate.ID, err) + } else if !doesLink { + continue + } else { + results = append(results, analysis.CreatePostRelationshipJob{ + FromID: certTemplate.ID, + ToID: certTemplate.ID, + Kind: ad.EnrollOnBehalfOf, + }) + } + } + + return results, nil +} diff --git a/packages/go/analysis/ad/esc6.go b/packages/go/analysis/ad/esc6.go new file mode 100644 index 000000000..304678481 --- /dev/null +++ b/packages/go/analysis/ad/esc6.go @@ -0,0 +1,79 @@ +package ad + +import ( + "context" + "fmt" + "github.com/specterops/bloodhound/analysis" + "github.com/specterops/bloodhound/dawgs/graph" + "github.com/specterops/bloodhound/dawgs/ops" + "github.com/specterops/bloodhound/dawgs/query" + "github.com/specterops/bloodhound/dawgs/util/channels" + "github.com/specterops/bloodhound/graphschema/ad" +) + +func PostCanAbuseUPNCertMapping(_ context.Context, _ graph.Database, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob], enterpriseCertAuthorities []*graph.Node) error { + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + for _, eca := range enterpriseCertAuthorities { + if ecaDomainSID, err := eca.Properties.Get(ad.DomainSID.String()).String(); err != nil { + return err + } else if ecaDomain, err := analysis.FetchNodeByObjectID(tx, ecaDomainSID); err != nil { + return err + } else if trustedByNodes, err := fetchNodesWithTrustedByParentChildRelationship(tx, ecaDomain); err != nil { + return err + } else { + for _, trustedByDomain := range trustedByNodes { + if dcForNodes, err := fetchNodesWithDCForEdge(tx, trustedByDomain); err != nil { + return err + } else { + for _, dcForNode := range dcForNodes { + if cmmrProperty, err := dcForNode.Properties.Get(ad.CertificateMappingMethodsRaw.String()).Int(); err != nil { + return err + } else if cmmrProperty&0x04 == 0x04 { + if !channels.Submit(ctx, outC, analysis.CreatePostRelationshipJob{ + FromID: eca.ID, + ToID: dcForNode.ID, + Kind: ad.CanAbuseUPNCertMapping, + }) { + return fmt.Errorf("context timed out while creating CanAbuseUPNCert edge") + } + } + } + } + } + } + } + return nil + }) + return nil +} + +func fetchNodesWithTrustedByParentChildRelationship(tx graph.Transaction, root *graph.Node) (graph.NodeSet, error) { + if nodeSet, err := ops.AcyclicTraverseTerminals(tx, ops.TraversalPlan{ + Root: root, + Direction: graph.DirectionOutbound, + BranchQuery: func() graph.Criteria { + return query.And( + query.KindIn(query.Relationship(), ad.TrustedBy), + query.Equals(query.RelationshipProperty(ad.TrustType.String()), "ParentChild"), + ) + }, + }); err != nil { + return graph.NodeSet{}, err + } else { + nodeSet.Add(root) + return nodeSet, nil + } +} + +func fetchNodesWithDCForEdge(tx graph.Transaction, rootNode *graph.Node) (graph.NodeSet, error) { + return ops.AcyclicTraverseTerminals(tx, ops.TraversalPlan{ + Root: rootNode, + Direction: graph.DirectionInbound, + BranchQuery: func() graph.Criteria { + return query.And( + query.KindIn(query.Start(), ad.Computer), + query.KindIn(query.Relationship(), ad.DCFor), + ) + }, + }) +} diff --git a/packages/go/analysis/ad/esc_shared.go b/packages/go/analysis/ad/esc_shared.go new file mode 100644 index 000000000..c55a8965d --- /dev/null +++ b/packages/go/analysis/ad/esc_shared.go @@ -0,0 +1,152 @@ +package ad + +import ( + "context" + "errors" + "fmt" + "github.com/specterops/bloodhound/analysis" + "github.com/specterops/bloodhound/dawgs/graph" + "github.com/specterops/bloodhound/dawgs/ops" + "github.com/specterops/bloodhound/dawgs/query" + "github.com/specterops/bloodhound/dawgs/util/channels" + "github.com/specterops/bloodhound/graphschema/ad" + "github.com/specterops/bloodhound/slices" +) + +func PostTrustedForNTAuth(ctx context.Context, db graph.Database, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob]) error { + + if ntAuthStoreNodes, err := FetchNodesByKind(ctx, db, ad.NTAuthStore); err != nil { + return err + } else { + for _, node := range ntAuthStoreNodes { + innerNode := node + + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + if thumbprints, err := innerNode.Properties.Get(ad.CertThumbprints.String()).StringSlice(); err != nil { + return err + } else { + for _, thumbprint := range thumbprints { + if thumbprint != "" { + if sourceNodeIDs, err := findNodesByCertThumbprint(thumbprint, tx, ad.EnterpriseCA); err != nil { + return err + } else { + for _, sourceNodeID := range sourceNodeIDs { + if !channels.Submit(ctx, outC, analysis.CreatePostRelationshipJob{ + FromID: sourceNodeID, + ToID: innerNode.ID, + Kind: ad.TrustedForNTAuth, + }) { + return nil + } + } + } + } + } + } + return nil + }) + } + } + + return nil +} + +func PostIssuedSignedBy(_ context.Context, _ graph.Database, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob], enterpriseCertAuthorities []*graph.Node, rootCertAuthorities []*graph.Node) error { + + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + for _, node := range enterpriseCertAuthorities { + if postRels, err := processCertChainParent(node, tx); err != nil && !errors.Is(err, ErrNoCertParent) { + return err + } else if errors.Is(err, ErrNoCertParent) { + continue + } else { + for _, rel := range postRels { + if !channels.Submit(ctx, outC, rel) { + return nil + } + } + } + } + + return nil + }) + + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + for _, node := range rootCertAuthorities { + if postRels, err := processCertChainParent(node, tx); err != nil && !errors.Is(err, ErrNoCertParent) { + return err + } else if errors.Is(err, ErrNoCertParent) { + continue + } else { + for _, rel := range postRels { + if !channels.Submit(ctx, outC, rel) { + return nil + } + } + } + } + + return nil + }) + + return nil +} + +func PostEnterpriseCAFor(_ context.Context, _ graph.Database, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob], enterpriseCertAuthorities []*graph.Node) error { + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + for _, ecaNode := range enterpriseCertAuthorities { + if thumbprint, err := ecaNode.Properties.Get(ad.CertThumbprint.String()).String(); err != nil { + return err + } else if thumbprint != "" { + if rootCAIDs, err := findNodesByCertThumbprint(thumbprint, tx, ad.RootCA); err != nil { + return err + } else { + for _, rootCANodeID := range rootCAIDs { + if !channels.Submit(ctx, outC, analysis.CreatePostRelationshipJob{ + FromID: ecaNode.ID, + ToID: rootCANodeID, + Kind: ad.EnterpriseCAFor, + }) { + return fmt.Errorf("context timed out while creating EnterpriseCAFor edge") + } + } + } + } + } + return nil + }) + return nil +} + +func processCertChainParent(node *graph.Node, tx graph.Transaction) ([]analysis.CreatePostRelationshipJob, error) { + if certChain, err := node.Properties.Get(ad.CertChain.String()).StringSlice(); err != nil { + return []analysis.CreatePostRelationshipJob{}, err + } else if len(certChain) > 1 { + parentCert := certChain[1] + if targetNodes, err := findNodesByCertThumbprint(parentCert, tx, ad.EnterpriseCA, ad.RootCA); err != nil { + return []analysis.CreatePostRelationshipJob{}, err + } else { + return slices.Map(targetNodes, func(nodeId graph.ID) analysis.CreatePostRelationshipJob { + return analysis.CreatePostRelationshipJob{ + FromID: node.ID, + ToID: nodeId, + Kind: ad.IssuedSignedBy, + } + }), nil + } + } else { + return []analysis.CreatePostRelationshipJob{}, ErrNoCertParent + } +} + +func findNodesByCertThumbprint(certThumbprint string, tx graph.Transaction, kinds ...graph.Kind) ([]graph.ID, error) { + return ops.FetchNodeIDs(tx.Nodes().Filterf(func() graph.Criteria { + return query.And( + query.KindIn(query.Node(), kinds...), + query.Equals( + query.NodeProperty(ad.CertThumbprint.String()), + certThumbprint, + ), + ) + })) +} diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go index 0bc22fd7b..00e602349 100644 --- a/packages/go/graphschema/ad/ad.go +++ b/packages/go/graphschema/ad/ad.go @@ -65,6 +65,7 @@ var ( HasSIDHistory = graph.StringKind("HasSIDHistory") AddSelf = graph.StringKind("AddSelf") DCSync = graph.StringKind("DCSync") + DCFor = graph.StringKind("DCFor") ReadLAPSPassword = graph.StringKind("ReadLAPSPassword") ReadGMSAPassword = graph.StringKind("ReadGMSAPassword") DumpSMSAPassword = graph.StringKind("DumpSMSAPassword") @@ -89,6 +90,7 @@ var ( NTAuthStoreFor = graph.StringKind("NTAuthStoreFor") TrustedForNTAuth = graph.StringKind("TrustedForNTAuth") EnterpriseCAFor = graph.StringKind("EnterpriseCAFor") + CanAbuseUPNCertMapping = graph.StringKind("CanAbuseUPNCertMapping") IssuedSignedBy = graph.StringKind("IssuedSignedBy") GoldenCert = graph.StringKind("GoldenCert") EnrollOnBehalfOf = graph.StringKind("EnrollOnBehalfOf")