From dd48147eabe2a729aef822a2aa0d26334fedb106 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Tue, 23 Jan 2024 15:39:57 -0500 Subject: [PATCH 01/36] feat: esc9a post --- packages/cue/bh/ad/ad.cue | 11 +- packages/go/analysis/ad/adcs.go | 10 ++ packages/go/analysis/ad/esc9.go | 168 ++++++++++++++++++ packages/go/analysis/ad/queries.go | 14 ++ packages/go/graphschema/ad/ad.go | 7 +- packages/go/graphschema/azure/azure.go | 2 +- packages/go/graphschema/common/common.go | 2 +- packages/go/graphschema/graph.go | 2 +- .../bh-shared-ui/src/graphSchema.ts | 6 +- 9 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 packages/go/analysis/ad/esc9.go diff --git a/packages/cue/bh/ad/ad.cue b/packages/cue/bh/ad/ad.cue index aace277a96..951e4b9e35 100644 --- a/packages/cue/bh/ad/ad.cue +++ b/packages/cue/bh/ad/ad.cue @@ -1001,6 +1001,11 @@ ADCSESC7: types.#Kind & { schema: "active_directory" } +ADCSESC9a: types.#Kind & { + symbol: "ADCSESC9a" + schema: "active_directory" +} + // Relationship Kinds RelationshipKinds: [ Owns, @@ -1063,7 +1068,8 @@ RelationshipKinds: [ ADCSESC4, ADCSESC5, ADCSESC6, - ADCSESC7 + ADCSESC7, + ADCSESC9a ] // ACL Relationships @@ -1135,5 +1141,6 @@ PathfindingRelationships: [ ADCSESC5, ADCSESC6, ADCSESC7, - DCFor + DCFor, + ADCSESC9a ] diff --git a/packages/go/analysis/ad/adcs.go b/packages/go/analysis/ad/adcs.go index 492c1b15b9..d0763091c7 100644 --- a/packages/go/analysis/ad/adcs.go +++ b/packages/go/analysis/ad/adcs.go @@ -309,6 +309,16 @@ func PostADCS(ctx context.Context, db graph.Database, groupExpansions impact.Pat return nil }) + + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + if err := PostADCSESC9a(ctx, tx, outC, groupExpansions, innerEnterpriseCA, innerDomain, cache); err != nil { + log.Errorf("failed post processing for %s: %v", ad.ADCSESC3.String(), err) + } else { + return nil + } + + return nil + }) } } diff --git a/packages/go/analysis/ad/esc9.go b/packages/go/analysis/ad/esc9.go new file mode 100644 index 0000000000..fd6790310d --- /dev/null +++ b/packages/go/analysis/ad/esc9.go @@ -0,0 +1,168 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package ad + +import ( + "context" + "github.com/specterops/bloodhound/analysis" + "github.com/specterops/bloodhound/analysis/impact" + "github.com/specterops/bloodhound/dawgs/cardinality" + "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/log" +) + +func PostADCSESC9a(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob, groupExpansions impact.PathAggregator, eca, domain *graph.Node, cache ADCSCache) error { + results := cardinality.NewBitmap32() + + if canAbuseWeakCertBindingRels, err := FetchCanAbuseWeakCertBindingRels(tx, eca); err != nil { + if graph.IsErrNotFound(err) { + return nil + } + + return err + } else if len(canAbuseWeakCertBindingRels) == 0 { + return nil + } else if publishedCertTemplates, ok := cache.PublishedTemplateCache[eca.ID]; !ok { + return nil + } else { + for _, template := range publishedCertTemplates { + if valid, err := isCertTemplateValidForESC9a(template); err != nil { + log.Errorf("Error checking cert template validity for template %d: %v", template.ID, err) + } else if !valid { + continue + } else if certTemplateControllers, ok := cache.CertTemplateControllers[template.ID]; !ok { + log.Errorf("Failed to retrieve controllers for cert template %d from cache", template.ID) + continue + } else if ecaControllers, ok := cache.EnterpriseCAEnrollers[eca.ID]; !ok { + log.Errorf("Failed to retrieve controllers for enterprise ca %d from cache", eca.ID) + continue + } else { + //Expand controllers for the eca + template completely because we don't do group shortcutting here + var ( + victimBitmap = expandNodeSliceToBitmapWithoutGroups(certTemplateControllers, groupExpansions) + ecaBitmap = expandNodeSliceToBitmapWithoutGroups(ecaControllers, groupExpansions) + ) + + victimBitmap.And(ecaBitmap) + //Use our id list to filter down to users + if userNodes, err := ops.FetchNodeSet(tx.Nodes().Filterf(func() graph.Criteria { + return query.And( + query.KindIn(query.Node(), ad.User), + query.InIDs(query.NodeID(), cardinality.DuplexToGraphIDs(victimBitmap)...), + ) + })); err != nil { + if !graph.IsErrNotFound(err) { + return err + } + } else if len(userNodes) > 0 { + if subjRequireDns, err := template.Properties.Get(ad.SubjectAltRequireDNS.String()).Bool(); err != nil { + log.Errorf("Failed to retrieve subjectAltRequireDNS for template %d: %v", template.ID, err) + } else if subjRequireDomainDns, err := template.Properties.Get(ad.SubjectAltRequireDomainDNS.String()).Bool(); err != nil { + log.Errorf("Failed to retrieve subjectAltRequireDomainDNS for template %d: %v", template.ID, err) + } else if subjRequireDns || subjRequireDomainDns { + //If either of these properties is true, we need to remove all these users from our victims list + victimBitmap.Xor(cardinality.NodeSetToDuplex(userNodes)) + } + } + + if attackers, err := ops.FetchStartNodes(tx.Relationships().Filterf(func() graph.Criteria { + return query.And( + query.KindIn(query.Start(), ad.Group, ad.User, ad.Computer), + query.KindIn(query.Relationship(), ad.GenericAll, ad.GenericWrite, ad.Owns, ad.WriteOwner, ad.WriteDACL), + query.InIDs(query.EndID(), cardinality.DuplexToGraphIDs(victimBitmap)...), + ) + })); err != nil { + return err + } else { + results.Or(cardinality.NodeSetToDuplex(attackers)) + } + } + } + + results.Each(func(value uint32) (bool, error) { + if !channels.Submit(ctx, outC, analysis.CreatePostRelationshipJob{ + FromID: graph.ID(value), + ToID: domain.ID, + Kind: ad.ADCSESC9a, + }) { + return false, nil + } else { + return true, nil + } + }) + + return nil + } +} + +func expandNodeSliceToBitmapWithoutGroups(nodes []*graph.Node, groupExpansions impact.PathAggregator) cardinality.Duplex[uint32] { + var bitmap = cardinality.NewBitmap32() + for _, controller := range nodes { + if controller.Kinds.ContainsOneOf(ad.Group) { + groupExpansions.Cardinality(controller.ID.Uint32()).(cardinality.Duplex[uint32]).Each(func(id uint32) (bool, error) { + //Check group expansions against each id, if cardinality is 0 than its not a group + if groupExpansions.Cardinality(id).Cardinality() == 0 { + bitmap.Add(id) + } + + return true, nil + }) + } else { + bitmap.Add(controller.ID.Uint32()) + } + } + + return bitmap +} + +func isCertTemplateValidForESC9a(ct *graph.Node) (bool, error) { + if reqManagerApproval, err := ct.Properties.Get(ad.RequiresManagerApproval.String()).Bool(); err != nil { + return false, err + } else if reqManagerApproval { + return false, nil + } else if authenticationEnabled, err := ct.Properties.Get(ad.AuthenticationEnabled.String()).Bool(); err != nil { + return false, err + } else if !authenticationEnabled { + return false, nil + } else if noSecurityExtension, err := ct.Properties.Get(ad.NoSecurityExtension.String()).Bool(); err != nil { + return false, err + } else if !noSecurityExtension { + return false, nil + } else if enrolleeSuppliesSubject, err := ct.Properties.Get(ad.EnrolleeSuppliesSubject.String()).Bool(); err != nil { + return false, err + } else if enrolleeSuppliesSubject { + return false, nil + } else if schemaVersion, err := ct.Properties.Get(ad.SchemaVersion.String()).Float64(); err != nil { + return false, err + } else if authorizedSignatures, err := ct.Properties.Get(ad.AuthorizedSignatures.String()).Float64(); err != nil { + return false, err + } else if schemaVersion > 1 && authorizedSignatures > 0 { + return false, nil + } else if subjectAltRequireUPN, err := ct.Properties.Get(ad.SubjectAltRequireUPN.String()).Bool(); err != nil { + return false, err + } else if subjectAltRequireSPN, err := ct.Properties.Get(ad.SubjectAltRequireSPN.String()).Bool(); err != nil { + return false, err + } else if subjectAltRequireSPN || subjectAltRequireUPN { + return true, nil + } else { + return false, nil + } +} diff --git a/packages/go/analysis/ad/queries.go b/packages/go/analysis/ad/queries.go index 1293ea56f8..7c3dc6bbf2 100644 --- a/packages/go/analysis/ad/queries.go +++ b/packages/go/analysis/ad/queries.go @@ -1425,6 +1425,20 @@ func FetchCertTemplatesPublishedToCA(tx graph.Transaction, ca *graph.Node) (grap })) } +func FetchCanAbuseWeakCertBindingRels(tx graph.Transaction, node *graph.Node) ([]*graph.Relationship, error) { + if rels, err := ops.FetchRelationships(tx.Relationships().Filterf(func() graph.Criteria { + return query.And( + query.Equals(query.StartID(), node.ID), + query.Kind(query.Relationship(), ad.CanAbuseWeakCertBinding), + query.Kind(query.End(), ad.Entity), + ) + })); err != nil { + return nil, err + } else { + return rels, nil + } +} + func FetchEnterpriseCAsCertChainPathToDomain(tx graph.Transaction, enterpriseCA, domain *graph.Node) (graph.PathSet, error) { return ops.TraversePaths(tx, ops.TraversalPlan{ Root: enterpriseCA, diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go index ccfe7bffc8..fe283c6766 100644 --- a/packages/go/graphschema/ad/ad.go +++ b/packages/go/graphschema/ad/ad.go @@ -1,4 +1,4 @@ -// Copyright 2023 Specter Ops, Inc. +// Copyright 2024 Specter Ops, Inc. // // Licensed under the Apache License, Version 2.0 // you may not use this file except in compliance with the License. @@ -101,6 +101,7 @@ var ( ADCSESC5 = graph.StringKind("ADCSESC5") ADCSESC6 = graph.StringKind("ADCSESC6") ADCSESC7 = graph.StringKind("ADCSESC7") + ADCSESC9a = graph.StringKind("ADCSESC9a") ) type Property string @@ -638,13 +639,13 @@ func Nodes() []graph.Kind { return []graph.Kind{Entity, User, Computer, Group, GPO, OU, Container, Domain, LocalGroup, LocalUser, AIACA, RootCA, EnterpriseCA, NTAuthStore, CertTemplate} } func Relationships() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, GetChanges, GetChangesAll, GetChangesInFilteredSet, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, LocalToComputer, MemberOfLocalGroup, RemoteInteractiveLogonPrivilege, SyncLAPSPassword, WriteAccountRestrictions, RootCAFor, DCFor, PublishedTo, ManageCertificates, ManageCA, DelegatedEnrollmentAgent, Enroll, HostsCAService, WritePKIEnrollmentFlag, WritePKINameFlag, NTAuthStoreFor, TrustedForNTAuth, EnterpriseCAFor, CanAbuseUPNCertMapping, CanAbuseWeakCertBinding, IssuedSignedBy, GoldenCert, EnrollOnBehalfOf, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6, ADCSESC7} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, GetChanges, GetChangesAll, GetChangesInFilteredSet, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, LocalToComputer, MemberOfLocalGroup, RemoteInteractiveLogonPrivilege, SyncLAPSPassword, WriteAccountRestrictions, RootCAFor, DCFor, PublishedTo, ManageCertificates, ManageCA, DelegatedEnrollmentAgent, Enroll, HostsCAService, WritePKIEnrollmentFlag, WritePKINameFlag, NTAuthStoreFor, TrustedForNTAuth, EnterpriseCAFor, CanAbuseUPNCertMapping, CanAbuseWeakCertBinding, IssuedSignedBy, GoldenCert, EnrollOnBehalfOf, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6, ADCSESC7, ADCSESC9a} } func ACLRelationships() []graph.Kind { return []graph.Kind{AllExtendedRights, ForceChangePassword, AddMember, AddAllowedToAct, GenericAll, WriteDACL, WriteOwner, GenericWrite, ReadLAPSPassword, ReadGMSAPassword, Owns, AddSelf, WriteSPN, AddKeyCredentialLink, GetChanges, GetChangesAll, GetChangesInFilteredSet, WriteAccountRestrictions, SyncLAPSPassword, DCSync, ManageCertificates, ManageCA, Enroll, WritePKIEnrollmentFlag, WritePKINameFlag} } func PathfindingRelationships() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6, ADCSESC7, DCFor} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6, ADCSESC7, DCFor, ADCSESC9a} } func IsACLKind(s graph.Kind) bool { for _, acl := range ACLRelationships() { diff --git a/packages/go/graphschema/azure/azure.go b/packages/go/graphschema/azure/azure.go index 2d551de659..8262cbf3a0 100644 --- a/packages/go/graphschema/azure/azure.go +++ b/packages/go/graphschema/azure/azure.go @@ -1,4 +1,4 @@ -// Copyright 2023 Specter Ops, Inc. +// Copyright 2024 Specter Ops, Inc. // // Licensed under the Apache License, Version 2.0 // you may not use this file except in compliance with the License. diff --git a/packages/go/graphschema/common/common.go b/packages/go/graphschema/common/common.go index 058969f506..6fd161585e 100644 --- a/packages/go/graphschema/common/common.go +++ b/packages/go/graphschema/common/common.go @@ -1,4 +1,4 @@ -// Copyright 2023 Specter Ops, Inc. +// Copyright 2024 Specter Ops, Inc. // // Licensed under the Apache License, Version 2.0 // you may not use this file except in compliance with the License. diff --git a/packages/go/graphschema/graph.go b/packages/go/graphschema/graph.go index 34a7f9b11b..3deedefca6 100644 --- a/packages/go/graphschema/graph.go +++ b/packages/go/graphschema/graph.go @@ -1,4 +1,4 @@ -// Copyright 2023 Specter Ops, Inc. +// Copyright 2024 Specter Ops, Inc. // // Licensed under the Apache License, Version 2.0 // you may not use this file except in compliance with the License. diff --git a/packages/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts index 05ddc0b561..4e691b0839 100644 --- a/packages/javascript/bh-shared-ui/src/graphSchema.ts +++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts @@ -1,4 +1,4 @@ -// Copyright 2023 Specter Ops, Inc. +// Copyright 2024 Specter Ops, Inc. // // Licensed under the Apache License, Version 2.0 // you may not use this file except in compliance with the License. @@ -129,6 +129,7 @@ export enum ActiveDirectoryRelationshipKind { ADCSESC5 = 'ADCSESC5', ADCSESC6 = 'ADCSESC6', ADCSESC7 = 'ADCSESC7', + ADCSESC9a = 'ADCSESC9a', } export function ActiveDirectoryRelationshipKindToDisplay(value: ActiveDirectoryRelationshipKind): string | undefined { switch (value) { @@ -254,6 +255,8 @@ export function ActiveDirectoryRelationshipKindToDisplay(value: ActiveDirectoryR return 'ADCSESC6'; case ActiveDirectoryRelationshipKind.ADCSESC7: return 'ADCSESC7'; + case ActiveDirectoryRelationshipKind.ADCSESC9a: + return 'ADCSESC9a'; default: return undefined; } @@ -521,6 +524,7 @@ export function ActiveDirectoryPathfindingEdges(): ActiveDirectoryRelationshipKi ActiveDirectoryRelationshipKind.ADCSESC6, ActiveDirectoryRelationshipKind.ADCSESC7, ActiveDirectoryRelationshipKind.DCFor, + ActiveDirectoryRelationshipKind.ADCSESC9a, ]; } export enum AzureNodeKind { From 5ea03247cfac66fd874e6e55f7d9e6aec661783e Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Tue, 23 Jan 2024 16:37:09 -0500 Subject: [PATCH 02/36] test: add esc9 test --- .../src/analysis/ad/adcs_integration_test.go | 63 ++++++++++++++++++- cmd/api/src/test/integration/harnesses.go | 38 +++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/cmd/api/src/analysis/ad/adcs_integration_test.go b/cmd/api/src/analysis/ad/adcs_integration_test.go index 127b0f6b5c..bcd8cef6f7 100644 --- a/cmd/api/src/analysis/ad/adcs_integration_test.go +++ b/cmd/api/src/analysis/ad/adcs_integration_test.go @@ -492,7 +492,7 @@ func TestADCSESC3(t *testing.T) { for _, enterpriseCA := range enterpriseCAs { if cache.DoesCAChainProperlyToDomain(enterpriseCA, innerDomain) { if err := ad2.PostADCSESC3(ctx, tx, outC, groupExpansions, enterpriseCA, innerDomain, cache); err != nil { - t.Logf("failed post processing for %s: %v", ad.ADCSESC1.String(), err) + t.Logf("failed post processing for %s: %v", ad.ADCSESC3.String(), err) } else { return nil } @@ -549,7 +549,7 @@ func TestADCSESC3(t *testing.T) { for _, enterpriseCA := range enterpriseCAs { if cache.DoesCAChainProperlyToDomain(enterpriseCA, innerDomain) { if err := ad2.PostADCSESC3(ctx, tx, outC, groupExpansions, enterpriseCA, innerDomain, cache); err != nil { - t.Logf("failed post processing for %s: %v", ad.ADCSESC1.String(), err) + t.Logf("failed post processing for %s: %v", ad.ADCSESC3.String(), err) } else { return nil } @@ -586,3 +586,62 @@ func TestADCSESC3(t *testing.T) { }) }) } + +func TestADCSESC9a(t *testing.T) { + testContext := integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema()) + testContext.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) error { + harness.ESC9AHarness.Setup(testContext) + return nil + }, func(harness integration.HarnessDetails, db graph.Database) { + operation := analysis.NewPostRelationshipOperation(context.Background(), db, "ADCS Post Process Test - ESC9a") + + groupExpansions, err := ad2.ExpandAllRDPLocalGroups(context.Background(), db) + require.Nil(t, err) + enterpriseCertAuthorities, err := ad2.FetchNodesByKind(context.Background(), db, ad.EnterpriseCA) + require.Nil(t, err) + certTemplates, err := ad2.FetchNodesByKind(context.Background(), db, ad.CertTemplate) + require.Nil(t, err) + domains, err := ad2.FetchNodesByKind(context.Background(), db, ad.Domain) + require.Nil(t, err) + + cache := ad2.NewADCSCache() + cache.BuildCache(context.Background(), db, enterpriseCertAuthorities, certTemplates) + + for _, domain := range domains { + innerDomain := domain + + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + if enterpriseCAs, err := ad2.FetchEnterpriseCAsTrustedForNTAuthToDomain(tx, innerDomain); err != nil { + return err + } else { + for _, enterpriseCA := range enterpriseCAs { + if cache.DoesCAChainProperlyToDomain(enterpriseCA, innerDomain) { + if err := ad2.PostADCSESC9a(ctx, tx, outC, groupExpansions, enterpriseCA, innerDomain, cache); err != nil { + t.Logf("failed post processing for %s: %v", ad.ADCSESC9a.String(), err) + } else { + return nil + } + } + } + } + return nil + }) + } + operation.Done() + + db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { + if results, err := ops.FetchStartNodes(tx.Relationships().Filterf(func() graph.Criteria { + return query.Kind(query.Relationship(), ad.ADCSESC9a) + })); err != nil { + t.Fatalf("error fetching esc3 edges in integration test; %v", err) + } else { + assert.Equal(t, 1, len(results)) + + require.True(t, results.Contains(harness.ESC9AHarness.Attacker)) + + } + return nil + }) + }) + +} diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go index 2b5b74a6b5..8b2bacd254 100644 --- a/cmd/api/src/test/integration/harnesses.go +++ b/cmd/api/src/test/integration/harnesses.go @@ -1638,6 +1638,43 @@ func (s *ESC3Harness2) Setup(c *GraphTestContext) { c.UpdateNode(s.EnterpriseCA1) } +type ESC9AHarness struct { + Domain *graph.Node + NTAuthStore *graph.Node + RootCA *graph.Node + DC *graph.Node + EnterpriseCA *graph.Node + CertTemplate *graph.Node + Victim *graph.Node + Attacker *graph.Node +} + +func (s *ESC9AHarness) Setup(c *GraphTestContext) { + sid := RandomDomainSID() + s.Domain = c.NewActiveDirectoryDomain("ESC9aDomain", sid, false, true) + s.NTAuthStore = c.NewActiveDirectoryNTAuthStore("NTAuthStore", sid) + s.RootCA = c.NewActiveDirectoryRootCA("RootCA", sid) + s.DC = c.NewActiveDirectoryComputer("DC", sid) + s.EnterpriseCA = c.NewActiveDirectoryEnterpriseCA("eca", sid) + s.CertTemplate = c.NewActiveDirectoryCertTemplate("certtemplate", sid, false, true, false, true, 1, 0, make([]string, 0), make([]string, 0)) + s.CertTemplate.Properties.Set(ad.NoSecurityExtension.String(), true) + s.CertTemplate.Properties.Set(ad.SubjectAltRequireSPN.String(), true) + c.UpdateNode(s.CertTemplate) + s.Victim = c.NewActiveDirectoryUser("victim", sid, false) + s.Attacker = c.NewActiveDirectoryUser("attacker", sid, false) + + c.NewRelationship(s.DC, s.Domain, ad.DCFor) + c.NewRelationship(s.NTAuthStore, s.Domain, ad.NTAuthStoreFor) + c.NewRelationship(s.RootCA, s.Domain, ad.RootCAFor) + c.NewRelationship(s.EnterpriseCA, s.DC, ad.CanAbuseWeakCertBinding) + c.NewRelationship(s.EnterpriseCA, s.NTAuthStore, ad.TrustedForNTAuth) + c.NewRelationship(s.EnterpriseCA, s.RootCA, ad.IssuedSignedBy) + c.NewRelationship(s.CertTemplate, s.EnterpriseCA, ad.PublishedTo) + c.NewRelationship(s.Victim, s.EnterpriseCA, ad.Enroll) + c.NewRelationship(s.Victim, s.CertTemplate, ad.Enroll) + c.NewRelationship(s.Attacker, s.Victim, ad.GenericWrite) +} + type ShortcutHarness struct { Group1 *graph.Node Group2 *graph.Node @@ -1731,4 +1768,5 @@ type HarnessDetails struct { AZInboundControlHarness AZInboundControlHarness ESC3Harness1 ESC3Harness1 ESC3Harness2 ESC3Harness2 + ESC9AHarness ESC9AHarness } From 47eb58294073333866ac96ef156bac6816f15b6f Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Tue, 23 Jan 2024 16:42:13 -0500 Subject: [PATCH 03/36] chore: add harness files --- .../integration/harnesses/esc9aharness.json | 314 ++++++++++++++++++ .../integration/harnesses/esc9aharness.svg | 1 + 2 files changed, 315 insertions(+) create mode 100644 cmd/api/src/test/integration/harnesses/esc9aharness.json create mode 100644 cmd/api/src/test/integration/harnesses/esc9aharness.svg diff --git a/cmd/api/src/test/integration/harnesses/esc9aharness.json b/cmd/api/src/test/integration/harnesses/esc9aharness.json new file mode 100644 index 0000000000..edd9cad152 --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/esc9aharness.json @@ -0,0 +1,314 @@ +{ + "style": { + "font-family": "sans-serif", + "background-color": "#ffffff", + "background-image": "", + "background-size": "100%", + "node-color": "#ffffff", + "border-width": 4, + "border-color": "#000000", + "radius": 50, + "node-padding": 5, + "node-margin": 2, + "outside-position": "auto", + "node-icon-image": "", + "node-background-image": "", + "icon-position": "inside", + "icon-size": 64, + "caption-position": "inside", + "caption-max-width": 200, + "caption-color": "#000000", + "caption-font-size": 50, + "caption-font-weight": "normal", + "label-position": "inside", + "label-display": "pill", + "label-color": "#000000", + "label-background-color": "#ffffff", + "label-border-color": "#000000", + "label-border-width": 4, + "label-font-size": 40, + "label-padding": 5, + "label-margin": 4, + "directionality": "directed", + "detail-position": "inline", + "detail-orientation": "parallel", + "arrow-width": 5, + "arrow-color": "#000000", + "margin-start": 5, + "margin-end": 5, + "margin-peer": 20, + "attachment-start": "normal", + "attachment-end": "normal", + "relationship-icon-image": "", + "type-color": "#000000", + "type-background-color": "#ffffff", + "type-border-color": "#000000", + "type-border-width": 0, + "type-font-size": 16, + "type-padding": 5, + "property-position": "outside", + "property-alignment": "colon", + "property-color": "#000000", + "property-font-size": 16, + "property-font-weight": "normal" + }, + "nodes": [ + { + "id": "n1", + "position": { + "x": 2055.0393057401334, + "y": 641.1074078540869 + }, + "caption": "Domain", + "labels": [], + "properties": { + "name": "d" + }, + "style": { + "node-color": "#68ccca" + } + }, + { + "id": "n2", + "position": { + "x": 1596.163360694819, + "y": 182.23146280877245 + }, + "caption": "NTAuthStore", + "labels": [], + "properties": {}, + "style": { + "node-color": "#653294", + "caption-color": "#ffffff" + } + }, + { + "id": "n3", + "position": { + "x": 1596.163360694819, + "y": 350.35614824592676 + }, + "caption": "RootCA", + "labels": [], + "properties": {}, + "style": { + "node-color": "#653294", + "caption-color": "#ffffff" + } + }, + { + "id": "n4", + "position": { + "x": 1092.7453131854052, + "y": 182.23146280877245 + }, + "caption": "EnterpriseCA", + "labels": [], + "properties": { + "name": "eca" + }, + "style": { + "node-color": "#194d33", + "caption-color": "#ffffff" + } + }, + { + "id": "n5", + "position": { + "x": 748.6949982406376, + "y": 182.23146280877245 + }, + "caption": "CertTemplate", + "labels": [], + "properties": { + "name": "ct", + "AuthenticationEnabled": "True", + "RequiresManagerApproval": "False", + "NoSecurityExtension": "True", + "SubjectAltNameRequireUPN": "True", + "SchemaVersion": "1", + "EnrolleeSuppliesSubject": "False", + "SubjectAltRequireSPN": "True" + }, + "style": { + "node-color": "#fda1ff" + } + }, + { + "id": "n6", + "position": { + "x": 748.6949982406376, + "y": 436.7832960176713 + }, + "caption": "AD principal (Victim)", + "labels": [], + "properties": { + "name": "vp" + }, + "style": { + "node-color": "#ffffff" + } + }, + { + "id": "n7", + "position": { + "x": 472.5320881490078, + "y": 641.1074078540869 + }, + "caption": "AD principal (attacker)", + "labels": [], + "properties": { + "name": "ap" + }, + "style": {} + }, + { + "id": "n8", + "position": { + "x": 129, + "y": 641.1074078540869 + }, + "caption": "", + "labels": [], + "properties": {}, + "style": { + "border-color": "#ffffff" + } + }, + { + "id": "n9", + "position": { + "x": 1596.163360694819, + "y": -4 + }, + "caption": "DC", + "labels": [], + "properties": { + "name": "dc" + }, + "style": { + "node-color": "#f44e3b" + } + } + ], + "relationships": [ + { + "id": "n0", + "fromId": "n5", + "toId": "n4", + "type": "PublishedTo", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n1", + "fromId": "n3", + "toId": "n1", + "type": "RootCAFor", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n2", + "fromId": "n6", + "toId": "n4", + "type": "Enroll", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n3", + "fromId": "n6", + "toId": "n5", + "type": "Enroll", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n4", + "fromId": "n4", + "toId": "n3", + "type": "IssuedSignedBy", + "properties": {}, + "style": { + "arrow-color": "#7b64ff" + } + }, + { + "id": "n5", + "fromId": "n2", + "toId": "n1", + "type": "NTAuthStoreFor", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n6", + "fromId": "n4", + "toId": "n2", + "type": "TrustedForNTAuth", + "properties": {}, + "style": { + "arrow-color": "#7b64ff" + } + }, + { + "id": "n7", + "fromId": "n7", + "toId": "n6", + "type": "GenericWrite", + "properties": {}, + "style": {} + }, + { + "id": "n8", + "fromId": "n7", + "toId": "n1", + "type": "ADCSESC9a", + "properties": {}, + "style": { + "arrow-color": "#7b64ff" + } + }, + { + "id": "n9", + "fromId": "n7", + "toId": "n8", + "type": "", + "properties": {}, + "style": { + "arrow-color": "#ffffff" + } + }, + { + "id": "n10", + "fromId": "n9", + "toId": "n1", + "type": "DCFor", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n11", + "fromId": "n4", + "toId": "n9", + "type": "CanAbuseWeakCertBinding", + "properties": {}, + "style": { + "arrow-color": "#7b64ff" + } + } + ] +} \ No newline at end of file diff --git a/cmd/api/src/test/integration/harnesses/esc9aharness.svg b/cmd/api/src/test/integration/harnesses/esc9aharness.svg new file mode 100644 index 0000000000..4a4f246e18 --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/esc9aharness.svg @@ -0,0 +1 @@ +PublishedToRootCAForEnrollEnrollIssuedSignedByNTAuthStoreForTrustedForNTAuthGenericWriteADCSESC9aDCForCanAbuseWeakCertBindingDomainname:dNTAuthStoreRootCAEnterpriseCAname:ecaCertTemplatename:ctAuthenticationEnabled:TrueRequiresManagerApproval:FalseNoSecurityExtension:TrueSubjectAltNameRequireUPN:TrueSchemaVersion:1EnrolleeSuppliesSubject:FalseSubjectAltRequireSPN:TrueADprincipal(Victim)name:vpADprincipal(attacker)name:apDCname:dc \ No newline at end of file From 09f278fe3fa6dfae3f21adadd220508a9c28c5f3 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Wed, 24 Jan 2024 10:47:38 -0500 Subject: [PATCH 04/36] fix: regen schema after merge --- .../integration/harnesses/esc9aharness.svg | 19 ++++++++++++++++++- .../src/test/integration/harnesses/utils.go | 16 ++++++++++++++++ packages/go/graphschema/ad/ad.go | 4 ++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/cmd/api/src/test/integration/harnesses/esc9aharness.svg b/cmd/api/src/test/integration/harnesses/esc9aharness.svg index 4a4f246e18..bd9dfa524b 100644 --- a/cmd/api/src/test/integration/harnesses/esc9aharness.svg +++ b/cmd/api/src/test/integration/harnesses/esc9aharness.svg @@ -1 +1,18 @@ -PublishedToRootCAForEnrollEnrollIssuedSignedByNTAuthStoreForTrustedForNTAuthGenericWriteADCSESC9aDCForCanAbuseWeakCertBindingDomainname:dNTAuthStoreRootCAEnterpriseCAname:ecaCertTemplatename:ctAuthenticationEnabled:TrueRequiresManagerApproval:FalseNoSecurityExtension:TrueSubjectAltNameRequireUPN:TrueSchemaVersion:1EnrolleeSuppliesSubject:FalseSubjectAltRequireSPN:TrueADprincipal(Victim)name:vpADprincipal(attacker)name:apDCname:dc \ No newline at end of file + +PublishedToRootCAForEnrollEnrollIssuedSignedByNTAuthStoreForTrustedForNTAuthGenericWriteADCSESC9aDCForCanAbuseWeakCertBindingDomainname:dNTAuthStoreRootCAEnterpriseCAname:ecaCertTemplatename:ctAuthenticationEnabled:TrueRequiresManagerApproval:FalseNoSecurityExtension:TrueSubjectAltNameRequireUPN:TrueSchemaVersion:1EnrolleeSuppliesSubject:FalseSubjectAltRequireSPN:TrueADprincipal(Victim)name:vpADprincipal(attacker)name:apDCname:dc diff --git a/cmd/api/src/test/integration/harnesses/utils.go b/cmd/api/src/test/integration/harnesses/utils.go index 7d97df58e8..a4b98b853d 100644 --- a/cmd/api/src/test/integration/harnesses/utils.go +++ b/cmd/api/src/test/integration/harnesses/utils.go @@ -1,3 +1,19 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + package harnesses import ( diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go index 0a59e8a8fc..5e2b612864 100644 --- a/packages/go/graphschema/ad/ad.go +++ b/packages/go/graphschema/ad/ad.go @@ -639,13 +639,13 @@ func Nodes() []graph.Kind { return []graph.Kind{Entity, User, Computer, Group, GPO, OU, Container, Domain, LocalGroup, LocalUser, AIACA, RootCA, EnterpriseCA, NTAuthStore, CertTemplate} } func Relationships() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, GetChanges, GetChangesAll, GetChangesInFilteredSet, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, LocalToComputer, MemberOfLocalGroup, RemoteInteractiveLogonPrivilege, SyncLAPSPassword, WriteAccountRestrictions, RootCAFor, DCFor, PublishedTo, ManageCertificates, ManageCA, DelegatedEnrollmentAgent, Enroll, HostsCAService, WritePKIEnrollmentFlag, WritePKINameFlag, NTAuthStoreFor, TrustedForNTAuth, EnterpriseCAFor, CanAbuseUPNCertMapping, CanAbuseWeakCertBinding, IssuedSignedBy, GoldenCert, EnrollOnBehalfOf, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6, ADCSESC7, ADCSESC9a} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, GetChanges, GetChangesAll, GetChangesInFilteredSet, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, LocalToComputer, MemberOfLocalGroup, RemoteInteractiveLogonPrivilege, SyncLAPSPassword, WriteAccountRestrictions, RootCAFor, DCFor, PublishedTo, ManageCertificates, ManageCA, DelegatedEnrollmentAgent, Enroll, HostsCAService, WritePKIEnrollmentFlag, WritePKINameFlag, NTAuthStoreFor, TrustedForNTAuth, EnterpriseCAFor, CanAbuseUPNCertMapping, CanAbuseWeakCertBinding, IssuedSignedBy, GoldenCert, EnrollOnBehalfOf, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6a, ADCSESC7, ADCSESC9a} } func ACLRelationships() []graph.Kind { return []graph.Kind{AllExtendedRights, ForceChangePassword, AddMember, AddAllowedToAct, GenericAll, WriteDACL, WriteOwner, GenericWrite, ReadLAPSPassword, ReadGMSAPassword, Owns, AddSelf, WriteSPN, AddKeyCredentialLink, GetChanges, GetChangesAll, GetChangesInFilteredSet, WriteAccountRestrictions, SyncLAPSPassword, DCSync, ManageCertificates, ManageCA, Enroll, WritePKIEnrollmentFlag, WritePKINameFlag} } func PathfindingRelationships() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6, ADCSESC7, DCFor, ADCSESC9a} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6a, ADCSESC7, DCFor, ADCSESC9a} } func IsACLKind(s graph.Kind) bool { for _, acl := range ACLRelationships() { From 6c22570656ab0e04f2be84daedc72aa8e9714751 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Wed, 24 Jan 2024 10:54:10 -0500 Subject: [PATCH 05/36] chore: fix small nits --- packages/go/analysis/ad/adcs.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/go/analysis/ad/adcs.go b/packages/go/analysis/ad/adcs.go index 541026688a..eb64a1537e 100644 --- a/packages/go/analysis/ad/adcs.go +++ b/packages/go/analysis/ad/adcs.go @@ -482,9 +482,7 @@ func PostADCS(ctx context.Context, db graph.Database, groupExpansions impact.Pat operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { if err := PostADCSESC9a(ctx, tx, outC, groupExpansions, innerEnterpriseCA, innerDomain, cache); err != nil { - log.Errorf("failed post processing for %s: %v", ad.ADCSESC3.String(), err) - } else { - return nil + log.Errorf("failed post processing for %s: %v", ad.ADCSESC9a.String(), err) } return nil From f20ae0dfbb1ce8be0a39b520c1f65d17e5595905 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Wed, 24 Jan 2024 11:21:07 -0500 Subject: [PATCH 06/36] chore: cleanup cert template new function --- cmd/api/src/test/integration/graph.go | 34 ++- cmd/api/src/test/integration/harnesses.go | 342 ++++++++++++++++++++-- 2 files changed, 339 insertions(+), 37 deletions(-) diff --git a/cmd/api/src/test/integration/graph.go b/cmd/api/src/test/integration/graph.go index 1effa5d13d..c657175614 100644 --- a/cmd/api/src/test/integration/graph.go +++ b/cmd/api/src/test/integration/graph.go @@ -414,23 +414,37 @@ func (s *GraphTestContext) NewActiveDirectoryRootCAWithThumbprint(name, domainSI }), ad.Entity, ad.RootCA) } -func (s *GraphTestContext) NewActiveDirectoryCertTemplate(name, domainSID string, requiresManagerApproval, authenticationEnabled, enrolleeSupplieSubject, subjectAltRequireUpn, noSecurityExtension bool, schemaVersion, authorizedSignatures int, ekus, applicationPolicies []string) *graph.Node { +func (s *GraphTestContext) NewActiveDirectoryCertTemplate(name, domainSID string, data CertTemplateData) *graph.Node { return s.NewNode(graph.AsProperties(graph.PropertyMap{ common.Name: name, common.ObjectID: must.NewUUIDv4().String(), ad.DomainSID: domainSID, - ad.RequiresManagerApproval: requiresManagerApproval, - ad.AuthenticationEnabled: authenticationEnabled, - ad.EnrolleeSuppliesSubject: enrolleeSupplieSubject, - ad.NoSecurityExtension: noSecurityExtension, - ad.SchemaVersion: float64(schemaVersion), - ad.AuthorizedSignatures: float64(authorizedSignatures), - ad.EKUs: ekus, - ad.ApplicationPolicies: applicationPolicies, - ad.SubjectAltRequireUPN: subjectAltRequireUpn, + ad.RequiresManagerApproval: data.RequiresManagerApproval, + ad.AuthenticationEnabled: data.AuthenticationEnabled, + ad.EnrolleeSuppliesSubject: data.EnrolleeSuppliesSubject, + ad.NoSecurityExtension: data.NoSecurityExtension, + ad.SchemaVersion: data.SchemaVersion, + ad.AuthorizedSignatures: data.AuthorizedSignatures, + ad.EKUs: data.EKUS, + ad.ApplicationPolicies: data.ApplicationPolicies, + ad.SubjectAltRequireUPN: data.SubjectAltRequireUPN, + ad.SubjectAltRequireSPN: data.SubjectAltRequireSPN, }), ad.Entity, ad.CertTemplate) } +type CertTemplateData struct { + RequiresManagerApproval bool + AuthenticationEnabled bool + EnrolleeSuppliesSubject bool + SubjectAltRequireUPN bool + SubjectAltRequireSPN bool + NoSecurityExtension bool + SchemaVersion float64 + AuthorizedSignatures float64 + EKUS []string + ApplicationPolicies []string +} + func (s *GraphTestContext) setupAzure() { s.Harness.AZBaseHarness.Setup(s) s.Harness.AZGroupMembership.Setup(s) diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go index 49f6fde89f..7a7823780d 100644 --- a/cmd/api/src/test/integration/harnesses.go +++ b/cmd/api/src/test/integration/harnesses.go @@ -1144,7 +1144,18 @@ func (s *ADCSESC1Harness) Setup(graphTestContext *GraphTestContext) { s.AuthStore1 = graphTestContext.NewActiveDirectoryNTAuthStore("ntauthstore 1", sid) s.EnterpriseCA1 = graphTestContext.NewActiveDirectoryEnterpriseCA("eca 1", sid) s.RootCA1 = graphTestContext.NewActiveDirectoryRootCA("rca 1", sid) - s.CertTemplate1 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 1", sid, false, true, true, false, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate1 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) s.Group11 = graphTestContext.NewActiveDirectoryGroup("group1-1", sid) s.Group12 = graphTestContext.NewActiveDirectoryGroup("group1-2", sid) s.Group13 = graphTestContext.NewActiveDirectoryGroup("group1-3", sid) @@ -1180,7 +1191,18 @@ func (s *ADCSESC1Harness) Setup(graphTestContext *GraphTestContext) { s.EnterpriseCA22 = graphTestContext.NewActiveDirectoryEnterpriseCA("eca2-2", sid) s.Group21 = graphTestContext.NewActiveDirectoryGroup("group2-1", sid) s.Group22 = graphTestContext.NewActiveDirectoryGroup("group2-2", sid) - s.CertTemplate2 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 2", sid, false, true, true, false, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate2 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) graphTestContext.NewRelationship(s.RootCA2, s.Domain2, ad.RootCAFor) graphTestContext.NewRelationship(s.AuthStore2, s.Domain2, ad.NTAuthStoreFor) @@ -1202,7 +1224,18 @@ func (s *ADCSESC1Harness) Setup(graphTestContext *GraphTestContext) { s.EnterpriseCA32 = graphTestContext.NewActiveDirectoryEnterpriseCA("eca3-2", sid) s.Group31 = graphTestContext.NewActiveDirectoryGroup("group3-1", sid) s.Group32 = graphTestContext.NewActiveDirectoryGroup("group3-2", sid) - s.CertTemplate3 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 3", sid, false, true, true, false, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate3 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 3", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) graphTestContext.NewRelationship(s.RootCA3, s.Domain3, ad.RootCAFor) graphTestContext.NewRelationship(s.AuthStore3, s.Domain3, ad.NTAuthStoreFor) @@ -1227,12 +1260,78 @@ func (s *ADCSESC1Harness) Setup(graphTestContext *GraphTestContext) { s.Group44 = graphTestContext.NewActiveDirectoryGroup("group4-4", sid) s.Group45 = graphTestContext.NewActiveDirectoryGroup("group4-5", sid) s.Group46 = graphTestContext.NewActiveDirectoryGroup("group4-6", sid) - s.CertTemplate41 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-1", sid, false, true, true, false, false, 2, 1, emptyEkus, emptyEkus) - s.CertTemplate42 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-2", sid, false, true, true, false, false, 2, 0, emptyEkus, emptyEkus) - s.CertTemplate43 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-3", sid, false, true, true, false, false, 1, 0, emptyEkus, emptyEkus) - s.CertTemplate44 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-4", sid, true, true, true, false, false, 1, 0, emptyEkus, emptyEkus) - s.CertTemplate45 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-5", sid, false, false, true, false, false, 1, 0, emptyEkus, emptyEkus) - s.CertTemplate46 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-6", sid, false, true, false, true, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate41 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 1, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate42 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate43 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-3", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate44 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-4", sid, CertTemplateData{ + RequiresManagerApproval: true, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate45 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-5", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate46 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-6", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: true, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) graphTestContext.NewRelationship(s.AuthStore4, s.Domain4, ad.NTAuthStoreFor) graphTestContext.NewRelationship(s.RootCA4, s.Domain4, ad.RootCAFor) @@ -1279,11 +1378,66 @@ func (s *EnrollOnBehalfOfHarnessTwo) Setup(gt *GraphTestContext) { s.AuthStore2 = gt.NewActiveDirectoryNTAuthStore("authstore2", sid) s.RootCA2 = gt.NewActiveDirectoryRootCA("rca2", sid) s.EnterpriseCA2 = gt.NewActiveDirectoryEnterpriseCA("eca2", sid) - s.CertTemplate21 = gt.NewActiveDirectoryCertTemplate("certtemplate2-1", sid, false, false, false, false, false, 1, 0, certRequestAgentEKU, emptyAppPolicies) - s.CertTemplate22 = gt.NewActiveDirectoryCertTemplate("certtemplate2-2", sid, false, false, false, false, false, 1, 0, []string{adAnalysis.EkuCertRequestAgent, adAnalysis.EkuAnyPurpose}, emptyAppPolicies) - s.CertTemplate23 = gt.NewActiveDirectoryCertTemplate("certtemplate2-3", sid, false, false, false, false, false, 2, 1, certRequestAgentEKU, []string{adAnalysis.EkuCertRequestAgent}) - s.CertTemplate24 = gt.NewActiveDirectoryCertTemplate("certtemplate2-4", sid, false, false, false, false, false, 2, 1, []string{}, emptyAppPolicies) - s.CertTemplate25 = gt.NewActiveDirectoryCertTemplate("certtemplate2-5", sid, false, false, false, true, false, 1, 1, []string{}, emptyAppPolicies) + s.CertTemplate21 = gt.NewActiveDirectoryCertTemplate("certtemplate2-1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: certRequestAgentEKU, + ApplicationPolicies: emptyAppPolicies, + }) + s.CertTemplate22 = gt.NewActiveDirectoryCertTemplate("certtemplate2-2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: []string{adAnalysis.EkuCertRequestAgent, adAnalysis.EkuAnyPurpose}, + ApplicationPolicies: emptyAppPolicies, + }) + s.CertTemplate23 = gt.NewActiveDirectoryCertTemplate("certtemplate2-3", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 1, + EKUS: certRequestAgentEKU, + ApplicationPolicies: []string{adAnalysis.EkuCertRequestAgent}, + }) + s.CertTemplate24 = gt.NewActiveDirectoryCertTemplate("certtemplate2-4", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 1, + EKUS: emptyAppPolicies, + ApplicationPolicies: emptyAppPolicies, + }) + s.CertTemplate25 = gt.NewActiveDirectoryCertTemplate("certtemplate2-5", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 1, + EKUS: emptyAppPolicies, + ApplicationPolicies: emptyAppPolicies, + }) gt.NewRelationship(s.AuthStore2, s.Domain2, ad.NTAuthStoreFor) gt.NewRelationship(s.RootCA2, s.Domain2, ad.RootCAFor) @@ -1315,9 +1469,42 @@ func (s *EnrollOnBehalfOfHarnessOne) Setup(gt *GraphTestContext) { s.AuthStore1 = gt.NewActiveDirectoryNTAuthStore("authstore1", sid) s.RootCA1 = gt.NewActiveDirectoryRootCA("rca1", sid) s.EnterpriseCA1 = gt.NewActiveDirectoryEnterpriseCA("eca1", sid) - s.CertTemplate11 = gt.NewActiveDirectoryCertTemplate("certtemplate1-1", sid, false, false, false, false, false, 2, 0, anyPurposeEkus, emptyAppPolicies) - s.CertTemplate12 = gt.NewActiveDirectoryCertTemplate("certtemplate1-2", sid, false, false, false, false, false, 1, 0, anyPurposeEkus, emptyAppPolicies) - s.CertTemplate13 = gt.NewActiveDirectoryCertTemplate("certtemplate1-3", sid, false, false, false, false, false, 2, 0, anyPurposeEkus, emptyAppPolicies) + s.CertTemplate11 = gt.NewActiveDirectoryCertTemplate("certtemplate1-1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 0, + EKUS: anyPurposeEkus, + ApplicationPolicies: emptyAppPolicies, + }) + s.CertTemplate12 = gt.NewActiveDirectoryCertTemplate("certtemplate1-2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: anyPurposeEkus, + ApplicationPolicies: emptyAppPolicies, + }) + s.CertTemplate13 = gt.NewActiveDirectoryCertTemplate("certtemplate1-3", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 0, + EKUS: anyPurposeEkus, + ApplicationPolicies: emptyAppPolicies, + }) gt.NewRelationship(s.AuthStore1, s.Domain1, ad.NTAuthStoreFor) gt.NewRelationship(s.RootCA1, s.Domain1, ad.RootCAFor) @@ -1558,10 +1745,54 @@ func (s *ESC3Harness1) Setup(graphTestContext *GraphTestContext) { s.User3 = graphTestContext.NewActiveDirectoryUser("User3", sid) s.Group1 = graphTestContext.NewActiveDirectoryGroup("Group1", sid) s.Group2 = graphTestContext.NewActiveDirectoryGroup("Group2", sid) - s.CertTemplate0 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate0", sid, false, true, false, true, false, 1, 0, emptyEkus, emptyEkus) - s.CertTemplate1 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate1", sid, false, false, false, false, false, 2, 0, emptyEkus, emptyEkus) - s.CertTemplate2 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate2", sid, false, true, false, true, false, 1, 0, emptyEkus, emptyEkus) - s.CertTemplate3 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate3", sid, false, false, false, false, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate0 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate0", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: true, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate1 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate2 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: true, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate3 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate3", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) s.EnterpriseCA1 = graphTestContext.NewActiveDirectoryEnterpriseCA("EnterpriseCA1", sid) s.EnterpriseCA2 = graphTestContext.NewActiveDirectoryEnterpriseCA("EnterpriseCA2", sid) s.NTAuthStore = graphTestContext.NewActiveDirectoryNTAuthStore("NTAuthStore", sid) @@ -1616,8 +1847,30 @@ func (s *ESC3Harness2) Setup(c *GraphTestContext) { s.User1 = c.NewActiveDirectoryUser("User1", sid) s.User2 = c.NewActiveDirectoryUser("User2", sid) s.Group1 = c.NewActiveDirectoryGroup("Group1", sid) - s.CertTemplate1 = c.NewActiveDirectoryCertTemplate("CertTemplate1", sid, false, true, false, false, false, 2, 0, emptyEkus, emptyEkus) - s.CertTemplate2 = c.NewActiveDirectoryCertTemplate("CertTemplate2", sid, false, true, false, true, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate1 = c.NewActiveDirectoryCertTemplate("CertTemplate1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate2 = c.NewActiveDirectoryCertTemplate("CertTemplate2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: true, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) s.EnterpriseCA1 = c.NewActiveDirectoryEnterpriseCA("EnterpriseCA1", sid) s.NTAuthStore = c.NewActiveDirectoryNTAuthStore("NTAuthStore", sid) s.RootCA = c.NewActiveDirectoryRootCA("RootCA", sid) @@ -1655,13 +1908,24 @@ type ESC9AHarness struct { func (s *ESC9AHarness) Setup(c *GraphTestContext) { sid := RandomDomainSID() + emptyEkus := make([]string, 0) s.Domain = c.NewActiveDirectoryDomain("ESC9aDomain", sid, false, true) s.NTAuthStore = c.NewActiveDirectoryNTAuthStore("NTAuthStore", sid) s.RootCA = c.NewActiveDirectoryRootCA("RootCA", sid) s.DC = c.NewActiveDirectoryComputer("DC", sid) s.EnterpriseCA = c.NewActiveDirectoryEnterpriseCA("eca", sid) - s.CertTemplate = c.NewActiveDirectoryCertTemplate("certtemplate", sid, false, true, false, true, 1, 0, make([]string, 0), make([]string, 0)) - s.CertTemplate.Properties.Set(ad.NoSecurityExtension.String(), true) + s.CertTemplate = c.NewActiveDirectoryCertTemplate("certtemplate", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: true, + SubjectAltRequireSPN: true, + NoSecurityExtension: true, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) s.CertTemplate.Properties.Set(ad.SubjectAltRequireSPN.String(), true) c.UpdateNode(s.CertTemplate) s.Victim = c.NewActiveDirectoryUser("victim", sid, false) @@ -1702,7 +1966,18 @@ func (s *ESC6aHarnessPrincipalEdges) Setup(c *GraphTestContext) { s.Group2 = c.NewActiveDirectoryGroup("Group2", sid) s.Group3 = c.NewActiveDirectoryGroup("Group3", sid) s.Group4 = c.NewActiveDirectoryGroup("Group4", sid) - s.CertTemplate1 = c.NewActiveDirectoryCertTemplate("CertTemplate1", sid, false, true, false, false, true, 1, 0, []string{}, []string{}) + s.CertTemplate1 = c.NewActiveDirectoryCertTemplate("CertTemplate1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: true, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: []string{}, + ApplicationPolicies: []string{}, + }) s.EnterpriseCA1 = c.NewActiveDirectoryEnterpriseCA("EnterpriseCA1", sid) s.NTAuthStore = c.NewActiveDirectoryNTAuthStore("NTAuthStore", sid) s.RootCA = c.NewActiveDirectoryRootCA("RootCA", sid) @@ -1749,6 +2024,19 @@ func setupHarnessFromArrowsJson(c *GraphTestContext, fileName string) { func initHarnessNodes(c *GraphTestContext, nodes []harnesses.Node, sid string) map[string]*graph.Node { nodeMap := map[string]*graph.Node{} + ctData := CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: true, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: []string{}, + ApplicationPolicies: []string{}, + } + for _, node := range nodes { if kind, ok := node.Properties["kind"]; !ok { continue @@ -1758,7 +2046,7 @@ func initHarnessNodes(c *GraphTestContext, nodes []harnesses.Node, sid string) m case ad.Group.String(): nodeMap[node.ID] = c.NewActiveDirectoryGroup(node.Caption, sid) case ad.CertTemplate.String(): - nodeMap[node.ID] = c.NewActiveDirectoryCertTemplate(node.Caption, sid, false, true, false, false, true, 1, 0, []string{}, []string{}) + nodeMap[node.ID] = c.NewActiveDirectoryCertTemplate(node.Caption, sid, ctData) case ad.EnterpriseCA.String(): nodeMap[node.ID] = c.NewActiveDirectoryEnterpriseCA(node.Caption, sid) case ad.NTAuthStore.String(): From 0b2970855f8d945d9072d8955288680d30bf0a8a Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Wed, 24 Jan 2024 11:51:56 -0500 Subject: [PATCH 07/36] chore: add missing props --- cmd/api/src/test/integration/graph.go | 50 ++++++++++++----------- cmd/api/src/test/integration/harnesses.go | 3 +- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/cmd/api/src/test/integration/graph.go b/cmd/api/src/test/integration/graph.go index c657175614..b1459ba1cf 100644 --- a/cmd/api/src/test/integration/graph.go +++ b/cmd/api/src/test/integration/graph.go @@ -416,33 +416,37 @@ func (s *GraphTestContext) NewActiveDirectoryRootCAWithThumbprint(name, domainSI func (s *GraphTestContext) NewActiveDirectoryCertTemplate(name, domainSID string, data CertTemplateData) *graph.Node { return s.NewNode(graph.AsProperties(graph.PropertyMap{ - common.Name: name, - common.ObjectID: must.NewUUIDv4().String(), - ad.DomainSID: domainSID, - ad.RequiresManagerApproval: data.RequiresManagerApproval, - ad.AuthenticationEnabled: data.AuthenticationEnabled, - ad.EnrolleeSuppliesSubject: data.EnrolleeSuppliesSubject, - ad.NoSecurityExtension: data.NoSecurityExtension, - ad.SchemaVersion: data.SchemaVersion, - ad.AuthorizedSignatures: data.AuthorizedSignatures, - ad.EKUs: data.EKUS, - ad.ApplicationPolicies: data.ApplicationPolicies, - ad.SubjectAltRequireUPN: data.SubjectAltRequireUPN, - ad.SubjectAltRequireSPN: data.SubjectAltRequireSPN, + common.Name: name, + common.ObjectID: must.NewUUIDv4().String(), + ad.DomainSID: domainSID, + ad.RequiresManagerApproval: data.RequiresManagerApproval, + ad.AuthenticationEnabled: data.AuthenticationEnabled, + ad.EnrolleeSuppliesSubject: data.EnrolleeSuppliesSubject, + ad.NoSecurityExtension: data.NoSecurityExtension, + ad.SchemaVersion: data.SchemaVersion, + ad.AuthorizedSignatures: data.AuthorizedSignatures, + ad.EKUs: data.EKUS, + ad.ApplicationPolicies: data.ApplicationPolicies, + ad.SubjectAltRequireUPN: data.SubjectAltRequireUPN, + ad.SubjectAltRequireSPN: data.SubjectAltRequireSPN, + ad.SubjectAltRequireDNS: data.SubjectAltRequireDNS, + ad.SubjectAltRequireDomainDNS: data.SubjectAltRequireDomainDNS, }), ad.Entity, ad.CertTemplate) } type CertTemplateData struct { - RequiresManagerApproval bool - AuthenticationEnabled bool - EnrolleeSuppliesSubject bool - SubjectAltRequireUPN bool - SubjectAltRequireSPN bool - NoSecurityExtension bool - SchemaVersion float64 - AuthorizedSignatures float64 - EKUS []string - ApplicationPolicies []string + RequiresManagerApproval bool + AuthenticationEnabled bool + EnrolleeSuppliesSubject bool + SubjectAltRequireUPN bool + SubjectAltRequireSPN bool + SubjectAltRequireDNS bool + SubjectAltRequireDomainDNS bool + NoSecurityExtension bool + SchemaVersion float64 + AuthorizedSignatures float64 + EKUS []string + ApplicationPolicies []string } func (s *GraphTestContext) setupAzure() { diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go index 7a7823780d..9b11946519 100644 --- a/cmd/api/src/test/integration/harnesses.go +++ b/cmd/api/src/test/integration/harnesses.go @@ -1920,14 +1920,13 @@ func (s *ESC9AHarness) Setup(c *GraphTestContext) { EnrolleeSuppliesSubject: false, SubjectAltRequireUPN: true, SubjectAltRequireSPN: true, + SubjectAltRequireDNS: false, NoSecurityExtension: true, SchemaVersion: 1, AuthorizedSignatures: 0, EKUS: emptyEkus, ApplicationPolicies: emptyEkus, }) - s.CertTemplate.Properties.Set(ad.SubjectAltRequireSPN.String(), true) - c.UpdateNode(s.CertTemplate) s.Victim = c.NewActiveDirectoryUser("victim", sid, false) s.Attacker = c.NewActiveDirectoryUser("attacker", sid, false) From 8fcd861f016b0db76f204bc0c5b6ada45d47f041 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 10:10:49 -0500 Subject: [PATCH 08/36] wip: 9a composition --- packages/go/analysis/ad/ad.go | 191 ++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index 1c0d029a56..ef8e4a1b76 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -975,3 +975,194 @@ func getGoldenCertEdgeComposition(tx graph.Transaction, edge *graph.Relationship return finalPaths, nil } } + +func adcsESC9aPath1Pattern(domainID graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + Outbound( + query.And( + query.KindIn(query.Relationship(), ad.GenericWrite, ad.GenericAll, ad.Owns, ad.WriteOwner, ad.WriteDACL), + query.KindIn(query.End(), ad.Computer, ad.User), + ), + ). + Outbound( + query.And( + query.Kind(query.Relationship(), ad.MemberOf), + query.Kind(query.End(), ad.Group), + ), + ). + Outbound( + query.And( + query.KindIn(query.Relationship(), ad.GenericAll, ad.Enroll, ad.AllExtendedRights), + query.Kind(query.End(), ad.CertTemplate), + query.Equals(query.EndProperty(ad.RequiresManagerApproval.String()), false), + query.Equals(query.EndProperty(ad.AuthenticationEnabled.String()), true), + query.Equals(query.EndProperty(ad.NoSecurityExtension.String()), true), + query.Equals(query.EndProperty(ad.EnrolleeSuppliesSubject.String()), false), + query.Or( + query.Equals(query.EndProperty(ad.SubjectAltRequireUPN.String()), true), + query.Equals(query.EndProperty(ad.SubjectAltRequireSPN.String()), true), + ), + query.Or( + query.Equals(query.EndProperty(ad.SchemaVersion.String()), 1), + query.And( + query.GreaterThan(query.EndProperty(ad.SchemaVersion.String()), 1), + query.Equals(query.EndProperty(ad.AuthorizedSignatures.String()), 0), + ), + ), + ), + ). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.PublishedTo, ad.IssuedSignedBy), + query.Kind(query.End(), ad.EnterpriseCA), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.IssuedSignedBy, ad.EnterpriseCAFor), + query.Kind(query.End(), ad.RootCA), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.RootCAFor), + query.Equals(query.EndID(), domainID), + )) +} + +func adcsESC9APath2Pattern(caNodes []graph.ID, domainId graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + Outbound(query.And( + query.Kind(query.Relationship(), ad.MemberOf), + query.Kind(query.End(), ad.Group), + )). + Outbound(query.And( + query.Kind(query.Relationship(), ad.Enroll), + query.InIDs(query.End(), caNodes...), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.TrustedForNTAuth), + query.Kind(query.End(), ad.NTAuthStore), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.NTAuthStoreFor), + query.Equals(query.EndID(), domainId), + )) +} + +func adcsESC9APath3Pattern(domainId graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.CanAbuseWeakCertBinding, ad.DCFor, ad.TrustedBy), + query.Equals(query.EndID(), domainId), + )) +} + +func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *graph.Relationship) (graph.PathSet, error) { + /* + MATCH (n {objectid:'S-1-5-21-3933516454-2894985453-2515407000-500'})-[:ADCSESC9a]->(d:Domain {objectid:'S-1-5-21-3933516454-2894985453-2515407000'}) + OPTIONAL MATCH p1 = (n)-[:GenericAll|GenericWrite|Owns|WriteOwner|WriteDacl]->(m)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor|RootCAFor*1..]->(d) + WHERE ct.requiresmanagerapproval = false + AND ct.authenticationenabled = true + AND ct.nosecurityextension = true + AND ct.enrolleesuppliessubject = false + AND (ct.subjectaltrequireupn = true OR ct.subjectaltrequirespn = true) + AND ( + (ct.schemaversion > 1 AND ct.authorizedsignatures = 0) + OR ct.schemaversion = 1 + ) + AND ( + m:Computer + OR (m:User AND ct.subjectaltrequiredns = false AND ct.subjectaltrequiredomaindns = false) + ) + OPTIONAL MATCH p2 = (m)-[:MemberOf*0..]->()-[:Enroll]->(ca)-[:TrustedForNTAuth]->(nt)-[:NTAuthStoreFor]->(d) + OPTIONAL MATCH p3 = (ca)-[:CanAbuseWeakCertBinding|DCFor|TrustedBy*1..]->(d) + RETURN p1,p2,p3 + */ + + var ( + startNode *graph.Node + + traversalInst = traversal.New(db, analysis.MaximumDatabaseParallelWorkers) + paths = graph.PathSet{} + candidateSegments = map[graph.ID][]*graph.PathSegment{} + victimCANodes = map[graph.ID][]graph.ID{} + p2CAPaths = map[graph.ID][]*graph.PathSegment{} + nodeMap = map[graph.ID]*graph.Node{} + lock = &sync.Mutex{} + ) + + if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { + if node, err := ops.FetchNode(tx, edge.StartID); err != nil { + return err + } else { + startNode = node + return nil + } + }); err != nil { + return nil, err + } + + //Fully manifest p1 + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: startNode, + Driver: adcsESC9aPath1Pattern(edge.EndID).Do(func(terminal *graph.PathSegment) error { + victimNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Depth() == 1 + }) + + if victimNode.Kinds.ContainsOneOf(ad.User) { + certTemplate := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.CertTemplate) + }) + + if !certTemplateValidForUserVictim(certTemplate) { + return nil + } + } + + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + candidateSegments[victimNode.ID] = append(candidateSegments[victimNode.ID], terminal) + nodeMap[victimNode.ID] = victimNode + victimCANodes[victimNode.ID] = append(victimCANodes[victimNode.ID], caNode.ID) + lock.Unlock() + + return nil + }), + }); err != nil { + return nil, err + } + + for victim, p1CANodes := range victimCANodes { + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: nodeMap[victim], + Driver: adcsESC9APath2Pattern(p1CANodes, edge.EndID).Do(func(terminal *graph.PathSegment) error { + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + p2CAPaths[caNode.ID] = append(p2CAPaths[caNode.ID], terminal) + lock.Unlock() + + return nil + }), + }); err != nil { + return nil, err + } + } + +} + +func certTemplateValidForUserVictim(certTemplate *graph.Node) bool { + if subjectAltRequireDNS, err := certTemplate.Properties.Get(ad.SubjectAltRequireDNS.String()).Bool(); err != nil { + return false + } else if subjectAltRequireDNS { + return false + } else if subjectAltRequireDomainDNS, err := certTemplate.Properties.Get(ad.SubjectAltRequireDomainDNS.String()).Bool(); err != nil { + return false + } else if subjectAltRequireDomainDNS { + return false + } else { + return true + } +} From 28668006e9fb0b646f663787532ae067190220ac Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 10:22:04 -0500 Subject: [PATCH 09/36] fix: treat failure to grab properties as true --- packages/go/analysis/ad/esc9.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/go/analysis/ad/esc9.go b/packages/go/analysis/ad/esc9.go index fd6790310d..44aff9e63e 100644 --- a/packages/go/analysis/ad/esc9.go +++ b/packages/go/analysis/ad/esc9.go @@ -18,6 +18,7 @@ package ad import ( "context" + "errors" "github.com/specterops/bloodhound/analysis" "github.com/specterops/bloodhound/analysis/impact" "github.com/specterops/bloodhound/dawgs/cardinality" @@ -45,14 +46,18 @@ func PostADCSESC9a(ctx context.Context, tx graph.Transaction, outC chan<- analys } else { for _, template := range publishedCertTemplates { if valid, err := isCertTemplateValidForESC9a(template); err != nil { - log.Errorf("Error checking cert template validity for template %d: %v", template.ID, err) + if !errors.Is(err, graph.ErrPropertyNotFound) { + log.Errorf("Error checking cert template validity for template %d: %v", template.ID, err) + } else { + log.Debugf("Error checking cert template validity for template %d: %v", template.ID, err) + } } else if !valid { continue } else if certTemplateControllers, ok := cache.CertTemplateControllers[template.ID]; !ok { - log.Errorf("Failed to retrieve controllers for cert template %d from cache", template.ID) + log.Debugf("Failed to retrieve controllers for cert template %d from cache", template.ID) continue } else if ecaControllers, ok := cache.EnterpriseCAEnrollers[eca.ID]; !ok { - log.Errorf("Failed to retrieve controllers for enterprise ca %d from cache", eca.ID) + log.Debugf("Failed to retrieve controllers for enterprise ca %d from cache", eca.ID) continue } else { //Expand controllers for the eca + template completely because we don't do group shortcutting here @@ -74,9 +79,11 @@ func PostADCSESC9a(ctx context.Context, tx graph.Transaction, outC chan<- analys } } else if len(userNodes) > 0 { if subjRequireDns, err := template.Properties.Get(ad.SubjectAltRequireDNS.String()).Bool(); err != nil { - log.Errorf("Failed to retrieve subjectAltRequireDNS for template %d: %v", template.ID, err) + log.Debugf("Failed to retrieve subjectAltRequireDNS for template %d: %v", template.ID, err) + victimBitmap.Xor(cardinality.NodeSetToDuplex(userNodes)) } else if subjRequireDomainDns, err := template.Properties.Get(ad.SubjectAltRequireDomainDNS.String()).Bool(); err != nil { - log.Errorf("Failed to retrieve subjectAltRequireDomainDNS for template %d: %v", template.ID, err) + log.Debugf("Failed to retrieve subjectAltRequireDomainDNS for template %d: %v", template.ID, err) + victimBitmap.Xor(cardinality.NodeSetToDuplex(userNodes)) } else if subjRequireDns || subjRequireDomainDns { //If either of these properties is true, we need to remove all these users from our victims list victimBitmap.Xor(cardinality.NodeSetToDuplex(userNodes)) From 5b95b3b0181be2ace07a1b545b1684b57a23d4d3 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 12:09:41 -0500 Subject: [PATCH 10/36] wip: esc9a composition --- packages/go/analysis/ad/ad.go | 79 ++++++++++++++++--- .../HelpTexts/ADCSESC9a/ADCSESC9a.tsx | 2 + .../HelpTexts/ADCSESC9a/Composition.tsx | 54 +++++++++++++ 3 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index ef8e4a1b76..f3ed9798c9 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -549,6 +549,12 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph. } else { pathSet = results } + } else if edge.Kind == ad.ADCSESC9a { + if results, err := GetADCSESC9aEdgeComposition(ctx, db, edge); err != nil { + return err + } else { + pathSet = results + } } return nil }) @@ -1045,11 +1051,11 @@ func adcsESC9APath2Pattern(caNodes []graph.ID, domainId graph.ID) traversal.Patt )) } -func adcsESC9APath3Pattern(domainId graph.ID) traversal.PatternContinuation { +func adcsESC9APath3Pattern(caIDs []graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + Inbound(query.And( + query.InIDs(query.StartID(), caIDs...), query.KindIn(query.Relationship(), ad.CanAbuseWeakCertBinding, ad.DCFor, ad.TrustedBy), - query.Equals(query.EndID(), domainId), )) } @@ -1077,21 +1083,27 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g var ( startNode *graph.Node - - traversalInst = traversal.New(db, analysis.MaximumDatabaseParallelWorkers) - paths = graph.PathSet{} - candidateSegments = map[graph.ID][]*graph.PathSegment{} - victimCANodes = map[graph.ID][]graph.ID{} - p2CAPaths = map[graph.ID][]*graph.PathSegment{} - nodeMap = map[graph.ID]*graph.Node{} - lock = &sync.Mutex{} + endNode *graph.Node + + traversalInst = traversal.New(db, analysis.MaximumDatabaseParallelWorkers) + paths = graph.PathSet{} + path1CandidateSegments = map[graph.ID][]*graph.PathSegment{} + victimCANodes = map[graph.ID][]graph.ID{} + path2CandidateSegments = map[graph.ID][]*graph.PathSegment{} + path3CandidateSegments = map[graph.ID][]*graph.PathSegment{} + p2canodes = make([]graph.ID, 0) + nodeMap = map[graph.ID]*graph.Node{} + lock = &sync.Mutex{} ) if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { if node, err := ops.FetchNode(tx, edge.StartID); err != nil { return err + } else if eNode, err := ops.FetchNode(tx, edge.EndID); err != nil { + return err } else { startNode = node + endNode = eNode return nil } }); err != nil { @@ -1121,7 +1133,7 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g }) lock.Lock() - candidateSegments[victimNode.ID] = append(candidateSegments[victimNode.ID], terminal) + path1CandidateSegments[victimNode.ID] = append(path1CandidateSegments[victimNode.ID], terminal) nodeMap[victimNode.ID] = victimNode victimCANodes[victimNode.ID] = append(victimCANodes[victimNode.ID], caNode.ID) lock.Unlock() @@ -1141,7 +1153,8 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g }) lock.Lock() - p2CAPaths[caNode.ID] = append(p2CAPaths[caNode.ID], terminal) + path2CandidateSegments[caNode.ID] = append(path2CandidateSegments[caNode.ID], terminal) + p2canodes = append(p2canodes, caNode.ID) lock.Unlock() return nil @@ -1151,6 +1164,46 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g } } + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: endNode, + Driver: adcsESC9APath3Pattern(p2canodes).Do(func(terminal *graph.PathSegment) error { + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) + lock.Unlock() + return nil + }), + }); err != nil { + return nil, err + } + + for _, p1paths := range path1CandidateSegments { + for _, p1path := range p1paths { + caNode := p1path.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + if p2segments, ok := path2CandidateSegments[caNode.ID]; !ok { + continue + } else if p3segments, ok := path3CandidateSegments[caNode.ID]; !ok { + continue + } else { + paths.AddPath(p1path.Path()) + for _, p2 := range p2segments { + paths.AddPath(p2.Path()) + } + + for _, p3 := range p3segments { + paths.AddPath(p3.Path()) + } + } + } + } + + return paths, nil } func certTemplateValidForUserVictim(certTemplate *graph.Node) bool { diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx index 9de2b72c5f..7dcde37b44 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx @@ -19,6 +19,7 @@ import WindowsAbuse from './WindowsAbuse'; import LinuxAbuse from './LinuxAbuse'; import Opsec from './Opsec'; import References from './References'; +import Composition from "./Composition"; const ADCSESC9a = { general: General, @@ -26,6 +27,7 @@ const ADCSESC9a = { linuxAbuse: LinuxAbuse, opsec: Opsec, references: References, + composition: Composition }; export default ADCSESC9a; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx new file mode 100644 index 0000000000..4a24ff5eab --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx @@ -0,0 +1,54 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { FC } from 'react'; +import { Alert, Box, Skeleton, Typography } from '@mui/material'; +import { apiClient } from '../../../utils/api'; +import { EdgeInfoProps } from '..'; +import { useQuery } from 'react-query'; +import VirtualizedNodeList, { VirtualizedNodeListItem } from '../../VirtualizedNodeList'; + +const Composition: FC = ({ sourceDBId, targetDBId, edgeName }) => { + const { data, isLoading, isError } = useQuery(['edgeComposition', sourceDBId, targetDBId, edgeName], ({ signal }) => + apiClient.getEdgeComposition(sourceDBId!, targetDBId!, edgeName!).then((result) => result.data) + ); + + const nodesArray: VirtualizedNodeListItem[] = Object.values(data?.data.nodes || {}).map((node) => ({ + name: node.label, + objectId: node.objectId, + kind: node.kind, + })); + + return ( + <> + + The relationship represents the effective outcome of the configuration and relationships between several + different objects. All objects involved in the creation of this relationship are listed here: + + + {isLoading ? ( + + ) : isError ? ( + Couldn't load edge composition + ) : ( + + )} + + + ); +}; + +export default Composition; From 53283cefc8842c2bfeb8e92079e2d3c4a069ecbc Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 14:23:23 -0500 Subject: [PATCH 11/36] wip: esc9a composition --- packages/go/analysis/ad/ad.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index f3ed9798c9..a8b6067521 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -1053,9 +1053,12 @@ func adcsESC9APath2Pattern(caNodes []graph.ID, domainId graph.ID) traversal.Patt func adcsESC9APath3Pattern(caIDs []graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). + Inbound( + query.KindIn(query.Relationship(), ad.DCFor, ad.TrustedBy), + ). Inbound(query.And( + query.Kind(query.Relationship(), ad.CanAbuseWeakCertBinding), query.InIDs(query.StartID(), caIDs...), - query.KindIn(query.Relationship(), ad.CanAbuseWeakCertBinding, ad.DCFor, ad.TrustedBy), )) } @@ -1118,6 +1121,8 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Depth() == 1 }) + graph.FormatPathSegment(terminal) + if victimNode.Kinds.ContainsOneOf(ad.User) { certTemplate := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Node.Kinds.ContainsOneOf(ad.CertTemplate) @@ -1128,6 +1133,8 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g } } + graph.FormatPathSegment(terminal) + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) @@ -1152,8 +1159,11 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) + graph.FormatPathSegment(terminal) + lock.Lock() path2CandidateSegments[caNode.ID] = append(path2CandidateSegments[caNode.ID], terminal) + log.Infof("added ca node %d", caNode.ID) p2canodes = append(p2canodes, caNode.ID) lock.Unlock() @@ -1171,6 +1181,12 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) + log.Infof(graph.FormatPathSegment(terminal)) + + if caNode == nil { + return nil + } + lock.Lock() path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) lock.Unlock() From b4f1d4514f00b62b0bacf1311adf128406a02cfd Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 25 Jan 2024 12:33:09 -0800 Subject: [PATCH 12/36] feat+chore: add depth controls to dawgs patterns --- packages/go/dawgs/traversal/traversal.go | 86 ++++++++++++++++-------- 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go index 3ec0b72b44..96da8aac9d 100644 --- a/packages/go/dawgs/traversal/traversal.go +++ b/packages/go/dawgs/traversal/traversal.go @@ -49,7 +49,9 @@ type PatternMatchDelegate = func(terminal *graph.PathSegment) error // The return value of the Do(...) function may be passed directly to a Traversal via a Plan as the Plan.Driver field. type PatternContinuation interface { Outbound(criteria ...graph.Criteria) PatternContinuation + OutboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation Inbound(criteria ...graph.Criteria) PatternContinuation + InboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation Do(delegate PatternMatchDelegate) Driver } @@ -57,6 +59,8 @@ type PatternContinuation interface { type expansion struct { criteria []graph.Criteria direction graph.Direction + minDepth int + maxDepth int } func (s expansion) PrepareCriteria(segment *graph.PathSegment) (graph.Criteria, error) { @@ -84,6 +88,7 @@ func (s expansion) PrepareCriteria(segment *graph.PathSegment) (graph.Criteria, type patternTag struct { patternIdx int + depth int } func popSegmentPatternTag(segment *graph.PathSegment) *patternTag { @@ -95,6 +100,7 @@ func popSegmentPatternTag(segment *graph.PathSegment) *patternTag { } else { tag = &patternTag{ patternIdx: 0, + depth: 0, } } @@ -112,26 +118,42 @@ func (s *pattern) Do(delegate PatternMatchDelegate) Driver { return s.Driver } -// Outbound specifies the next outbound expansion step for this pattern. -func (s *pattern) Outbound(criteria ...graph.Criteria) PatternContinuation { +// OutboundWithDepth specifies the next outbound expansion step for this pattern with depth parameters. +func (s *pattern) OutboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation { s.expansions = append(s.expansions, expansion{ criteria: criteria, direction: graph.DirectionOutbound, + minDepth: min, + maxDepth: max, }) return s } -// Inbound specifies the next inbound expansion step for this pattern. -func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation { +// Outbound specifies the next outbound expansion step for this pattern. By default, this expansion will use a minimum +// depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely. +func (s *pattern) Outbound(criteria ...graph.Criteria) PatternContinuation { + return s.OutboundWithDepth(1, 0, criteria...) +} + +// InboundWithDepth specifies the next inbound expansion step for this pattern with depth parameters. +func (s *pattern) InboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation { s.expansions = append(s.expansions, expansion{ criteria: criteria, direction: graph.DirectionInbound, + minDepth: max, + maxDepth: min, }) return s } +// Inbound specifies the next inbound expansion step for this pattern. By default, this expansion will use a minimum +// depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely. +func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation { + return s.InboundWithDepth(1, 0) +} + // NewPattern returns a new PatternContinuation for building a new pattern. func NewPattern() PatternContinuation { return &pattern{} @@ -152,9 +174,9 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra for next := range cursor.Chan() { nextSegment := segment.Descend(next.Node, next.Relationship) nextSegment.Tag = &patternTag{ - // Use the tag's patternIdx here since this is the reference that will see the increment when - // the current expansion is exhausted + // Use the tag's patternIdx and depth since this is a continuation of the expansions patternIdx: tag.patternIdx, + depth: tag.depth + 1, } nextSegments = append(nextSegments, nextSegment) @@ -168,34 +190,42 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra if fetchDirection, err := currentExpansion.direction.Reverse(); err != nil { return nil, err } else { - // Perform the current expansion. - if criteria, err := currentExpansion.PrepareCriteria(segment); err != nil { - return nil, err - } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil { - return nil, err - } - - // No further expansions means this pattern segment is complete. Increment the pattern index to select the - // next pattern expansion. - tag.patternIdx++ - - // Perform the next expansion if there is one. - if tag.patternIdx < len(s.expansions) { - nextExpansion := s.expansions[tag.patternIdx] - - // Expand the next segments - if criteria, err := nextExpansion.PrepareCriteria(segment); err != nil { + // If no max depth was set or if a max depth was set expand the current step further + if currentExpansion.maxDepth == 0 || tag.depth < currentExpansion.maxDepth { + // Perform the current expansion. + if criteria, err := currentExpansion.PrepareCriteria(segment); err != nil { return nil, err } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil { return nil, err } - } else if len(nextSegments) == 0 { - // If there are no expanded segments and there are no remaining expansions, this is a terminal segment. - // Hand it off to the delegate and handle any returned error. - if err := s.delegate(segment); err != nil { - return nil, err + } + + // Check first if this current segment was fetched using the current expansion (i.e. non-optional) + if tag.depth > 0 && currentExpansion.minDepth == 0 || tag.depth >= currentExpansion.minDepth { + // No further expansions means this pattern segment is complete. Increment the pattern index to select the + // next pattern expansion. + tag.patternIdx++ + + // Perform the next expansion if there is one. + if tag.patternIdx < len(s.expansions) { + nextExpansion := s.expansions[tag.patternIdx] + + // Expand the next segments + if criteria, err := nextExpansion.PrepareCriteria(segment); err != nil { + return nil, err + } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil { + return nil, err + } + } else if len(nextSegments) == 0 { + // If there are no expanded segments and there are no remaining expansions, this is a terminal segment. + // Hand it off to the delegate and handle any returned error. + if err := s.delegate(segment); err != nil { + return nil, err + } } } + + // If the above condition does not match then this current expansion is non-terminal and non-continuable } // Return any collected segments From fdb9614520c1efd9bfb96b38ce045f4545e405ed Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 16:00:40 -0500 Subject: [PATCH 13/36] wip: esc9a composition --- packages/go/analysis/ad/ad.go | 47 ++++++++++++++++------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index a8b6067521..997170e7ac 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -984,13 +984,15 @@ func getGoldenCertEdgeComposition(tx graph.Transaction, edge *graph.Relationship func adcsESC9aPath1Pattern(domainID graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound( + OutboundWithDepth( + 1, 1, query.And( query.KindIn(query.Relationship(), ad.GenericWrite, ad.GenericAll, ad.Owns, ad.WriteOwner, ad.WriteDACL), query.KindIn(query.End(), ad.Computer, ad.User), ), ). - Outbound( + OutboundWithDepth( + 0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), @@ -1117,12 +1119,11 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ Root: startNode, Driver: adcsESC9aPath1Pattern(edge.EndID).Do(func(terminal *graph.PathSegment) error { + log.Infof("Segment: %v", graph.FormatPathSegment(terminal)) victimNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Depth() == 1 }) - graph.FormatPathSegment(terminal) - if victimNode.Kinds.ContainsOneOf(ad.User) { certTemplate := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Node.Kinds.ContainsOneOf(ad.CertTemplate) @@ -1133,8 +1134,6 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g } } - graph.FormatPathSegment(terminal) - caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) @@ -1159,8 +1158,6 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) - graph.FormatPathSegment(terminal) - lock.Lock() path2CandidateSegments[caNode.ID] = append(path2CandidateSegments[caNode.ID], terminal) log.Infof("added ca node %d", caNode.ID) @@ -1174,26 +1171,26 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g } } - if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ - Root: endNode, - Driver: adcsESC9APath3Pattern(p2canodes).Do(func(terminal *graph.PathSegment) error { - caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { - return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) - }) + if len(p2canodes) > 0 { + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: endNode, + Driver: adcsESC9APath3Pattern(p2canodes).Do(func(terminal *graph.PathSegment) error { + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) - log.Infof(graph.FormatPathSegment(terminal)) + if caNode == nil { + return nil + } - if caNode == nil { + lock.Lock() + path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) + lock.Unlock() return nil - } - - lock.Lock() - path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) - lock.Unlock() - return nil - }), - }); err != nil { - return nil, err + }), + }); err != nil { + return nil, err + } } for _, p1paths := range path1CandidateSegments { From 9a1c50ccb2abc1ed82a3e73564f29e23751b1694 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 25 Jan 2024 13:20:06 -0800 Subject: [PATCH 14/36] fix: do not drop the current segment if the next pattern is optional --- packages/go/dawgs/traversal/traversal.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go index 96da8aac9d..0dfc2dc050 100644 --- a/packages/go/dawgs/traversal/traversal.go +++ b/packages/go/dawgs/traversal/traversal.go @@ -203,8 +203,10 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra // Check first if this current segment was fetched using the current expansion (i.e. non-optional) if tag.depth > 0 && currentExpansion.minDepth == 0 || tag.depth >= currentExpansion.minDepth { // No further expansions means this pattern segment is complete. Increment the pattern index to select the - // next pattern expansion. + // next pattern expansion. Additionally, set the depth back to zero for the tag since we are leaving the + // current expansion. tag.patternIdx++ + tag.depth = 0 // Perform the next expansion if there is one. if tag.patternIdx < len(s.expansions) { @@ -216,6 +218,13 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil { return nil, err } + + // If the next expansion is optional, make sure to preserve the current traversal branch + if nextExpansion.minDepth == 0 { + // Reattach the tag to the segment before adding it to the returned segments for the next expansion + segment.Tag = tag + nextSegments = append(nextSegments, segment) + } } else if len(nextSegments) == 0 { // If there are no expanded segments and there are no remaining expansions, this is a terminal segment. // Hand it off to the delegate and handle any returned error. @@ -301,6 +310,8 @@ func (s Traversal) BreadthFirst(ctx context.Context, plan Plan) error { if nextDescent, ok := channels.Receive(traversalCtx, segmentReaderC); !ok { return nil } else if pathTreeSize := pathTree.SizeOf(); pathTreeSize < tx.TraversalMemoryLimit() { + log.Infof("%s", graph.FormatPathSegment(nextDescent)) + // Traverse the descending relationships of the current segment if descendingSegments, err := plan.Driver(traversalCtx, tx, nextDescent); err != nil { return err From 4317b72337f684682a7d8333144ade3f2c0c95f1 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 16:54:23 -0500 Subject: [PATCH 15/36] wip: esc9a composition --- cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx index bc0e4217bb..2f46bf7109 100644 --- a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx +++ b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx @@ -73,7 +73,7 @@ const EdgeInfoContent: FC<{ selectedEdge: NonNullable }> = ({ sele const sendOnChange = (selectedEdge.name === 'GoldenCert' || selectedEdge.name === 'ADCSESC1' || - selectedEdge.name === 'ADCSESC3') && + selectedEdge.name === 'ADCSESC3' || selectedEdge.name === 'ADCSESC9a') && section[0] === 'composition'; return ( From 2947746664d6e15f37c20c096e4feaa6aca78af5 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 16:56:37 -0500 Subject: [PATCH 16/36] fix: update other continuations to respect depth correctly --- packages/go/analysis/ad/ad.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index 997170e7ac..dbf70fd41c 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -562,7 +562,7 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph. func ADCSESC3Path1Pattern(domainId graph.ID, enterpriseCAs cardinality.Duplex[uint32]) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -597,7 +597,7 @@ func ADCSESC3Path1Pattern(domainId graph.ID, enterpriseCAs cardinality.Duplex[ui func ADCSESC3Path2Pattern(domainId graph.ID, enterpriseCAs, candidateTemplates cardinality.Duplex[uint32]) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -624,7 +624,7 @@ func ADCSESC3Path2Pattern(domainId graph.ID, enterpriseCAs, candidateTemplates c func ADCSESC3Path3Pattern() traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -813,7 +813,7 @@ func getDelegatedEnrollmentAgentPath(ctx context.Context, startNode, certTemplat func ADCSESC1Path1Pattern(domainID graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -852,7 +852,7 @@ func ADCSESC1Path1Pattern(domainID graph.ID) traversal.PatternContinuation { func ADCSESC1Path2Pattern(domainID graph.ID, enterpriseCAs cardinality.Duplex[uint32]) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -1035,7 +1035,7 @@ func adcsESC9aPath1Pattern(domainID graph.ID) traversal.PatternContinuation { func adcsESC9APath2Pattern(caNodes []graph.ID, domainId graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). From 4e99b132083132cb483246fa30dd927aee5c5b18 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 17:19:04 -0500 Subject: [PATCH 17/36] wip: edge comp --- packages/go/analysis/ad/ad.go | 6 +----- packages/go/dawgs/traversal/traversal.go | 4 +--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index dbf70fd41c..5ca999b06c 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -1119,7 +1119,6 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ Root: startNode, Driver: adcsESC9aPath1Pattern(edge.EndID).Do(func(terminal *graph.PathSegment) error { - log.Infof("Segment: %v", graph.FormatPathSegment(terminal)) victimNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Depth() == 1 }) @@ -1160,7 +1159,6 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g lock.Lock() path2CandidateSegments[caNode.ID] = append(path2CandidateSegments[caNode.ID], terminal) - log.Infof("added ca node %d", caNode.ID) p2canodes = append(p2canodes, caNode.ID) lock.Unlock() @@ -1179,9 +1177,7 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) - if caNode == nil { - return nil - } + log.Infof("Segment: %v", graph.FormatPathSegment(terminal)) lock.Lock() path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go index 0dfc2dc050..13ea8d736c 100644 --- a/packages/go/dawgs/traversal/traversal.go +++ b/packages/go/dawgs/traversal/traversal.go @@ -151,7 +151,7 @@ func (s *pattern) InboundWithDepth(min, max int, criteria ...graph.Criteria) Pat // Inbound specifies the next inbound expansion step for this pattern. By default, this expansion will use a minimum // depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely. func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation { - return s.InboundWithDepth(1, 0) + return s.InboundWithDepth(1, 0, criteria...) } // NewPattern returns a new PatternContinuation for building a new pattern. @@ -310,8 +310,6 @@ func (s Traversal) BreadthFirst(ctx context.Context, plan Plan) error { if nextDescent, ok := channels.Receive(traversalCtx, segmentReaderC); !ok { return nil } else if pathTreeSize := pathTree.SizeOf(); pathTreeSize < tx.TraversalMemoryLimit() { - log.Infof("%s", graph.FormatPathSegment(nextDescent)) - // Traverse the descending relationships of the current segment if descendingSegments, err := plan.Driver(traversalCtx, tx, nextDescent); err != nil { return err From 28c98a61cffdc42cc8bea5d493718184f6668121 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 17:35:51 -0500 Subject: [PATCH 18/36] fix: swap --- packages/go/dawgs/traversal/traversal.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go index 13ea8d736c..4fa7213d4e 100644 --- a/packages/go/dawgs/traversal/traversal.go +++ b/packages/go/dawgs/traversal/traversal.go @@ -141,8 +141,8 @@ func (s *pattern) InboundWithDepth(min, max int, criteria ...graph.Criteria) Pat s.expansions = append(s.expansions, expansion{ criteria: criteria, direction: graph.DirectionInbound, - minDepth: max, - maxDepth: min, + minDepth: min, + maxDepth: max, }) return s From 554390f7c013e7f9aad15346778eabaa76b8924d Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 17:47:13 -0500 Subject: [PATCH 19/36] chore: remove unnecessary logs --- packages/go/analysis/ad/ad.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index 5ca999b06c..cdc9dadeab 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -937,8 +937,6 @@ func GetADCSESC1EdgeComposition(ctx context.Context, db graph.Database, edge *gr // Render paths from the segments return paths, path1EnterpriseCAs.Each(func(value uint32) (bool, error) { for _, segment := range candidateSegments[graph.ID(value)] { - log.Infof("Found ESC1 Path: %s", graph.FormatPathSegment(segment)) - paths.AddPath(segment.Path()) } @@ -1177,8 +1175,6 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) - log.Infof("Segment: %v", graph.FormatPathSegment(terminal)) - lock.Lock() path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) lock.Unlock() From c9385a99ad548014a827a09e72d33744e22c97e0 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Tue, 23 Jan 2024 15:39:57 -0500 Subject: [PATCH 20/36] feat: esc9a post --- packages/go/analysis/ad/adcs.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/go/analysis/ad/adcs.go b/packages/go/analysis/ad/adcs.go index eb64a1537e..f656aa9d1d 100644 --- a/packages/go/analysis/ad/adcs.go +++ b/packages/go/analysis/ad/adcs.go @@ -487,6 +487,16 @@ func PostADCS(ctx context.Context, db graph.Database, groupExpansions impact.Pat return nil }) + + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + if err := PostADCSESC9a(ctx, tx, outC, groupExpansions, innerEnterpriseCA, innerDomain, cache); err != nil { + log.Errorf("failed post processing for %s: %v", ad.ADCSESC3.String(), err) + } else { + return nil + } + + return nil + }) } } From 13923d277d708636ddcd468b22739066c381209b Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Tue, 23 Jan 2024 16:37:09 -0500 Subject: [PATCH 21/36] test: add esc9 test --- cmd/api/src/test/integration/harnesses.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go index 9b11946519..f01140325d 100644 --- a/cmd/api/src/test/integration/harnesses.go +++ b/cmd/api/src/test/integration/harnesses.go @@ -19,19 +19,15 @@ package integration import ( "fmt" "math/rand" - "strconv" - "strings" "time" "github.com/gofrs/uuid" - "github.com/specterops/bloodhound/analysis" adAnalysis "github.com/specterops/bloodhound/analysis/ad" "github.com/specterops/bloodhound/dawgs/graph" "github.com/specterops/bloodhound/graphschema/ad" "github.com/specterops/bloodhound/graphschema/azure" "github.com/specterops/bloodhound/graphschema/common" "github.com/specterops/bloodhound/src/test" - "github.com/specterops/bloodhound/src/test/integration/harnesses" ) func RandomObjectID(t test.Controller) string { From 032f89c2a714b2ba7bc4e5dcff85355f4093bb31 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Wed, 24 Jan 2024 10:54:10 -0500 Subject: [PATCH 22/36] chore: fix small nits --- packages/go/analysis/ad/adcs.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/go/analysis/ad/adcs.go b/packages/go/analysis/ad/adcs.go index f656aa9d1d..eb64a1537e 100644 --- a/packages/go/analysis/ad/adcs.go +++ b/packages/go/analysis/ad/adcs.go @@ -487,16 +487,6 @@ func PostADCS(ctx context.Context, db graph.Database, groupExpansions impact.Pat return nil }) - - operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { - if err := PostADCSESC9a(ctx, tx, outC, groupExpansions, innerEnterpriseCA, innerDomain, cache); err != nil { - log.Errorf("failed post processing for %s: %v", ad.ADCSESC3.String(), err) - } else { - return nil - } - - return nil - }) } } From c52df5c01fa8bc153285d5ef3e8af10e90f927a9 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 10:10:49 -0500 Subject: [PATCH 23/36] wip: 9a composition --- packages/go/analysis/ad/ad.go | 191 ++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index 1c0d029a56..ef8e4a1b76 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -975,3 +975,194 @@ func getGoldenCertEdgeComposition(tx graph.Transaction, edge *graph.Relationship return finalPaths, nil } } + +func adcsESC9aPath1Pattern(domainID graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + Outbound( + query.And( + query.KindIn(query.Relationship(), ad.GenericWrite, ad.GenericAll, ad.Owns, ad.WriteOwner, ad.WriteDACL), + query.KindIn(query.End(), ad.Computer, ad.User), + ), + ). + Outbound( + query.And( + query.Kind(query.Relationship(), ad.MemberOf), + query.Kind(query.End(), ad.Group), + ), + ). + Outbound( + query.And( + query.KindIn(query.Relationship(), ad.GenericAll, ad.Enroll, ad.AllExtendedRights), + query.Kind(query.End(), ad.CertTemplate), + query.Equals(query.EndProperty(ad.RequiresManagerApproval.String()), false), + query.Equals(query.EndProperty(ad.AuthenticationEnabled.String()), true), + query.Equals(query.EndProperty(ad.NoSecurityExtension.String()), true), + query.Equals(query.EndProperty(ad.EnrolleeSuppliesSubject.String()), false), + query.Or( + query.Equals(query.EndProperty(ad.SubjectAltRequireUPN.String()), true), + query.Equals(query.EndProperty(ad.SubjectAltRequireSPN.String()), true), + ), + query.Or( + query.Equals(query.EndProperty(ad.SchemaVersion.String()), 1), + query.And( + query.GreaterThan(query.EndProperty(ad.SchemaVersion.String()), 1), + query.Equals(query.EndProperty(ad.AuthorizedSignatures.String()), 0), + ), + ), + ), + ). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.PublishedTo, ad.IssuedSignedBy), + query.Kind(query.End(), ad.EnterpriseCA), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.IssuedSignedBy, ad.EnterpriseCAFor), + query.Kind(query.End(), ad.RootCA), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.RootCAFor), + query.Equals(query.EndID(), domainID), + )) +} + +func adcsESC9APath2Pattern(caNodes []graph.ID, domainId graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + Outbound(query.And( + query.Kind(query.Relationship(), ad.MemberOf), + query.Kind(query.End(), ad.Group), + )). + Outbound(query.And( + query.Kind(query.Relationship(), ad.Enroll), + query.InIDs(query.End(), caNodes...), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.TrustedForNTAuth), + query.Kind(query.End(), ad.NTAuthStore), + )). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.NTAuthStoreFor), + query.Equals(query.EndID(), domainId), + )) +} + +func adcsESC9APath3Pattern(domainId graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.CanAbuseWeakCertBinding, ad.DCFor, ad.TrustedBy), + query.Equals(query.EndID(), domainId), + )) +} + +func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *graph.Relationship) (graph.PathSet, error) { + /* + MATCH (n {objectid:'S-1-5-21-3933516454-2894985453-2515407000-500'})-[:ADCSESC9a]->(d:Domain {objectid:'S-1-5-21-3933516454-2894985453-2515407000'}) + OPTIONAL MATCH p1 = (n)-[:GenericAll|GenericWrite|Owns|WriteOwner|WriteDacl]->(m)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor|RootCAFor*1..]->(d) + WHERE ct.requiresmanagerapproval = false + AND ct.authenticationenabled = true + AND ct.nosecurityextension = true + AND ct.enrolleesuppliessubject = false + AND (ct.subjectaltrequireupn = true OR ct.subjectaltrequirespn = true) + AND ( + (ct.schemaversion > 1 AND ct.authorizedsignatures = 0) + OR ct.schemaversion = 1 + ) + AND ( + m:Computer + OR (m:User AND ct.subjectaltrequiredns = false AND ct.subjectaltrequiredomaindns = false) + ) + OPTIONAL MATCH p2 = (m)-[:MemberOf*0..]->()-[:Enroll]->(ca)-[:TrustedForNTAuth]->(nt)-[:NTAuthStoreFor]->(d) + OPTIONAL MATCH p3 = (ca)-[:CanAbuseWeakCertBinding|DCFor|TrustedBy*1..]->(d) + RETURN p1,p2,p3 + */ + + var ( + startNode *graph.Node + + traversalInst = traversal.New(db, analysis.MaximumDatabaseParallelWorkers) + paths = graph.PathSet{} + candidateSegments = map[graph.ID][]*graph.PathSegment{} + victimCANodes = map[graph.ID][]graph.ID{} + p2CAPaths = map[graph.ID][]*graph.PathSegment{} + nodeMap = map[graph.ID]*graph.Node{} + lock = &sync.Mutex{} + ) + + if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { + if node, err := ops.FetchNode(tx, edge.StartID); err != nil { + return err + } else { + startNode = node + return nil + } + }); err != nil { + return nil, err + } + + //Fully manifest p1 + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: startNode, + Driver: adcsESC9aPath1Pattern(edge.EndID).Do(func(terminal *graph.PathSegment) error { + victimNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Depth() == 1 + }) + + if victimNode.Kinds.ContainsOneOf(ad.User) { + certTemplate := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.CertTemplate) + }) + + if !certTemplateValidForUserVictim(certTemplate) { + return nil + } + } + + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + candidateSegments[victimNode.ID] = append(candidateSegments[victimNode.ID], terminal) + nodeMap[victimNode.ID] = victimNode + victimCANodes[victimNode.ID] = append(victimCANodes[victimNode.ID], caNode.ID) + lock.Unlock() + + return nil + }), + }); err != nil { + return nil, err + } + + for victim, p1CANodes := range victimCANodes { + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: nodeMap[victim], + Driver: adcsESC9APath2Pattern(p1CANodes, edge.EndID).Do(func(terminal *graph.PathSegment) error { + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + p2CAPaths[caNode.ID] = append(p2CAPaths[caNode.ID], terminal) + lock.Unlock() + + return nil + }), + }); err != nil { + return nil, err + } + } + +} + +func certTemplateValidForUserVictim(certTemplate *graph.Node) bool { + if subjectAltRequireDNS, err := certTemplate.Properties.Get(ad.SubjectAltRequireDNS.String()).Bool(); err != nil { + return false + } else if subjectAltRequireDNS { + return false + } else if subjectAltRequireDomainDNS, err := certTemplate.Properties.Get(ad.SubjectAltRequireDomainDNS.String()).Bool(); err != nil { + return false + } else if subjectAltRequireDomainDNS { + return false + } else { + return true + } +} From 869762fd8b555af69ad3c0cfcb09d0ccafe2088a Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 12:09:41 -0500 Subject: [PATCH 24/36] wip: esc9a composition --- packages/go/analysis/ad/ad.go | 79 ++++++++++++++++--- .../HelpTexts/ADCSESC9a/ADCSESC9a.tsx | 2 + .../HelpTexts/ADCSESC9a/Composition.tsx | 54 +++++++++++++ 3 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index ef8e4a1b76..f3ed9798c9 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -549,6 +549,12 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph. } else { pathSet = results } + } else if edge.Kind == ad.ADCSESC9a { + if results, err := GetADCSESC9aEdgeComposition(ctx, db, edge); err != nil { + return err + } else { + pathSet = results + } } return nil }) @@ -1045,11 +1051,11 @@ func adcsESC9APath2Pattern(caNodes []graph.ID, domainId graph.ID) traversal.Patt )) } -func adcsESC9APath3Pattern(domainId graph.ID) traversal.PatternContinuation { +func adcsESC9APath3Pattern(caIDs []graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + Inbound(query.And( + query.InIDs(query.StartID(), caIDs...), query.KindIn(query.Relationship(), ad.CanAbuseWeakCertBinding, ad.DCFor, ad.TrustedBy), - query.Equals(query.EndID(), domainId), )) } @@ -1077,21 +1083,27 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g var ( startNode *graph.Node - - traversalInst = traversal.New(db, analysis.MaximumDatabaseParallelWorkers) - paths = graph.PathSet{} - candidateSegments = map[graph.ID][]*graph.PathSegment{} - victimCANodes = map[graph.ID][]graph.ID{} - p2CAPaths = map[graph.ID][]*graph.PathSegment{} - nodeMap = map[graph.ID]*graph.Node{} - lock = &sync.Mutex{} + endNode *graph.Node + + traversalInst = traversal.New(db, analysis.MaximumDatabaseParallelWorkers) + paths = graph.PathSet{} + path1CandidateSegments = map[graph.ID][]*graph.PathSegment{} + victimCANodes = map[graph.ID][]graph.ID{} + path2CandidateSegments = map[graph.ID][]*graph.PathSegment{} + path3CandidateSegments = map[graph.ID][]*graph.PathSegment{} + p2canodes = make([]graph.ID, 0) + nodeMap = map[graph.ID]*graph.Node{} + lock = &sync.Mutex{} ) if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { if node, err := ops.FetchNode(tx, edge.StartID); err != nil { return err + } else if eNode, err := ops.FetchNode(tx, edge.EndID); err != nil { + return err } else { startNode = node + endNode = eNode return nil } }); err != nil { @@ -1121,7 +1133,7 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g }) lock.Lock() - candidateSegments[victimNode.ID] = append(candidateSegments[victimNode.ID], terminal) + path1CandidateSegments[victimNode.ID] = append(path1CandidateSegments[victimNode.ID], terminal) nodeMap[victimNode.ID] = victimNode victimCANodes[victimNode.ID] = append(victimCANodes[victimNode.ID], caNode.ID) lock.Unlock() @@ -1141,7 +1153,8 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g }) lock.Lock() - p2CAPaths[caNode.ID] = append(p2CAPaths[caNode.ID], terminal) + path2CandidateSegments[caNode.ID] = append(path2CandidateSegments[caNode.ID], terminal) + p2canodes = append(p2canodes, caNode.ID) lock.Unlock() return nil @@ -1151,6 +1164,46 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g } } + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: endNode, + Driver: adcsESC9APath3Pattern(p2canodes).Do(func(terminal *graph.PathSegment) error { + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) + lock.Unlock() + return nil + }), + }); err != nil { + return nil, err + } + + for _, p1paths := range path1CandidateSegments { + for _, p1path := range p1paths { + caNode := p1path.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + if p2segments, ok := path2CandidateSegments[caNode.ID]; !ok { + continue + } else if p3segments, ok := path3CandidateSegments[caNode.ID]; !ok { + continue + } else { + paths.AddPath(p1path.Path()) + for _, p2 := range p2segments { + paths.AddPath(p2.Path()) + } + + for _, p3 := range p3segments { + paths.AddPath(p3.Path()) + } + } + } + } + + return paths, nil } func certTemplateValidForUserVictim(certTemplate *graph.Node) bool { diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx index 9de2b72c5f..7dcde37b44 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx @@ -19,6 +19,7 @@ import WindowsAbuse from './WindowsAbuse'; import LinuxAbuse from './LinuxAbuse'; import Opsec from './Opsec'; import References from './References'; +import Composition from "./Composition"; const ADCSESC9a = { general: General, @@ -26,6 +27,7 @@ const ADCSESC9a = { linuxAbuse: LinuxAbuse, opsec: Opsec, references: References, + composition: Composition }; export default ADCSESC9a; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx new file mode 100644 index 0000000000..4a24ff5eab --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx @@ -0,0 +1,54 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { FC } from 'react'; +import { Alert, Box, Skeleton, Typography } from '@mui/material'; +import { apiClient } from '../../../utils/api'; +import { EdgeInfoProps } from '..'; +import { useQuery } from 'react-query'; +import VirtualizedNodeList, { VirtualizedNodeListItem } from '../../VirtualizedNodeList'; + +const Composition: FC = ({ sourceDBId, targetDBId, edgeName }) => { + const { data, isLoading, isError } = useQuery(['edgeComposition', sourceDBId, targetDBId, edgeName], ({ signal }) => + apiClient.getEdgeComposition(sourceDBId!, targetDBId!, edgeName!).then((result) => result.data) + ); + + const nodesArray: VirtualizedNodeListItem[] = Object.values(data?.data.nodes || {}).map((node) => ({ + name: node.label, + objectId: node.objectId, + kind: node.kind, + })); + + return ( + <> + + The relationship represents the effective outcome of the configuration and relationships between several + different objects. All objects involved in the creation of this relationship are listed here: + + + {isLoading ? ( + + ) : isError ? ( + Couldn't load edge composition + ) : ( + + )} + + + ); +}; + +export default Composition; From 10eda9ec4f03f2447b1c10e897c55fb781965aee Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 14:23:23 -0500 Subject: [PATCH 25/36] wip: esc9a composition --- packages/go/analysis/ad/ad.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index f3ed9798c9..a8b6067521 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -1053,9 +1053,12 @@ func adcsESC9APath2Pattern(caNodes []graph.ID, domainId graph.ID) traversal.Patt func adcsESC9APath3Pattern(caIDs []graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). + Inbound( + query.KindIn(query.Relationship(), ad.DCFor, ad.TrustedBy), + ). Inbound(query.And( + query.Kind(query.Relationship(), ad.CanAbuseWeakCertBinding), query.InIDs(query.StartID(), caIDs...), - query.KindIn(query.Relationship(), ad.CanAbuseWeakCertBinding, ad.DCFor, ad.TrustedBy), )) } @@ -1118,6 +1121,8 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Depth() == 1 }) + graph.FormatPathSegment(terminal) + if victimNode.Kinds.ContainsOneOf(ad.User) { certTemplate := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Node.Kinds.ContainsOneOf(ad.CertTemplate) @@ -1128,6 +1133,8 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g } } + graph.FormatPathSegment(terminal) + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) @@ -1152,8 +1159,11 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) + graph.FormatPathSegment(terminal) + lock.Lock() path2CandidateSegments[caNode.ID] = append(path2CandidateSegments[caNode.ID], terminal) + log.Infof("added ca node %d", caNode.ID) p2canodes = append(p2canodes, caNode.ID) lock.Unlock() @@ -1171,6 +1181,12 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) + log.Infof(graph.FormatPathSegment(terminal)) + + if caNode == nil { + return nil + } + lock.Lock() path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) lock.Unlock() From 60b5d4842d05c7ecb762d190458371b474eef2a5 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 25 Jan 2024 12:33:09 -0800 Subject: [PATCH 26/36] feat+chore: add depth controls to dawgs patterns --- packages/go/dawgs/traversal/traversal.go | 86 ++++++++++++++++-------- 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go index 3ec0b72b44..96da8aac9d 100644 --- a/packages/go/dawgs/traversal/traversal.go +++ b/packages/go/dawgs/traversal/traversal.go @@ -49,7 +49,9 @@ type PatternMatchDelegate = func(terminal *graph.PathSegment) error // The return value of the Do(...) function may be passed directly to a Traversal via a Plan as the Plan.Driver field. type PatternContinuation interface { Outbound(criteria ...graph.Criteria) PatternContinuation + OutboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation Inbound(criteria ...graph.Criteria) PatternContinuation + InboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation Do(delegate PatternMatchDelegate) Driver } @@ -57,6 +59,8 @@ type PatternContinuation interface { type expansion struct { criteria []graph.Criteria direction graph.Direction + minDepth int + maxDepth int } func (s expansion) PrepareCriteria(segment *graph.PathSegment) (graph.Criteria, error) { @@ -84,6 +88,7 @@ func (s expansion) PrepareCriteria(segment *graph.PathSegment) (graph.Criteria, type patternTag struct { patternIdx int + depth int } func popSegmentPatternTag(segment *graph.PathSegment) *patternTag { @@ -95,6 +100,7 @@ func popSegmentPatternTag(segment *graph.PathSegment) *patternTag { } else { tag = &patternTag{ patternIdx: 0, + depth: 0, } } @@ -112,26 +118,42 @@ func (s *pattern) Do(delegate PatternMatchDelegate) Driver { return s.Driver } -// Outbound specifies the next outbound expansion step for this pattern. -func (s *pattern) Outbound(criteria ...graph.Criteria) PatternContinuation { +// OutboundWithDepth specifies the next outbound expansion step for this pattern with depth parameters. +func (s *pattern) OutboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation { s.expansions = append(s.expansions, expansion{ criteria: criteria, direction: graph.DirectionOutbound, + minDepth: min, + maxDepth: max, }) return s } -// Inbound specifies the next inbound expansion step for this pattern. -func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation { +// Outbound specifies the next outbound expansion step for this pattern. By default, this expansion will use a minimum +// depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely. +func (s *pattern) Outbound(criteria ...graph.Criteria) PatternContinuation { + return s.OutboundWithDepth(1, 0, criteria...) +} + +// InboundWithDepth specifies the next inbound expansion step for this pattern with depth parameters. +func (s *pattern) InboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation { s.expansions = append(s.expansions, expansion{ criteria: criteria, direction: graph.DirectionInbound, + minDepth: max, + maxDepth: min, }) return s } +// Inbound specifies the next inbound expansion step for this pattern. By default, this expansion will use a minimum +// depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely. +func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation { + return s.InboundWithDepth(1, 0) +} + // NewPattern returns a new PatternContinuation for building a new pattern. func NewPattern() PatternContinuation { return &pattern{} @@ -152,9 +174,9 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra for next := range cursor.Chan() { nextSegment := segment.Descend(next.Node, next.Relationship) nextSegment.Tag = &patternTag{ - // Use the tag's patternIdx here since this is the reference that will see the increment when - // the current expansion is exhausted + // Use the tag's patternIdx and depth since this is a continuation of the expansions patternIdx: tag.patternIdx, + depth: tag.depth + 1, } nextSegments = append(nextSegments, nextSegment) @@ -168,34 +190,42 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra if fetchDirection, err := currentExpansion.direction.Reverse(); err != nil { return nil, err } else { - // Perform the current expansion. - if criteria, err := currentExpansion.PrepareCriteria(segment); err != nil { - return nil, err - } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil { - return nil, err - } - - // No further expansions means this pattern segment is complete. Increment the pattern index to select the - // next pattern expansion. - tag.patternIdx++ - - // Perform the next expansion if there is one. - if tag.patternIdx < len(s.expansions) { - nextExpansion := s.expansions[tag.patternIdx] - - // Expand the next segments - if criteria, err := nextExpansion.PrepareCriteria(segment); err != nil { + // If no max depth was set or if a max depth was set expand the current step further + if currentExpansion.maxDepth == 0 || tag.depth < currentExpansion.maxDepth { + // Perform the current expansion. + if criteria, err := currentExpansion.PrepareCriteria(segment); err != nil { return nil, err } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil { return nil, err } - } else if len(nextSegments) == 0 { - // If there are no expanded segments and there are no remaining expansions, this is a terminal segment. - // Hand it off to the delegate and handle any returned error. - if err := s.delegate(segment); err != nil { - return nil, err + } + + // Check first if this current segment was fetched using the current expansion (i.e. non-optional) + if tag.depth > 0 && currentExpansion.minDepth == 0 || tag.depth >= currentExpansion.minDepth { + // No further expansions means this pattern segment is complete. Increment the pattern index to select the + // next pattern expansion. + tag.patternIdx++ + + // Perform the next expansion if there is one. + if tag.patternIdx < len(s.expansions) { + nextExpansion := s.expansions[tag.patternIdx] + + // Expand the next segments + if criteria, err := nextExpansion.PrepareCriteria(segment); err != nil { + return nil, err + } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil { + return nil, err + } + } else if len(nextSegments) == 0 { + // If there are no expanded segments and there are no remaining expansions, this is a terminal segment. + // Hand it off to the delegate and handle any returned error. + if err := s.delegate(segment); err != nil { + return nil, err + } } } + + // If the above condition does not match then this current expansion is non-terminal and non-continuable } // Return any collected segments From 093e954f20fee09a3f3a68e9c8669085fdbcbbd5 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 16:00:40 -0500 Subject: [PATCH 27/36] wip: esc9a composition --- packages/go/analysis/ad/ad.go | 47 ++++++++++++++++------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index a8b6067521..997170e7ac 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -984,13 +984,15 @@ func getGoldenCertEdgeComposition(tx graph.Transaction, edge *graph.Relationship func adcsESC9aPath1Pattern(domainID graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound( + OutboundWithDepth( + 1, 1, query.And( query.KindIn(query.Relationship(), ad.GenericWrite, ad.GenericAll, ad.Owns, ad.WriteOwner, ad.WriteDACL), query.KindIn(query.End(), ad.Computer, ad.User), ), ). - Outbound( + OutboundWithDepth( + 0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), @@ -1117,12 +1119,11 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ Root: startNode, Driver: adcsESC9aPath1Pattern(edge.EndID).Do(func(terminal *graph.PathSegment) error { + log.Infof("Segment: %v", graph.FormatPathSegment(terminal)) victimNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Depth() == 1 }) - graph.FormatPathSegment(terminal) - if victimNode.Kinds.ContainsOneOf(ad.User) { certTemplate := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Node.Kinds.ContainsOneOf(ad.CertTemplate) @@ -1133,8 +1134,6 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g } } - graph.FormatPathSegment(terminal) - caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) @@ -1159,8 +1158,6 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) - graph.FormatPathSegment(terminal) - lock.Lock() path2CandidateSegments[caNode.ID] = append(path2CandidateSegments[caNode.ID], terminal) log.Infof("added ca node %d", caNode.ID) @@ -1174,26 +1171,26 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g } } - if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ - Root: endNode, - Driver: adcsESC9APath3Pattern(p2canodes).Do(func(terminal *graph.PathSegment) error { - caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { - return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) - }) + if len(p2canodes) > 0 { + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: endNode, + Driver: adcsESC9APath3Pattern(p2canodes).Do(func(terminal *graph.PathSegment) error { + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) - log.Infof(graph.FormatPathSegment(terminal)) + if caNode == nil { + return nil + } - if caNode == nil { + lock.Lock() + path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) + lock.Unlock() return nil - } - - lock.Lock() - path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) - lock.Unlock() - return nil - }), - }); err != nil { - return nil, err + }), + }); err != nil { + return nil, err + } } for _, p1paths := range path1CandidateSegments { From cac9e2f8e79a6e32bb41b5d13726f5c06bab04f3 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Thu, 25 Jan 2024 13:20:06 -0800 Subject: [PATCH 28/36] fix: do not drop the current segment if the next pattern is optional --- packages/go/dawgs/traversal/traversal.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go index 96da8aac9d..0dfc2dc050 100644 --- a/packages/go/dawgs/traversal/traversal.go +++ b/packages/go/dawgs/traversal/traversal.go @@ -203,8 +203,10 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra // Check first if this current segment was fetched using the current expansion (i.e. non-optional) if tag.depth > 0 && currentExpansion.minDepth == 0 || tag.depth >= currentExpansion.minDepth { // No further expansions means this pattern segment is complete. Increment the pattern index to select the - // next pattern expansion. + // next pattern expansion. Additionally, set the depth back to zero for the tag since we are leaving the + // current expansion. tag.patternIdx++ + tag.depth = 0 // Perform the next expansion if there is one. if tag.patternIdx < len(s.expansions) { @@ -216,6 +218,13 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil { return nil, err } + + // If the next expansion is optional, make sure to preserve the current traversal branch + if nextExpansion.minDepth == 0 { + // Reattach the tag to the segment before adding it to the returned segments for the next expansion + segment.Tag = tag + nextSegments = append(nextSegments, segment) + } } else if len(nextSegments) == 0 { // If there are no expanded segments and there are no remaining expansions, this is a terminal segment. // Hand it off to the delegate and handle any returned error. @@ -301,6 +310,8 @@ func (s Traversal) BreadthFirst(ctx context.Context, plan Plan) error { if nextDescent, ok := channels.Receive(traversalCtx, segmentReaderC); !ok { return nil } else if pathTreeSize := pathTree.SizeOf(); pathTreeSize < tx.TraversalMemoryLimit() { + log.Infof("%s", graph.FormatPathSegment(nextDescent)) + // Traverse the descending relationships of the current segment if descendingSegments, err := plan.Driver(traversalCtx, tx, nextDescent); err != nil { return err From 61895bcc50e657795164ac68e409dc0ecd2c6fbb Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 16:54:23 -0500 Subject: [PATCH 29/36] wip: esc9a composition --- cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx index bc0e4217bb..2f46bf7109 100644 --- a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx +++ b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx @@ -73,7 +73,7 @@ const EdgeInfoContent: FC<{ selectedEdge: NonNullable }> = ({ sele const sendOnChange = (selectedEdge.name === 'GoldenCert' || selectedEdge.name === 'ADCSESC1' || - selectedEdge.name === 'ADCSESC3') && + selectedEdge.name === 'ADCSESC3' || selectedEdge.name === 'ADCSESC9a') && section[0] === 'composition'; return ( From bd845e1fd5d7b0655fee174d081bf497b9833626 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 16:56:37 -0500 Subject: [PATCH 30/36] fix: update other continuations to respect depth correctly --- packages/go/analysis/ad/ad.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index 997170e7ac..dbf70fd41c 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -562,7 +562,7 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph. func ADCSESC3Path1Pattern(domainId graph.ID, enterpriseCAs cardinality.Duplex[uint32]) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -597,7 +597,7 @@ func ADCSESC3Path1Pattern(domainId graph.ID, enterpriseCAs cardinality.Duplex[ui func ADCSESC3Path2Pattern(domainId graph.ID, enterpriseCAs, candidateTemplates cardinality.Duplex[uint32]) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -624,7 +624,7 @@ func ADCSESC3Path2Pattern(domainId graph.ID, enterpriseCAs, candidateTemplates c func ADCSESC3Path3Pattern() traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -813,7 +813,7 @@ func getDelegatedEnrollmentAgentPath(ctx context.Context, startNode, certTemplat func ADCSESC1Path1Pattern(domainID graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -852,7 +852,7 @@ func ADCSESC1Path1Pattern(domainID graph.ID) traversal.PatternContinuation { func ADCSESC1Path2Pattern(domainID graph.ID, enterpriseCAs cardinality.Duplex[uint32]) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). @@ -1035,7 +1035,7 @@ func adcsESC9aPath1Pattern(domainID graph.ID) traversal.PatternContinuation { func adcsESC9APath2Pattern(caNodes []graph.ID, domainId graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). - Outbound(query.And( + OutboundWithDepth(0, 0, query.And( query.Kind(query.Relationship(), ad.MemberOf), query.Kind(query.End(), ad.Group), )). From 7f0e94f94cf849d2c421883a5da24a26a615ccdd Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 17:19:04 -0500 Subject: [PATCH 31/36] wip: edge comp --- packages/go/analysis/ad/ad.go | 6 +----- packages/go/dawgs/traversal/traversal.go | 4 +--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index dbf70fd41c..5ca999b06c 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -1119,7 +1119,6 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ Root: startNode, Driver: adcsESC9aPath1Pattern(edge.EndID).Do(func(terminal *graph.PathSegment) error { - log.Infof("Segment: %v", graph.FormatPathSegment(terminal)) victimNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { return nextSegment.Depth() == 1 }) @@ -1160,7 +1159,6 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g lock.Lock() path2CandidateSegments[caNode.ID] = append(path2CandidateSegments[caNode.ID], terminal) - log.Infof("added ca node %d", caNode.ID) p2canodes = append(p2canodes, caNode.ID) lock.Unlock() @@ -1179,9 +1177,7 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) - if caNode == nil { - return nil - } + log.Infof("Segment: %v", graph.FormatPathSegment(terminal)) lock.Lock() path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go index 0dfc2dc050..13ea8d736c 100644 --- a/packages/go/dawgs/traversal/traversal.go +++ b/packages/go/dawgs/traversal/traversal.go @@ -151,7 +151,7 @@ func (s *pattern) InboundWithDepth(min, max int, criteria ...graph.Criteria) Pat // Inbound specifies the next inbound expansion step for this pattern. By default, this expansion will use a minimum // depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely. func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation { - return s.InboundWithDepth(1, 0) + return s.InboundWithDepth(1, 0, criteria...) } // NewPattern returns a new PatternContinuation for building a new pattern. @@ -310,8 +310,6 @@ func (s Traversal) BreadthFirst(ctx context.Context, plan Plan) error { if nextDescent, ok := channels.Receive(traversalCtx, segmentReaderC); !ok { return nil } else if pathTreeSize := pathTree.SizeOf(); pathTreeSize < tx.TraversalMemoryLimit() { - log.Infof("%s", graph.FormatPathSegment(nextDescent)) - // Traverse the descending relationships of the current segment if descendingSegments, err := plan.Driver(traversalCtx, tx, nextDescent); err != nil { return err From 30d62535866fa6806afab3af421d9f3129c807ad Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 17:35:51 -0500 Subject: [PATCH 32/36] fix: swap --- packages/go/dawgs/traversal/traversal.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go index 13ea8d736c..4fa7213d4e 100644 --- a/packages/go/dawgs/traversal/traversal.go +++ b/packages/go/dawgs/traversal/traversal.go @@ -141,8 +141,8 @@ func (s *pattern) InboundWithDepth(min, max int, criteria ...graph.Criteria) Pat s.expansions = append(s.expansions, expansion{ criteria: criteria, direction: graph.DirectionInbound, - minDepth: max, - maxDepth: min, + minDepth: min, + maxDepth: max, }) return s From 8f835318939ae914a4b07ab815dd9eb1923a8a72 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Thu, 25 Jan 2024 17:47:13 -0500 Subject: [PATCH 33/36] chore: remove unnecessary logs --- packages/go/analysis/ad/ad.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index 5ca999b06c..cdc9dadeab 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -937,8 +937,6 @@ func GetADCSESC1EdgeComposition(ctx context.Context, db graph.Database, edge *gr // Render paths from the segments return paths, path1EnterpriseCAs.Each(func(value uint32) (bool, error) { for _, segment := range candidateSegments[graph.ID(value)] { - log.Infof("Found ESC1 Path: %s", graph.FormatPathSegment(segment)) - paths.AddPath(segment.Path()) } @@ -1177,8 +1175,6 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) }) - log.Infof("Segment: %v", graph.FormatPathSegment(terminal)) - lock.Lock() path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) lock.Unlock() From 61ca2acb5be87f28fa8c5185de532eb7d8f62af2 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Fri, 26 Jan 2024 10:57:09 -0500 Subject: [PATCH 34/36] test: add test covering esc9a edge comp --- .../src/analysis/ad/adcs_integration_test.go | 27 +++++++++++++++++++ cmd/api/src/test/integration/harnesses.go | 4 +++ 2 files changed, 31 insertions(+) diff --git a/cmd/api/src/analysis/ad/adcs_integration_test.go b/cmd/api/src/analysis/ad/adcs_integration_test.go index bda73f3ce8..d6f856be22 100644 --- a/cmd/api/src/analysis/ad/adcs_integration_test.go +++ b/cmd/api/src/analysis/ad/adcs_integration_test.go @@ -639,6 +639,33 @@ func TestADCSESC9a(t *testing.T) { } return nil }) + + db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { + if results, err := ops.FetchRelationships(tx.Relationships().Filterf(func() graph.Criteria { + return query.Kind(query.Relationship(), ad.ADCSESC9a) + })); err != nil { + t.Fatalf("error fetching esc9a edges in integration test; %v", err) + } else { + assert.Equal(t, 1, len(results)) + edge := results[0] + + if edgeComp, err := ad2.GetEdgeCompositionPath(context.Background(), db, edge); err != nil { + t.Fatalf("error getting edge composition for esc9: %v", err) + } else { + nodes := edgeComp.AllNodes().Slice() + assert.Contains(t, nodes, harness.ESC9AHarness.Attacker) + assert.Contains(t, nodes, harness.ESC9AHarness.Victim) + assert.Contains(t, nodes, harness.ESC9AHarness.Domain) + assert.Contains(t, nodes, harness.ESC9AHarness.NTAuthStore) + assert.Contains(t, nodes, harness.ESC9AHarness.RootCA) + assert.Contains(t, nodes, harness.ESC9AHarness.DC) + assert.Contains(t, nodes, harness.ESC9AHarness.EnterpriseCA) + assert.Contains(t, nodes, harness.ESC9AHarness.CertTemplate) + } + } + + return nil + }) }) } diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go index f01140325d..c50d2452de 100644 --- a/cmd/api/src/test/integration/harnesses.go +++ b/cmd/api/src/test/integration/harnesses.go @@ -18,7 +18,11 @@ package integration import ( "fmt" + "github.com/specterops/bloodhound/analysis" + "github.com/specterops/bloodhound/src/test/integration/harnesses" "math/rand" + "strconv" + "strings" "time" "github.com/gofrs/uuid" From 008b0dff75ff55bb35930d476a18eb6a9c064ff4 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Fri, 26 Jan 2024 10:59:12 -0500 Subject: [PATCH 35/36] chore: revert random re-ordering --- cmd/api/src/test/integration/harnesses.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go index c50d2452de..9b11946519 100644 --- a/cmd/api/src/test/integration/harnesses.go +++ b/cmd/api/src/test/integration/harnesses.go @@ -18,20 +18,20 @@ package integration import ( "fmt" - "github.com/specterops/bloodhound/analysis" - "github.com/specterops/bloodhound/src/test/integration/harnesses" "math/rand" "strconv" "strings" "time" "github.com/gofrs/uuid" + "github.com/specterops/bloodhound/analysis" adAnalysis "github.com/specterops/bloodhound/analysis/ad" "github.com/specterops/bloodhound/dawgs/graph" "github.com/specterops/bloodhound/graphschema/ad" "github.com/specterops/bloodhound/graphschema/azure" "github.com/specterops/bloodhound/graphschema/common" "github.com/specterops/bloodhound/src/test" + "github.com/specterops/bloodhound/src/test/integration/harnesses" ) func RandomObjectID(t test.Controller) string { From dba887683c26a8cd5bcfe845a14e99f7522ee8c8 Mon Sep 17 00:00:00 2001 From: rvazarkar Date: Mon, 29 Jan 2024 16:07:46 -0500 Subject: [PATCH 36/36] chore: handle negative min/max depth on continuations --- packages/go/dawgs/traversal/traversal.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go index 4fa7213d4e..dcfe83f106 100644 --- a/packages/go/dawgs/traversal/traversal.go +++ b/packages/go/dawgs/traversal/traversal.go @@ -120,6 +120,16 @@ func (s *pattern) Do(delegate PatternMatchDelegate) Driver { // OutboundWithDepth specifies the next outbound expansion step for this pattern with depth parameters. func (s *pattern) OutboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation { + if min < 0 { + min = 1 + log.Warnf("Negative mindepth not allowed. Setting min depth for expansion to 1") + } + + if max < 0 { + max = 0 + log.Warnf("Negative maxdepth not allowed. Setting max depth for expansion to 0") + } + s.expansions = append(s.expansions, expansion{ criteria: criteria, direction: graph.DirectionOutbound, @@ -138,6 +148,16 @@ func (s *pattern) Outbound(criteria ...graph.Criteria) PatternContinuation { // InboundWithDepth specifies the next inbound expansion step for this pattern with depth parameters. func (s *pattern) InboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation { + if min < 0 { + min = 1 + log.Warnf("Negative mindepth not allowed. Setting min depth for expansion to 1") + } + + if max < 0 { + max = 0 + log.Warnf("Negative maxdepth not allowed. Setting max depth for expansion to 0") + } + s.expansions = append(s.expansions, expansion{ criteria: criteria, direction: graph.DirectionInbound,