diff --git a/cmd/api/src/analysis/ad/adcs_integration_test.go b/cmd/api/src/analysis/ad/adcs_integration_test.go index 5a85fe3ed..3d83d5545 100644 --- a/cmd/api/src/analysis/ad/adcs_integration_test.go +++ b/cmd/api/src/analysis/ad/adcs_integration_test.go @@ -551,10 +551,10 @@ func TestEnrollOnBehalfOf(t *testing.T) { }, func(harness integration.HarnessDetails, db graph.Database) { operation := analysis.NewPostRelationshipOperation(context.Background(), db, "ADCS Post Process Test - EnrollOnBehalfOf 3") - _, enterpriseCertAuthorities, certTemplates, domains, cache, err := FetchADCSPrereqs(db) + _, _, _, _, cache, err := FetchADCSPrereqs(db) require.Nil(t, err) - if err := ad2.PostEnrollOnBehalfOf(domains, enterpriseCertAuthorities, certTemplates, cache, operation); err != nil { + if err := ad2.PostEnrollOnBehalfOf(cache, operation); err != nil { t.Logf("failed post processing for %s: %v", ad.EnrollOnBehalfOf.String(), err) } err = operation.Done() @@ -2559,16 +2559,10 @@ func TestADCSESC6b(t *testing.T) { func FetchADCSPrereqs(db graph.Database) (impact.PathAggregator, []*graph.Node, []*graph.Node, []*graph.Node, ad2.ADCSCache, error) { if expansions, err := ad2.ExpandAllRDPLocalGroups(context.Background(), db); err != nil { return nil, nil, nil, nil, ad2.ADCSCache{}, err - } else if eca, err := ad2.FetchNodesByKind(context.Background(), db, ad.EnterpriseCA); err != nil { - return nil, nil, nil, nil, ad2.ADCSCache{}, err - } else if certTemplates, err := ad2.FetchNodesByKind(context.Background(), db, ad.CertTemplate); err != nil { - return nil, nil, nil, nil, ad2.ADCSCache{}, err - } else if domains, err := ad2.FetchNodesByKind(context.Background(), db, ad.Domain); err != nil { - return nil, nil, nil, nil, ad2.ADCSCache{}, err } else { cache := ad2.NewADCSCache() - cache.BuildCache(context.Background(), db, eca, certTemplates, domains) - return expansions, eca, certTemplates, domains, cache, nil + cache.BuildCache(context.Background(), db) + return expansions, cache.GetEnterpriseCertAuthorities(), cache.GetCertTemplates(), cache.GetDomains(), cache, nil } } diff --git a/cmd/api/src/analysis/ad/ntlm_integration_test.go b/cmd/api/src/analysis/ad/ntlm_integration_test.go index 66280ea64..9f589bc37 100644 --- a/cmd/api/src/analysis/ad/ntlm_integration_test.go +++ b/cmd/api/src/analysis/ad/ntlm_integration_test.go @@ -21,9 +21,10 @@ package ad_test import ( "context" - "strings" "testing" + "github.com/specterops/bloodhound/dawgs/cardinality" + "github.com/specterops/bloodhound/analysis" ad2 "github.com/specterops/bloodhound/analysis/ad" "github.com/specterops/bloodhound/analysis/impact" @@ -33,11 +34,56 @@ import ( "github.com/specterops/bloodhound/graphschema" "github.com/specterops/bloodhound/graphschema/ad" "github.com/specterops/bloodhound/src/test/integration" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestPostNTLM(t *testing.T) { +func TestPostNTLMRelayADCS(t *testing.T) { + //TODO: Add some negative tests here + testContext := integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema()) + + testContext.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) error { + harness.NTLMCoerceAndRelayNTLMToADCS.Setup(testContext) + return nil + }, func(harness integration.HarnessDetails, db graph.Database) { + operation := analysis.NewPostRelationshipOperation(context.Background(), db, "NTLM Post Process Test - CoerceAndRelayNTLMToADCS") + _, _, domains, authenticatedUsers, err := fetchNTLMPrereqs(db) + + require.NoError(t, err) + + for _, domain := range domains { + innerDomain := domain + computerCache, err := fetchComputerCache(db, innerDomain) + require.NoError(t, err) + + err = ad2.PostCoerceAndRelayNTLMToADCS(context.Background(), db, operation, authenticatedUsers, computerCache) + require.NoError(t, err) + } + + operation.Done() + + 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.CoerceAndRelayNTLMToADCS) + })); err != nil { + t.Fatalf("error fetching ntlm to smb edges in integration test; %v", err) + } else { + + require.Len(t, results, 1) + rel := results[0] + + start, end, err := ops.FetchRelationshipNodes(tx, rel) + require.NoError(t, err) + + require.Equal(t, start.ID, harness.NTLMCoerceAndRelayNTLMToADCS.AuthenticatedUsersGroup.ID) + require.Equal(t, end.ID, harness.NTLMCoerceAndRelayNTLMToADCS.Computer.ID) + } + return nil + }) + }) +} + +func TestPostNTLMRelaySMB(t *testing.T) { + //TODO: Add some negative tests here testContext := integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema()) testContext.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) error { @@ -55,7 +101,7 @@ func TestPostNTLM(t *testing.T) { err = operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { for _, computer := range computers { innerComputer := computer - domainSid, _ := innerDomain.Properties.Get(ad.Domain.String()).String() + domainSid, _ := innerDomain.Properties.Get(ad.DomainSID.String()).String() authenticatedUserID := authenticatedUsers[domainSid] if err = ad2.PostCoerceAndRelayNTLMToSMB(tx, outC, groupExpansions, innerComputer, authenticatedUserID); err != nil { @@ -72,49 +118,45 @@ func TestPostNTLM(t *testing.T) { // Test start node db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { - if results, err := ops.FetchStartNodes(tx.Relationships().Filterf(func() graph.Criteria { + if results, err := ops.FetchRelationships(tx.Relationships().Filterf(func() graph.Criteria { return query.Kind(query.Relationship(), ad.CoerceAndRelayNTLMToSMB) })); err != nil { t.Fatalf("error fetching ntlm to smb edges in integration test; %v", err) } else { require.Len(t, results, 1) - resultIds := results.IDs() - - objectId := results.Get(resultIds[0]).Properties.Get("objectid") - require.False(t, objectId.IsNil()) - - objectIdStr, err := objectId.String() + rel := results[0] + start, end, err := ops.FetchRelationshipNodes(tx, rel) require.NoError(t, err) - assert.True(t, strings.HasSuffix(objectIdStr, ad2.AuthenticatedUsersSuffix)) + + require.Equal(t, start.ID, harness.NTLMCoerceAndRelayNTLMToSMB.AuthenticatedUsers.ID) + require.Equal(t, end.ID, harness.NTLMCoerceAndRelayNTLMToSMB.Computer8.ID) } return nil }) + }) +} - // Test end node - db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { - if results, err := ops.FetchEndNodes(tx.Relationships().Filterf(func() graph.Criteria { - return query.Kind(query.Relationship(), ad.CoerceAndRelayNTLMToSMB) - })); err != nil { - t.Fatalf("error fetching ntlm to smb edges in integration test; %v", err) - } else { - require.Len(t, results, 1) - resultIds := results.IDs() - - objectId := results.Get(resultIds[0]).Properties.Get("objectid") - require.False(t, objectId.IsNil()) - - smbSigning, err := results.Get(resultIds[0]).Properties.Get(ad.SMBSigning.String()).Bool() - require.NoError(t, err) - - restrictOutbountNtlm, err := results.Get(resultIds[0]).Properties.Get(ad.RestrictOutboundNTLM.String()).Bool() - require.NoError(t, err) +func fetchComputerCache(db graph.Database, domain *graph.Node) (map[string]cardinality.Duplex[uint64], error) { + cache := make(map[string]cardinality.Duplex[uint64]) + if domainSid, err := domain.Properties.Get(ad.DomainSID.String()).String(); err != nil { + return cache, err + } else { + cache[domainSid] = cardinality.NewBitmap64() + return cache, db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { + return tx.Nodes().Filter( + query.And( + query.Kind(query.Node(), ad.Computer), + query.Equals(query.NodeProperty(ad.DomainSID.String()), domainSid), + ), + ).FetchIDs(func(cursor graph.Cursor[graph.ID]) error { + for id := range cursor.Chan() { + cache[domainSid].Add(id.Uint64()) + } - assert.False(t, smbSigning) - assert.False(t, restrictOutbountNtlm) - } - return nil + return cursor.Error() + }) }) - }) + } } func fetchNTLMPrereqs(db graph.Database) (expansions impact.PathAggregator, computers []*graph.Node, domains []*graph.Node, authenticatedUsers map[string]graph.ID, err error) { diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go index 106943bad..033f8c9ea 100644 --- a/cmd/api/src/test/integration/harnesses.go +++ b/cmd/api/src/test/integration/harnesses.go @@ -8498,40 +8498,84 @@ func (s *ESC10bHarnessDC2) Setup(graphTestContext *GraphTestContext) { graphTestContext.UpdateNode(s.DC1) } +type CoerceAndRelayNTLMtoADCS struct { + AuthenticatedUsersGroup *graph.Node + CertTemplate1 *graph.Node + Computer *graph.Node + Domain *graph.Node + EnterpriseCA1 *graph.Node + NTAuthStore *graph.Node + RootCA *graph.Node +} + +func (s *CoerceAndRelayNTLMtoADCS) Setup(graphTestContext *GraphTestContext) { + domainSid := RandomDomainSID() + s.AuthenticatedUsersGroup = graphTestContext.NewActiveDirectoryGroup("Authenticated Users Group", domainSid) + s.CertTemplate1 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate1", domainSid, CertTemplateData{ + ApplicationPolicies: []string{}, + AuthenticationEnabled: true, + AuthorizedSignatures: 0, + EffectiveEKUs: []string{}, + EnrolleeSuppliesSubject: false, + NoSecurityExtension: false, + RequiresManagerApproval: false, + SchannelAuthenticationEnabled: false, + SchemaVersion: 1, + SubjectAltRequireEmail: false, + SubjectAltRequireSPN: false, + SubjectAltRequireUPN: false, + }) + s.Computer = graphTestContext.NewActiveDirectoryComputer("Computer", domainSid) + s.Domain = graphTestContext.NewActiveDirectoryDomain("Domain", domainSid, false, true) + s.EnterpriseCA1 = graphTestContext.NewActiveDirectoryEnterpriseCA("EnterpriseCA1", domainSid) + s.NTAuthStore = graphTestContext.NewActiveDirectoryNTAuthStore("NTAuthStore", domainSid) + s.RootCA = graphTestContext.NewActiveDirectoryRootCA("RootCA", domainSid) + graphTestContext.NewRelationship(s.AuthenticatedUsersGroup, s.CertTemplate1, ad.Enroll) + graphTestContext.NewRelationship(s.AuthenticatedUsersGroup, s.EnterpriseCA1, ad.Enroll) + graphTestContext.NewRelationship(s.CertTemplate1, s.EnterpriseCA1, ad.PublishedTo) + graphTestContext.NewRelationship(s.EnterpriseCA1, s.RootCA, ad.IssuedSignedBy) + graphTestContext.NewRelationship(s.EnterpriseCA1, s.NTAuthStore, ad.TrustedForNTAuth) + graphTestContext.NewRelationship(s.NTAuthStore, s.Domain, ad.NTAuthStoreFor) + graphTestContext.NewRelationship(s.RootCA, s.Domain, ad.RootCAFor) + + s.EnterpriseCA1.Properties.Set(ad.ADCSWebEnrollmentHTTP.String(), true) + graphTestContext.UpdateNode(s.EnterpriseCA1) + s.Computer.Properties.Set(ad.WebClientRunning.String(), true) + graphTestContext.UpdateNode(s.Computer) + s.AuthenticatedUsersGroup.Properties.Set(common.ObjectID.String(), fmt.Sprintf("authenticated-users%s", adAnalysis.AuthenticatedUsersSuffix)) + graphTestContext.UpdateNode(s.AuthenticatedUsersGroup) +} + type NTLMCoerceAndRelayNTLMToSMB struct { AuthenticatedUsers *graph.Node DomainAdminsUser *graph.Node ServerAdmins *graph.Node - computer3 *graph.Node - computer8 *graph.Node + Computer3 *graph.Node + Computer8 *graph.Node + Domain *graph.Node } func (s *NTLMCoerceAndRelayNTLMToSMB) Setup(graphTestContext *GraphTestContext) { domainSid := RandomDomainSID() + s.Domain = graphTestContext.NewActiveDirectoryDomain("Domain", domainSid, false, true) s.AuthenticatedUsers = graphTestContext.NewActiveDirectoryGroup("Authenticated Users", domainSid) - s.AuthenticatedUsers.Properties.Set("objectid", fmt.Sprintf("authenticated-users%s", adAnalysis.AuthenticatedUsersSuffix)) - s.AuthenticatedUsers.Properties.Set("Domain", domainSid) + s.AuthenticatedUsers.Properties.Set(common.ObjectID.String(), fmt.Sprintf("authenticated-users%s", adAnalysis.AuthenticatedUsersSuffix)) graphTestContext.UpdateNode(s.AuthenticatedUsers) - s.DomainAdminsUser = graphTestContext.NewActiveDirectoryUser("Domain Admins User", domainSid) + s.DomainAdminsUser = graphTestContext.NewActiveDirectoryUser("Domain Admin User", domainSid) - s.ServerAdmins = graphTestContext.NewActiveDirectoryDomain("Server Admins", domainSid, false, true) - s.ServerAdmins.Properties.Set("objectid", fmt.Sprintf("server-admins%s", adAnalysis.AuthenticatedUsersSuffix)) - s.ServerAdmins.Properties.Set("Domain", domainSid) + s.ServerAdmins = graphTestContext.NewActiveDirectoryGroup("Server Admins", domainSid) graphTestContext.UpdateNode(s.ServerAdmins) + s.Computer3 = graphTestContext.NewActiveDirectoryComputer("Computer3", domainSid) - s.DomainAdminsUser.Properties.Set("objectid", fmt.Sprintf("domainadminuser-users%s", adAnalysis.AuthenticatedUsersSuffix)) - s.computer3 = graphTestContext.NewActiveDirectoryComputer("computer3", domainSid) - - s.computer8 = graphTestContext.NewActiveDirectoryComputer("computer8", domainSid) - s.computer8.Properties.Set(ad.SMBSigning.String(), false) - s.computer8.Properties.Set(ad.RestrictOutboundNTLM.String(), false) - graphTestContext.UpdateNode(s.computer8) + s.Computer8 = graphTestContext.NewActiveDirectoryComputer("Computer8", domainSid) + s.Computer8.Properties.Set(ad.SMBSigning.String(), false) + s.Computer8.Properties.Set(ad.RestrictOutboundNTLM.String(), false) + graphTestContext.UpdateNode(s.Computer8) - graphTestContext.NewRelationship(s.computer3, s.ServerAdmins, ad.MemberOf) - graphTestContext.NewRelationship(s.ServerAdmins, s.computer8, ad.AdminTo) - graphTestContext.NewRelationship(s.AuthenticatedUsers, s.computer8, ad.CoerceAndRelayNTLMToSMB) - graphTestContext.NewRelationship(s.computer8, s.DomainAdminsUser, ad.HasSession) + graphTestContext.NewRelationship(s.Computer3, s.ServerAdmins, ad.MemberOf) + graphTestContext.NewRelationship(s.ServerAdmins, s.Computer8, ad.AdminTo) + graphTestContext.NewRelationship(s.Computer8, s.DomainAdminsUser, ad.HasSession) } type HarnessDetails struct { @@ -8635,4 +8679,5 @@ type HarnessDetails struct { SyncLAPSPasswordHarness SyncLAPSPasswordHarness HybridAttackPaths HybridAttackPaths NTLMCoerceAndRelayNTLMToSMB NTLMCoerceAndRelayNTLMToSMB + NTLMCoerceAndRelayNTLMToADCS CoerceAndRelayNTLMtoADCS } diff --git a/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToADCS.json b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToADCS.json new file mode 100644 index 000000000..e8f1a3d80 --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToADCS.json @@ -0,0 +1,209 @@ +{ + "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": 429.32101001233576, + "y": 86.3210100123357 + }, + "caption": "Authenticated Users Group", + "labels": [], + "properties": {}, + "style": {} + }, + { + "id": "n2", + "position": { + "x": 429.32101001233576, + "y": 296.3210100123357 + }, + "caption": "CertTemplate1", + "labels": [], + "properties": { + "RequiresManagerApproval ": "false", + "AuthenticationEnabled": "true", + "SchemaVersion": "1", + "": "" + }, + "style": {} + }, + { + "id": "n3", + "position": { + "x": 827.9582662874807, + "y": -71.12263236735485 + }, + "caption": "Computer", + "labels": [], + "properties": { + "webclientrunning": "false" + }, + "style": {} + }, + { + "id": "n4", + "position": { + "x": 729.6420200246715, + "y": 296.32101001233565 + }, + "caption": "EnterpriseCA1", + "labels": [], + "properties": { + "webenrollmenthttp": "true" + }, + "style": {} + }, + { + "id": "n5", + "position": { + "x": 1049.8710085341406, + "y": 147.9615151329445 + }, + "caption": "RootCA", + "style": {}, + "labels": [], + "properties": {} + }, + { + "id": "n6", + "position": { + "x": 1049.8710085341406, + "y": 398.6954896068796 + }, + "caption": "NTAuthStore", + "style": {}, + "labels": [], + "properties": {} + }, + { + "id": "n7", + "position": { + "x": 1353.9722201033771, + "y": 255.3441998457691 + }, + "caption": "Domain", + "style": {}, + "labels": [], + "properties": {} + } + ], + "relationships": [ + { + "id": "n0", + "fromId": "n1", + "toId": "n2", + "type": "Enroll", + "properties": {}, + "style": {} + }, + { + "id": "n1", + "fromId": "n1", + "toId": "n3", + "type": "CoerceAndRelayNTLMToADCS", + "properties": {}, + "style": {} + }, + { + "id": "n2", + "type": "PublishedTo", + "style": {}, + "properties": {}, + "fromId": "n2", + "toId": "n4" + }, + { + "id": "n3", + "type": "IssuedSignedBy", + "style": {}, + "properties": {}, + "fromId": "n4", + "toId": "n5" + }, + { + "id": "n4", + "type": "TrustedForNTAuth", + "style": {}, + "properties": {}, + "fromId": "n4", + "toId": "n6" + }, + { + "id": "n5", + "type": "NTAuthStoreFor", + "style": {}, + "properties": {}, + "fromId": "n6", + "toId": "n7" + }, + { + "id": "n6", + "type": "RootCAFor", + "style": {}, + "properties": {}, + "fromId": "n5", + "toId": "n7" + }, + { + "id": "n7", + "type": "Enroll", + "style": {}, + "properties": {}, + "fromId": "n1", + "toId": "n4" + } + ] +} \ No newline at end of file diff --git a/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.svg b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.svg deleted file mode 100644 index acbab2092..000000000 --- a/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.svg +++ /dev/null @@ -1,18 +0,0 @@ - -MemberOfAdminToCoerceAndRelayNTLMToSMBHasSessioncomputer3ServerAdminscomputer8smb_signing:falseAuthenticatedUsersDomainAdminsUser diff --git a/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMtoADCS.svg b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMtoADCS.svg new file mode 100644 index 000000000..2cc5a25de --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMtoADCS.svg @@ -0,0 +1,18 @@ + +EnrollCoerceAndRelayNTLMToADCSPublishedToIssuedSignedByTrustedForNTAuthNTAuthStoreForRootCAForEnrollAuthenticatedUsers GroupCertTemplate1RequiresManagerApproval :falseAuthenticationEnabled:trueSchemaVersion:1:Computerwebclientrunning:falseEnterpriseCA1webenrollmenthttp:trueRootCANTAuthStoreDomain diff --git a/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.json b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMtoSMB.json similarity index 74% rename from cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.json rename to cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMtoSMB.json index 7b37bf301..6a5474109 100644 --- a/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.json +++ b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMtoSMB.json @@ -13,7 +13,7 @@ "outside-position": "auto", "node-icon-image": "", "node-background-image": "", - "icon-position": "outside", + "icon-position": "inside", "icon-size": 64, "caption-position": "inside", "caption-max-width": 200, @@ -56,58 +56,69 @@ { "id": "n0", "position": { - "x": 0, - "y": 0 + "x": 75, + "y": 431.3359396237863 }, "caption": "computer3", - "style": {}, "labels": [], - "properties": {} + "properties": {}, + "style": {} }, { "id": "n1", "position": { - "x": 284.5, - "y": 0 + "x": 359.5, + "y": 431.3359396237863 }, - "caption": "Server Admins", - "style": {}, + "caption": "Server Admins Group", "labels": [], - "properties": {} + "properties": {}, + "style": {} }, { "id": "n2", "position": { - "x": 485.67187924757275, - "y": -201.17187924757275 + "x": 560.6718792475727, + "y": 230.16406037621357 }, "caption": "computer8", - "style": {}, "labels": [], "properties": { "smb_signing": "false" - } + }, + "style": {} }, { "id": "n3", "position": { - "x": 0, - "y": -201.17187924757275 + "x": 75, + "y": 230.16406037621357 }, - "caption": "Authenticated Users", - "style": {}, + "caption": "Authenticated Users Group", "labels": [], "properties": { "objectid": "authenticatedusers-S-1-5-11" - } + }, + "style": {} }, { "id": "n4", "position": { - "x": 665.8359396237863, - "y": -381.3359396237863 + "x": 740.8359396237863, + "y": 50 + }, + "caption": "Domain Admin User", + "labels": [], + "properties": {}, + "style": {} + }, + { + "id": "n5", + "position": { + "x": 644, + "y": 431.3359396237863 }, - "caption": "Domain Admins User", + "caption": "Domain", "style": {}, "labels": [], "properties": {} @@ -116,35 +127,35 @@ "relationships": [ { "id": "n0", + "fromId": "n0", + "toId": "n1", "type": "MemberOf", - "style": {}, "properties": {}, - "fromId": "n0", - "toId": "n1" + "style": {} }, { "id": "n1", + "fromId": "n1", + "toId": "n2", "type": "AdminTo", - "style": {}, "properties": {}, - "fromId": "n1", - "toId": "n2" + "style": {} }, { "id": "n2", + "fromId": "n3", + "toId": "n2", "type": "CoerceAndRelayNTLMToSMB", - "style": {}, "properties": {}, - "fromId": "n3", - "toId": "n2" + "style": {} }, { "id": "n3", + "fromId": "n2", + "toId": "n4", "type": "HasSession", - "style": {}, "properties": {}, - "fromId": "n2", - "toId": "n4" + "style": {} } ] } \ No newline at end of file diff --git a/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMtoSMB.svg b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMtoSMB.svg new file mode 100644 index 000000000..86efa69ee --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMtoSMB.svg @@ -0,0 +1,18 @@ + +MemberOfAdminToCoerceAndRelayNTLMToSMBHasSessioncomputer3ServerAdminsGroupcomputer8smb_signing:falseAuthenticatedUsers Groupobjectid:authenticatedusers-S-1-5-11DomainAdminUserDomain diff --git a/packages/cue/bh/ad/ad.cue b/packages/cue/bh/ad/ad.cue index 7bca63cf0..3c3c724bc 100644 --- a/packages/cue/bh/ad/ad.cue +++ b/packages/cue/bh/ad/ad.cue @@ -747,6 +747,13 @@ SMBSigning: types.#StringEnum & { representation: "smbsigning" } +WebClientRunning: types.#StringEnum & { + symbol: "WebClientRunning" + schema: "ad" + name: "WebClient Running" + representation: "webclientrunning" +} + RestrictOutboundNTLM: types.#StringEnum & { symbol: "RestrictOutboundNTLM" schema: "ad" @@ -754,6 +761,27 @@ RestrictOutboundNTLM: types.#StringEnum & { representation: "restrictoutboundntlm" } +ADCSWebEnrollmentHTTP: types.#StringEnum & { + symbol: "ADCSWebEnrollmentHTTP" + schema: "ad" + name: "ADCS Web Enrollment HTTP" + representation: "adcswebenrollmenthttp" +} + +ADCSWebEnrollmentHTTPS: types.#StringEnum & { + symbol: "ADCSWebEnrollmentHTTPS" + schema: "ad" + name: "ADCS Web Enrollment HTTPS" + representation: "adcswebenrollmenthttps" +} + +ADCSWebEnrollmentHTTPSEPA: types.#StringEnum & { + symbol: "ADCSWebEnrollmentHTTPSEPA" + schema: "ad" + name: "ADCS Web Enrollment HTTPS EPA" + representation: "adcswebenrollmenthttpsepa" +} + Properties: [ AdminCount, CASecurityCollected, @@ -857,7 +885,11 @@ Properties: [ LockoutDuration, LockoutObservationWindow, SMBSigning, - RestrictOutboundNTLM + WebClientRunning, + RestrictOutboundNTLM, + ADCSWebEnrollmentHTTP, + ADCSWebEnrollmentHTTPS, + ADCSWebEnrollmentHTTPSEPA, ] // Kinds @@ -1314,6 +1346,11 @@ CoerceAndRelayNTLMToSMB: types.#Kind & { schema: "active_directory" } +CoerceAndRelayNTLMToADCS: types.#Kind & { + symbol: "CoerceAndRelayNTLMToADCS" + schema: "active_directory" +} + // Relationship Kinds RelationshipKinds: [ Owns, @@ -1385,6 +1422,7 @@ RelationshipKinds: [ ADCSESC13, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, + CoerceAndRelayNTLMToADCS, ] // ACL Relationships @@ -1463,6 +1501,7 @@ SharedRelationshipKinds: [ ADCSESC13, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, + CoerceAndRelayNTLMToADCS, ] // Edges that are used during inbound traversal @@ -1486,5 +1525,6 @@ EdgeCompositionRelationships: [ ADCSESC10a, ADCSESC10b, ADCSESC13, - CoerceAndRelayNTLMToSMB + CoerceAndRelayNTLMToSMB, + CoerceAndRelayNTLMToADCS, ] diff --git a/packages/go/analysis/ad/adcs.go b/packages/go/analysis/ad/adcs.go index d08cb1479..dbfef2857 100644 --- a/packages/go/analysis/ad/adcs.go +++ b/packages/go/analysis/ad/adcs.go @@ -35,45 +35,35 @@ var ( ) func PostADCS(ctx context.Context, db graph.Database, groupExpansions impact.PathAggregator, adcsEnabled bool) (*analysis.AtomicPostProcessingStats, error) { - if enterpriseCertAuthorities, err := FetchNodesByKind(ctx, db, ad.EnterpriseCA); err != nil { - return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed fetching enterpriseCA nodes: %w", err) - } else if rootCertAuthorities, err := FetchNodesByKind(ctx, db, ad.RootCA); err != nil { + var cache = NewADCSCache() + if rootCertAuthorities, err := FetchNodesByKind(ctx, db, ad.RootCA); err != nil { return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed fetching rootCA nodes: %w", err) } else if aiaCertAuthorities, err := FetchNodesByKind(ctx, db, ad.AIACA); err != nil { return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed fetching AIACA nodes: %w", err) - } else if certTemplates, err := FetchNodesByKind(ctx, db, ad.CertTemplate); err != nil { - return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed fetching cert template nodes: %w", err) - } else if domains, err := FetchNodesByKind(ctx, db, ad.Domain); err != nil { - return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed fetching domain nodes: %w", err) - } else if step1Stats, err := postADCSPreProcessStep1(ctx, db, enterpriseCertAuthorities, rootCertAuthorities, aiaCertAuthorities, certTemplates); err != nil { + } else if err := cache.BuildCache(ctx, db); err != nil { + return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed building ADCS cache: %w", err) + } else if step1Stats, err := postADCSPreProcessStep1(ctx, db, cache.GetEnterpriseCertAuthorities(), rootCertAuthorities, aiaCertAuthorities, cache.GetCertTemplates()); err != nil { return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed adcs pre-processing step 1: %w", err) + } else if step2Stats, err := postADCSPreProcessStep2(ctx, db, cache); err != nil { + return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed adcs pre-processing step 2: %w", err) } else { - var cache = NewADCSCache() - cache.BuildCache(ctx, db, enterpriseCertAuthorities, certTemplates, domains) + operation := analysis.NewPostRelationshipOperation(ctx, db, "ADCS Post Processing") - if step2Stats, err := postADCSPreProcessStep2(ctx, db, domains, enterpriseCertAuthorities, certTemplates, cache); err != nil { - return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed adcs pre-processing step 2: %w", err) - } else { + operation.Stats.Merge(step1Stats) + operation.Stats.Merge(step2Stats) - operation := analysis.NewPostRelationshipOperation(ctx, db, "ADCS Post Processing") + for _, domain := range cache.GetDomains() { + innerDomain := domain - operation.Stats.Merge(step1Stats) - operation.Stats.Merge(step2Stats) + for _, enterpriseCA := range cache.GetEnterpriseCertAuthorities() { + innerEnterpriseCA := enterpriseCA - for _, domain := range domains { - innerDomain := domain - - for _, enterpriseCA := range enterpriseCertAuthorities { - innerEnterpriseCA := enterpriseCA - - if cache.DoesCAChainProperlyToDomain(innerEnterpriseCA, innerDomain) { - processEnterpriseCAWithValidCertChainToDomain(innerEnterpriseCA, innerDomain, groupExpansions, cache, operation) - } + if cache.DoesCAChainProperlyToDomain(innerEnterpriseCA, innerDomain) { + processEnterpriseCAWithValidCertChainToDomain(innerEnterpriseCA, innerDomain, groupExpansions, cache, operation) } } - return &operation.Stats, operation.Done() - } + return &operation.Stats, operation.Done() } } @@ -100,10 +90,10 @@ func postADCSPreProcessStep1(ctx context.Context, db graph.Database, enterpriseC } // postADCSPreProcessStep2 Processes the edges that are dependent on those processed in postADCSPreProcessStep1 -func postADCSPreProcessStep2(ctx context.Context, db graph.Database, domains, enterpriseCertAuthorities, certTemplates []*graph.Node, cache ADCSCache) (*analysis.AtomicPostProcessingStats, error) { +func postADCSPreProcessStep2(ctx context.Context, db graph.Database, cache ADCSCache) (*analysis.AtomicPostProcessingStats, error) { operation := analysis.NewPostRelationshipOperation(ctx, db, "ADCS Post Processing Step 2") - if err := PostEnrollOnBehalfOf(domains, enterpriseCertAuthorities, certTemplates, cache, operation); err != nil { + if err := PostEnrollOnBehalfOf(cache, operation); err != nil { operation.Done() return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed post processing for %s: %w", ad.EnrollOnBehalfOf.String(), err) } else { @@ -112,7 +102,6 @@ func postADCSPreProcessStep2(ctx context.Context, db graph.Database, domains, en } func processEnterpriseCAWithValidCertChainToDomain(enterpriseCA, domain *graph.Node, groupExpansions impact.PathAggregator, cache ADCSCache, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob]) { - operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { if err := PostGoldenCert(ctx, tx, outC, domain, enterpriseCA); errors.Is(err, graph.ErrPropertyNotFound) { slog.WarnContext(ctx, fmt.Sprintf("Post processing for %s: %v", ad.GoldenCert.String(), err)) diff --git a/packages/go/analysis/ad/adcscache.go b/packages/go/analysis/ad/adcscache.go index 05e245795..c510fcf2d 100644 --- a/packages/go/analysis/ad/adcscache.go +++ b/packages/go/analysis/ad/adcscache.go @@ -31,6 +31,10 @@ import ( type ADCSCache struct { mu *sync.RWMutex + enterpriseCertAuthorities []*graph.Node + certTemplates []*graph.Node + domains []*graph.Node + // To discourage direct access without getting a read lock, these are private authStoreForChainValid map[graph.ID]cardinality.Duplex[uint64] rootCAForChainValid map[graph.ID]cardinality.Duplex[uint64] @@ -62,12 +66,23 @@ func NewADCSCache() ADCSCache { } } -func (s *ADCSCache) BuildCache(ctx context.Context, db graph.Database, enterpriseCAs, certTemplates, domains []*graph.Node) { +func (s *ADCSCache) BuildCache(ctx context.Context, db graph.Database) error { s.mu.Lock() defer s.mu.Unlock() err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { - for _, ct := range certTemplates { + if enterpriseCertAuthorities, err := FetchNodesByKind(ctx, db, ad.EnterpriseCA); err != nil { + return fmt.Errorf("failed fetching enterpriseCA nodes: %w", err) + } else if certTemplates, err := FetchNodesByKind(ctx, db, ad.CertTemplate); err != nil { + return fmt.Errorf("failed fetching certTemplate nodes: %w", err) + } else if domains, err := FetchNodesByKind(ctx, db, ad.Domain); err != nil { + return fmt.Errorf("failed fetching domain nodes: %w", err) + } else { + s.certTemplates = certTemplates + s.enterpriseCertAuthorities = enterpriseCertAuthorities + s.domains = domains + } + for _, ct := range s.certTemplates { // cert template enrollers if firstDegreePrincipals, err := fetchFirstDegreeNodes(tx, ct, ad.Enroll, ad.GenericAll, ad.AllExtendedRights); err != nil { slog.ErrorContext(ctx, fmt.Sprintf("Error fetching enrollers for cert template %d: %v", ct.ID, err)) @@ -93,7 +108,7 @@ func (s *ADCSCache) BuildCache(ctx context.Context, db graph.Database, enterpris } - for _, eca := range enterpriseCAs { + for _, eca := range s.enterpriseCertAuthorities { if firstDegreeEnrollers, err := fetchFirstDegreeNodes(tx, eca, ad.Enroll); err != nil { slog.ErrorContext(ctx, fmt.Sprintf("Error fetching enrollers for enterprise ca %d: %v", eca.ID, err)) } else { @@ -116,7 +131,7 @@ func (s *ADCSCache) BuildCache(ctx context.Context, db graph.Database, enterpris } } - for _, domain := range domains { + for _, domain := range s.domains { if rootCaForNodes, err := FetchEnterpriseCAsRootCAForPathToDomain(tx, domain); err != nil { slog.ErrorContext(ctx, fmt.Sprintf("Error getting cas via rootcafor for domain %d: %v", domain.ID, err)) } else if authStoreForNodes, err := FetchEnterpriseCAsTrustedForNTAuthToDomain(tx, domain); err != nil { @@ -148,6 +163,7 @@ func (s *ADCSCache) BuildCache(ctx context.Context, db graph.Database, enterpris } slog.InfoContext(ctx, "Finished building adcs cache") + return err } func (s *ADCSCache) DoesCAChainProperlyToDomain(enterpriseCA, domain *graph.Node) bool { @@ -243,6 +259,27 @@ func (s *ADCSCache) HasWeakCertBindingInForest(id uint64) bool { return s.hasWeakCertBindingInForest.Contains(id) } +func (s *ADCSCache) GetEnterpriseCertAuthorities() []*graph.Node { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.enterpriseCertAuthorities +} + +func (s *ADCSCache) GetCertTemplates() []*graph.Node { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.certTemplates +} + +func (s *ADCSCache) GetDomains() []*graph.Node { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.domains +} + func hasUPNCertMappingInForest(tx graph.Transaction, domain *graph.Node) (bool, error) { if trustedByNodes, err := FetchNodesWithTrustedByParentChildRelationship(tx, domain); err != nil { return false, err diff --git a/packages/go/analysis/ad/esc3.go b/packages/go/analysis/ad/esc3.go index ea03be076..0083ca8af 100644 --- a/packages/go/analysis/ad/esc3.go +++ b/packages/go/analysis/ad/esc3.go @@ -142,10 +142,10 @@ func PostADCSESC3(ctx context.Context, tx graph.Transaction, outC chan<- analysi return nil } -func PostEnrollOnBehalfOf(domains, enterpriseCertAuthorities, certTemplates []*graph.Node, cache ADCSCache, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob]) error { +func PostEnrollOnBehalfOf(cache ADCSCache, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob]) error { versionOneTemplates := make([]*graph.Node, 0) versionTwoTemplates := make([]*graph.Node, 0) - for _, node := range certTemplates { + for _, node := range cache.GetCertTemplates() { if version, err := node.Properties.Get(ad.SchemaVersion.String()).Float64(); errors.Is(err, graph.ErrPropertyNotFound) { slog.Warn(fmt.Sprintf("Did not get schema version for cert template %d: %v", node.ID, err)) } else if err != nil { @@ -159,10 +159,10 @@ func PostEnrollOnBehalfOf(domains, enterpriseCertAuthorities, certTemplates []*g } } - for _, domain := range domains { + for _, domain := range cache.GetDomains() { innerDomain := domain - for _, enterpriseCA := range enterpriseCertAuthorities { + for _, enterpriseCA := range cache.GetEnterpriseCertAuthorities() { innerEnterpriseCA := enterpriseCA if cache.DoesCAChainProperlyToDomain(innerEnterpriseCA, innerDomain) { diff --git a/packages/go/analysis/ad/ntlm.go b/packages/go/analysis/ad/ntlm.go index d505202cc..05fd48b58 100644 --- a/packages/go/analysis/ad/ntlm.go +++ b/packages/go/analysis/ad/ntlm.go @@ -34,21 +34,27 @@ import ( // PostNTLM is the initial function used to execute our NTLM analysis func PostNTLM(ctx context.Context, db graph.Database, groupExpansions impact.PathAggregator) (*analysis.AtomicPostProcessingStats, error) { - operation := analysis.NewPostRelationshipOperation(ctx, db, "PostNTLM") + var ( + adcsComputerCache = make(map[string]cardinality.Duplex[uint64]) + operation = analysis.NewPostRelationshipOperation(ctx, db, "PostNTLM") + authenticatedUsersCache = make(map[string]graph.ID) + ) // TODO: after adding all of our new NTLM edges, benchmark performance between submitting multiple readers per computer or single reader per computer err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { - // Fetch all nodes where the node is a Group and is an Authenticated User - if authenticatedUsersCache, err := FetchAuthUsersMappedToDomains(tx); err != nil { + if innerAuthenticatedUsersCache, err := FetchAuthUsersMappedToDomains(tx); err != nil { return err } else { + authenticatedUsersCache = innerAuthenticatedUsersCache // Fetch all nodes where the type is Computer return tx.Nodes().Filter(query.Kind(query.Node(), ad.Computer)).Fetch(func(cursor graph.Cursor[*graph.Node]) error { for computer := range cursor.Chan() { innerComputer := computer - if domain, err := innerComputer.Properties.Get(ad.Domain.String()).String(); err != nil { + domain, err := innerComputer.Properties.Get(ad.DomainSID.String()).String() + + if err != nil { continue } else if authenticatedUserID, ok := authenticatedUsersCache[domain]; !ok { continue @@ -59,6 +65,14 @@ func PostNTLM(ctx context.Context, db graph.Database, groupExpansions impact.Pat // Additional analysis may occur if one of our analysis errors continue } + + if webclientRunning, err := innerComputer.Properties.Get(ad.WebClientRunning.String()).Bool(); err != nil && !errors.Is(err, graph.ErrPropertyNotFound) { + slog.WarnContext(ctx, fmt.Sprintf("Error getting webclientrunningproperty from computer %d", innerComputer.ID)) + } else if restrictOutboundNtlm, err := innerComputer.Properties.Get(ad.RestrictOutboundNTLM.String()).Bool(); err != nil && !errors.Is(err, graph.ErrPropertyNotFound) { + slog.WarnContext(ctx, fmt.Sprintf("Error getting restrictoutboundntlm from computer %d", innerComputer.ID)) + } else if webclientRunning && !restrictOutboundNtlm { + adcsComputerCache[domain].Add(innerComputer.ID.Uint64()) + } } return cursor.Error() @@ -70,10 +84,126 @@ func PostNTLM(ctx context.Context, db graph.Database, groupExpansions impact.Pat return nil, err } + if err := PostCoerceAndRelayNTLMToADCS(ctx, db, operation, authenticatedUsersCache, adcsComputerCache); err != nil { + return nil, err + } + return &operation.Stats, operation.Done() } -// PostCoerceAndRelayNTLMToSMB creates edges that allow a computer with unrolled admin access to one or more computers where SMB signing is disabled. +func PostCoerceAndRelayNTLMToADCS(ctx context.Context, db graph.Database, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob], authUsersCache map[string]graph.ID, adcsComputerCache map[string]cardinality.Duplex[uint64]) error { + adcsCache := NewADCSCache() + if err := adcsCache.BuildCache(ctx, db); err != nil { + return err + } + for _, outerDomain := range adcsCache.domains { + for _, outerEnterpriseCA := range adcsCache.GetEnterpriseCertAuthorities() { + domain := outerDomain + enterpriseCA := outerEnterpriseCA + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + if publishedCertTemplates := adcsCache.GetPublishedTemplateCache(enterpriseCA.ID); len(publishedCertTemplates) == 0 { + //If this enterprise CA has no published templates, then there's no reason to check further + return nil + } else if !adcsCache.DoesCAChainProperlyToDomain(enterpriseCA, domain) { + //If the CA doesn't chain up to the domain properly then its invalid + return nil + } else if ecaValid, err := isEnterpriseCAValidForADCS(enterpriseCA); err != nil { + slog.ErrorContext(ctx, fmt.Sprintf("Error validating EnterpriseCA %d for ADCS relay: %v", enterpriseCA.ID, err)) + return nil + } else if !ecaValid { + //Check some prereqs on the enterprise CA. If the enterprise CA is invalid, we can fast skip it + return nil + } else if domainsid, err := domain.Properties.Get(ad.DomainSID.String()).String(); err != nil { + slog.WarnContext(ctx, fmt.Sprintf("Error getting domainsid for domain %d: %v", domain.ID, err)) + return nil + } else if authUsersGroup, ok := authUsersCache[domainsid]; !ok { + //If we cant find an auth users group for this domain then we're not going to be able to make an edge regardless + slog.WarnContext(ctx, fmt.Sprintf("Unable to find auth users group for domain %s", domainsid)) + return nil + } else { + //If auth users doesn't have enroll rights here than it's not valid either. Unroll enrollers into a slice and check if auth users is in it + ecaEnrollers := adcsCache.GetEnterpriseCAEnrollers(enterpriseCA.ID) + authUsersHasEnrollmentRights := false + for _, l := range ecaEnrollers { + if l.ID == authUsersGroup { + authUsersHasEnrollmentRights = true + break + } + } + + if !authUsersHasEnrollmentRights { + return nil + } + + for _, certTemplate := range publishedCertTemplates { + if valid, err := isCertTemplateValidForADCSRelay(certTemplate); err != nil { + slog.ErrorContext(ctx, fmt.Sprintf("Error validating cert template %d for NTLM ADCS relay: %v", certTemplate.ID, err)) + continue + } else if !valid { + continue + } else if computers, ok := adcsComputerCache[domainsid]; !ok { + continue + } else { + computers.Each(func(value uint64) bool { + outC <- analysis.CreatePostRelationshipJob{ + FromID: authUsersGroup, + ToID: graph.ID(value), + Kind: ad.CoerceAndRelayNTLMToADCS, + } + return true + }) + } + } + + return nil + } + }) + } + } + + return nil +} + +func isEnterpriseCAValidForADCS(eca *graph.Node) (bool, error) { + if httpEnrollment, err := eca.Properties.Get(ad.ADCSWebEnrollmentHTTP.String()).Bool(); err != nil && !errors.Is(err, graph.ErrPropertyNotFound) { + return false, err + } else if httpEnrollment { + return true, nil + } else if httpsEnrollment, err := eca.Properties.Get(ad.ADCSWebEnrollmentHTTPS.String()).Bool(); err != nil && !errors.Is(err, graph.ErrPropertyNotFound) { + return false, err + } else if !httpsEnrollment { + return false, nil + } else if httpsEnrollmentEpa, err := eca.Properties.Get(ad.ADCSWebEnrollmentHTTPSEPA.String()).Bool(); err != nil { + if errors.Is(err, graph.ErrPropertyNotFound) { + return false, nil + } + return false, err + } else { + return !httpsEnrollmentEpa, nil + } +} + +func isCertTemplateValidForADCSRelay(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 schemaVersion, err := ct.Properties.Get(ad.SchemaVersion.String()).Float64(); err != nil { + return false, err + } else if schemaVersion <= 1 { + return true, nil + } else if authorizedSignatures, err := ct.Properties.Get(ad.AuthorizedSignatures.String()).Float64(); err != nil { + return false, err + } else { + return authorizedSignatures == 0, nil + } +} + +// PostCoerceAndRelayNtlmToSmb creates edges that allow a computer with unrolled admin access to one or more computers where SMB signing is disabled. // Comprised solely of adminTo and memberOf edges func PostCoerceAndRelayNTLMToSMB(tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob, expandedGroups impact.PathAggregator, computer *graph.Node, authenticatedUserID graph.ID) error { if smbSigningEnabled, err := computer.Properties.Get(ad.SMBSigning.String()).Bool(); errors.Is(err, graph.ErrPropertyNotFound) { @@ -85,7 +215,6 @@ func PostCoerceAndRelayNTLMToSMB(tx graph.Transaction, outC chan<- analysis.Crea } else if err != nil { return err } else if !smbSigningEnabled && !restrictOutboundNtlm { - // Fetch the admins with edges to the provided computer if firstDegreeAdmins, err := fetchFirstDegreeNodes(tx, computer, ad.AdminTo); err != nil { return err @@ -98,7 +227,7 @@ func PostCoerceAndRelayNTLMToSMB(tx graph.Transaction, outC chan<- analysis.Crea } else { allAdminGroups := cardinality.NewBitmap64() for group := range firstDegreeAdmins.ContainingNodeKinds(ad.Group) { - allAdminGroups.And(expandedGroups.Cardinality(group.Uint64())) + allAdminGroups.Or(expandedGroups.Cardinality(group.Uint64())) } // Fetch nodes where the node id is in our allAdminGroups bitmap and are of type Computer @@ -132,7 +261,7 @@ func FetchAuthUsersMappedToDomains(tx graph.Transaction) (map[string]graph.ID, e query.StringEndsWith(query.NodeProperty(common.ObjectID.String()), AuthenticatedUsersSuffix)), ).Fetch(func(cursor graph.Cursor[*graph.Node]) error { for authenticatedUser := range cursor.Chan() { - if domain, err := authenticatedUser.Properties.Get(ad.Domain.String()).String(); err != nil { + if domain, err := authenticatedUser.Properties.Get(ad.DomainSID.String()).String(); err != nil { continue } else { authenticatedUsers[domain] = authenticatedUser.ID diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go index 478c42ce2..2aa5bd090 100644 --- a/packages/go/graphschema/ad/ad.go +++ b/packages/go/graphschema/ad/ad.go @@ -111,6 +111,7 @@ var ( ADCSESC13 = graph.StringKind("ADCSESC13") SyncedToEntraUser = graph.StringKind("SyncedToEntraUser") CoerceAndRelayNTLMToSMB = graph.StringKind("CoerceAndRelayNTLMToSMB") + CoerceAndRelayNTLMToADCS = graph.StringKind("CoerceAndRelayNTLMToADCS") ) type Property string @@ -218,11 +219,15 @@ const ( LockoutDuration Property = "lockoutduration" LockoutObservationWindow Property = "lockoutobservationwindow" SMBSigning Property = "smbsigning" + WebClientRunning Property = "webclientrunning" RestrictOutboundNTLM Property = "restrictoutboundntlm" + ADCSWebEnrollmentHTTP Property = "adcswebenrollmenthttp" + ADCSWebEnrollmentHTTPS Property = "adcswebenrollmenthttps" + ADCSWebEnrollmentHTTPSEPA Property = "adcswebenrollmenthttpsepa" ) func AllProperties() []Property { - return []Property{AdminCount, CASecurityCollected, CAName, CertChain, CertName, CertThumbprint, CertThumbprints, HasEnrollmentAgentRestrictions, EnrollmentAgentRestrictionsCollected, IsUserSpecifiesSanEnabled, IsUserSpecifiesSanEnabledCollected, RoleSeparationEnabled, RoleSeparationEnabledCollected, HasBasicConstraints, BasicConstraintPathLength, UnresolvedPublishedTemplates, DNSHostname, CrossCertificatePair, DistinguishedName, DomainFQDN, DomainSID, Sensitive, HighValue, BlocksInheritance, IsACL, IsACLProtected, IsDeleted, Enforced, Department, HasCrossCertificatePair, HasSPN, UnconstrainedDelegation, LastLogon, LastLogonTimestamp, IsPrimaryGroup, HasLAPS, DontRequirePreAuth, LogonType, HasURA, PasswordNeverExpires, PasswordNotRequired, FunctionalLevel, TrustType, SidFiltering, TrustedToAuth, SamAccountName, CertificateMappingMethodsRaw, CertificateMappingMethods, StrongCertificateBindingEnforcementRaw, StrongCertificateBindingEnforcement, EKUs, SubjectAltRequireUPN, SubjectAltRequireDNS, SubjectAltRequireDomainDNS, SubjectAltRequireEmail, SubjectAltRequireSPN, SubjectRequireEmail, AuthorizedSignatures, ApplicationPolicies, IssuancePolicies, SchemaVersion, RequiresManagerApproval, AuthenticationEnabled, SchannelAuthenticationEnabled, EnrolleeSuppliesSubject, CertificateApplicationPolicy, CertificateNameFlag, EffectiveEKUs, EnrollmentFlag, Flags, NoSecurityExtension, RenewalPeriod, ValidityPeriod, OID, HomeDirectory, CertificatePolicy, CertTemplateOID, GroupLinkID, ObjectGUID, ExpirePasswordsOnSmartCardOnlyAccounts, MachineAccountQuota, SupportedKerberosEncryptionTypes, TGTDelegationEnabled, PasswordStoredUsingReversibleEncryption, SmartcardRequired, UseDESKeyOnly, LogonScriptEnabled, LockedOut, UserCannotChangePassword, PasswordExpired, DSHeuristics, UserAccountControl, TrustAttributes, MinPwdLength, PwdProperties, PwdHistoryLength, LockoutThreshold, MinPwdAge, MaxPwdAge, LockoutDuration, LockoutObservationWindow, SMBSigning, RestrictOutboundNTLM} + return []Property{AdminCount, CASecurityCollected, CAName, CertChain, CertName, CertThumbprint, CertThumbprints, HasEnrollmentAgentRestrictions, EnrollmentAgentRestrictionsCollected, IsUserSpecifiesSanEnabled, IsUserSpecifiesSanEnabledCollected, RoleSeparationEnabled, RoleSeparationEnabledCollected, HasBasicConstraints, BasicConstraintPathLength, UnresolvedPublishedTemplates, DNSHostname, CrossCertificatePair, DistinguishedName, DomainFQDN, DomainSID, Sensitive, HighValue, BlocksInheritance, IsACL, IsACLProtected, IsDeleted, Enforced, Department, HasCrossCertificatePair, HasSPN, UnconstrainedDelegation, LastLogon, LastLogonTimestamp, IsPrimaryGroup, HasLAPS, DontRequirePreAuth, LogonType, HasURA, PasswordNeverExpires, PasswordNotRequired, FunctionalLevel, TrustType, SidFiltering, TrustedToAuth, SamAccountName, CertificateMappingMethodsRaw, CertificateMappingMethods, StrongCertificateBindingEnforcementRaw, StrongCertificateBindingEnforcement, EKUs, SubjectAltRequireUPN, SubjectAltRequireDNS, SubjectAltRequireDomainDNS, SubjectAltRequireEmail, SubjectAltRequireSPN, SubjectRequireEmail, AuthorizedSignatures, ApplicationPolicies, IssuancePolicies, SchemaVersion, RequiresManagerApproval, AuthenticationEnabled, SchannelAuthenticationEnabled, EnrolleeSuppliesSubject, CertificateApplicationPolicy, CertificateNameFlag, EffectiveEKUs, EnrollmentFlag, Flags, NoSecurityExtension, RenewalPeriod, ValidityPeriod, OID, HomeDirectory, CertificatePolicy, CertTemplateOID, GroupLinkID, ObjectGUID, ExpirePasswordsOnSmartCardOnlyAccounts, MachineAccountQuota, SupportedKerberosEncryptionTypes, TGTDelegationEnabled, PasswordStoredUsingReversibleEncryption, SmartcardRequired, UseDESKeyOnly, LogonScriptEnabled, LockedOut, UserCannotChangePassword, PasswordExpired, DSHeuristics, UserAccountControl, TrustAttributes, MinPwdLength, PwdProperties, PwdHistoryLength, LockoutThreshold, MinPwdAge, MaxPwdAge, LockoutDuration, LockoutObservationWindow, SMBSigning, WebClientRunning, RestrictOutboundNTLM, ADCSWebEnrollmentHTTP, ADCSWebEnrollmentHTTPS, ADCSWebEnrollmentHTTPSEPA} } func ParseProperty(source string) (Property, error) { switch source { @@ -430,8 +435,16 @@ func ParseProperty(source string) (Property, error) { return LockoutObservationWindow, nil case "smbsigning": return SMBSigning, nil + case "webclientrunning": + return WebClientRunning, nil case "restrictoutboundntlm": return RestrictOutboundNTLM, nil + case "adcswebenrollmenthttp": + return ADCSWebEnrollmentHTTP, nil + case "adcswebenrollmenthttps": + return ADCSWebEnrollmentHTTPS, nil + case "adcswebenrollmenthttpsepa": + return ADCSWebEnrollmentHTTPSEPA, nil default: return "", errors.New("Invalid enumeration value: " + source) } @@ -642,8 +655,16 @@ func (s Property) String() string { return string(LockoutObservationWindow) case SMBSigning: return string(SMBSigning) + case WebClientRunning: + return string(WebClientRunning) case RestrictOutboundNTLM: return string(RestrictOutboundNTLM) + case ADCSWebEnrollmentHTTP: + return string(ADCSWebEnrollmentHTTP) + case ADCSWebEnrollmentHTTPS: + return string(ADCSWebEnrollmentHTTPS) + case ADCSWebEnrollmentHTTPSEPA: + return string(ADCSWebEnrollmentHTTPSEPA) default: return "Invalid enumeration case: " + string(s) } @@ -854,8 +875,16 @@ func (s Property) Name() string { return "Lockout Observation Window" case SMBSigning: return "SMB Signing" + case WebClientRunning: + return "WebClient Running" case RestrictOutboundNTLM: return "Restrict Outbound NTLM" + case ADCSWebEnrollmentHTTP: + return "ADCS Web Enrollment HTTP" + case ADCSWebEnrollmentHTTPS: + return "ADCS Web Enrollment HTTPS" + case ADCSWebEnrollmentHTTPSEPA: + return "ADCS Web Enrollment HTTPS EPA" default: return "Invalid enumeration case: " + string(s) } @@ -872,19 +901,19 @@ func Nodes() []graph.Kind { return []graph.Kind{Entity, User, Computer, Group, GPO, OU, Container, Domain, LocalGroup, LocalUser, AIACA, RootCA, EnterpriseCA, NTAuthStore, CertTemplate, IssuancePolicy} } func Relationships() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, CoerceToTGT, GetChanges, GetChangesAll, GetChangesInFilteredSet, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, LocalToComputer, MemberOfLocalGroup, RemoteInteractiveLogonRight, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, RootCAFor, DCFor, PublishedTo, ManageCertificates, ManageCA, DelegatedEnrollmentAgent, Enroll, HostsCAService, WritePKIEnrollmentFlag, WritePKINameFlag, NTAuthStoreFor, TrustedForNTAuth, EnterpriseCAFor, IssuedSignedBy, GoldenCert, EnrollOnBehalfOf, OIDGroupLink, ExtendedByPolicy, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToEntraUser, CoerceAndRelayNTLMToSMB} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, CoerceToTGT, GetChanges, GetChangesAll, GetChangesInFilteredSet, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, LocalToComputer, MemberOfLocalGroup, RemoteInteractiveLogonRight, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, RootCAFor, DCFor, PublishedTo, ManageCertificates, ManageCA, DelegatedEnrollmentAgent, Enroll, HostsCAService, WritePKIEnrollmentFlag, WritePKINameFlag, NTAuthStoreFor, TrustedForNTAuth, EnterpriseCAFor, IssuedSignedBy, GoldenCert, EnrollOnBehalfOf, OIDGroupLink, ExtendedByPolicy, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS} } 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, WriteGPLink, SyncLAPSPassword, DCSync, ManageCertificates, ManageCA, Enroll, WritePKIEnrollmentFlag, WritePKINameFlag} } func PathfindingRelationships() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, Contains, DCFor, TrustedBy} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, Contains, DCFor, TrustedBy} } func InboundRelationshipKinds() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, Contains} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, Contains} } func OutboundRelationshipKinds() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, Contains, DCFor} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, Contains, DCFor} } func IsACLKind(s graph.Kind) bool { for _, acl := range ACLRelationships() { diff --git a/packages/go/graphschema/common/common.go b/packages/go/graphschema/common/common.go index b88cdfabf..33d32b259 100644 --- a/packages/go/graphschema/common/common.go +++ b/packages/go/graphschema/common/common.go @@ -41,10 +41,10 @@ func NodeKinds() []graph.Kind { return []graph.Kind{MigrationData} } func InboundRelationshipKinds() []graph.Kind { - return []graph.Kind{ad.Owns, ad.GenericAll, ad.GenericWrite, ad.WriteOwner, ad.WriteDACL, ad.MemberOf, ad.ForceChangePassword, ad.AllExtendedRights, ad.AddMember, ad.HasSession, ad.GPLink, ad.AllowedToDelegate, ad.CoerceToTGT, ad.AllowedToAct, ad.AdminTo, ad.CanPSRemote, ad.CanRDP, ad.ExecuteDCOM, ad.HasSIDHistory, ad.AddSelf, ad.DCSync, ad.ReadLAPSPassword, ad.ReadGMSAPassword, ad.DumpSMSAPassword, ad.SQLAdmin, ad.AddAllowedToAct, ad.WriteSPN, ad.AddKeyCredentialLink, ad.SyncLAPSPassword, ad.WriteAccountRestrictions, ad.WriteGPLink, ad.GoldenCert, ad.ADCSESC1, ad.ADCSESC3, ad.ADCSESC4, ad.ADCSESC6a, ad.ADCSESC6b, ad.ADCSESC9a, ad.ADCSESC9b, ad.ADCSESC10a, ad.ADCSESC10b, ad.ADCSESC13, ad.SyncedToEntraUser, ad.CoerceAndRelayNTLMToSMB, ad.Contains, azure.AvereContributor, azure.Contributor, azure.GetCertificates, azure.GetKeys, azure.GetSecrets, azure.HasRole, azure.MemberOf, azure.Owner, azure.RunsAs, azure.VMContributor, azure.AutomationContributor, azure.KeyVaultContributor, azure.VMAdminLogin, azure.AddMembers, azure.AddSecret, azure.ExecuteCommand, azure.GlobalAdmin, azure.PrivilegedAuthAdmin, azure.Grant, azure.GrantSelf, azure.PrivilegedRoleAdmin, azure.ResetPassword, azure.UserAccessAdministrator, azure.Owns, azure.CloudAppAdmin, azure.AppAdmin, azure.AddOwner, azure.ManagedIdentity, azure.AKSContributor, azure.NodeResourceGroup, azure.WebsiteContributor, azure.LogicAppContributor, azure.AZMGAddMember, azure.AZMGAddOwner, azure.AZMGAddSecret, azure.AZMGGrantAppRoles, azure.AZMGGrantRole, azure.SyncedToADUser} + return []graph.Kind{ad.Owns, ad.GenericAll, ad.GenericWrite, ad.WriteOwner, ad.WriteDACL, ad.MemberOf, ad.ForceChangePassword, ad.AllExtendedRights, ad.AddMember, ad.HasSession, ad.GPLink, ad.AllowedToDelegate, ad.CoerceToTGT, ad.AllowedToAct, ad.AdminTo, ad.CanPSRemote, ad.CanRDP, ad.ExecuteDCOM, ad.HasSIDHistory, ad.AddSelf, ad.DCSync, ad.ReadLAPSPassword, ad.ReadGMSAPassword, ad.DumpSMSAPassword, ad.SQLAdmin, ad.AddAllowedToAct, ad.WriteSPN, ad.AddKeyCredentialLink, ad.SyncLAPSPassword, ad.WriteAccountRestrictions, ad.WriteGPLink, ad.GoldenCert, ad.ADCSESC1, ad.ADCSESC3, ad.ADCSESC4, ad.ADCSESC6a, ad.ADCSESC6b, ad.ADCSESC9a, ad.ADCSESC9b, ad.ADCSESC10a, ad.ADCSESC10b, ad.ADCSESC13, ad.SyncedToEntraUser, ad.CoerceAndRelayNTLMToSMB, ad.CoerceAndRelayNTLMToADCS, ad.Contains, azure.AvereContributor, azure.Contributor, azure.GetCertificates, azure.GetKeys, azure.GetSecrets, azure.HasRole, azure.MemberOf, azure.Owner, azure.RunsAs, azure.VMContributor, azure.AutomationContributor, azure.KeyVaultContributor, azure.VMAdminLogin, azure.AddMembers, azure.AddSecret, azure.ExecuteCommand, azure.GlobalAdmin, azure.PrivilegedAuthAdmin, azure.Grant, azure.GrantSelf, azure.PrivilegedRoleAdmin, azure.ResetPassword, azure.UserAccessAdministrator, azure.Owns, azure.CloudAppAdmin, azure.AppAdmin, azure.AddOwner, azure.ManagedIdentity, azure.AKSContributor, azure.NodeResourceGroup, azure.WebsiteContributor, azure.LogicAppContributor, azure.AZMGAddMember, azure.AZMGAddOwner, azure.AZMGAddSecret, azure.AZMGGrantAppRoles, azure.AZMGGrantRole, azure.SyncedToADUser} } func OutboundRelationshipKinds() []graph.Kind { - return []graph.Kind{ad.Owns, ad.GenericAll, ad.GenericWrite, ad.WriteOwner, ad.WriteDACL, ad.MemberOf, ad.ForceChangePassword, ad.AllExtendedRights, ad.AddMember, ad.HasSession, ad.GPLink, ad.AllowedToDelegate, ad.CoerceToTGT, ad.AllowedToAct, ad.AdminTo, ad.CanPSRemote, ad.CanRDP, ad.ExecuteDCOM, ad.HasSIDHistory, ad.AddSelf, ad.DCSync, ad.ReadLAPSPassword, ad.ReadGMSAPassword, ad.DumpSMSAPassword, ad.SQLAdmin, ad.AddAllowedToAct, ad.WriteSPN, ad.AddKeyCredentialLink, ad.SyncLAPSPassword, ad.WriteAccountRestrictions, ad.WriteGPLink, ad.GoldenCert, ad.ADCSESC1, ad.ADCSESC3, ad.ADCSESC4, ad.ADCSESC6a, ad.ADCSESC6b, ad.ADCSESC9a, ad.ADCSESC9b, ad.ADCSESC10a, ad.ADCSESC10b, ad.ADCSESC13, ad.SyncedToEntraUser, ad.CoerceAndRelayNTLMToSMB, ad.Contains, ad.DCFor, azure.AvereContributor, azure.Contributor, azure.GetCertificates, azure.GetKeys, azure.GetSecrets, azure.HasRole, azure.MemberOf, azure.Owner, azure.RunsAs, azure.VMContributor, azure.AutomationContributor, azure.KeyVaultContributor, azure.VMAdminLogin, azure.AddMembers, azure.AddSecret, azure.ExecuteCommand, azure.GlobalAdmin, azure.PrivilegedAuthAdmin, azure.Grant, azure.GrantSelf, azure.PrivilegedRoleAdmin, azure.ResetPassword, azure.UserAccessAdministrator, azure.Owns, azure.CloudAppAdmin, azure.AppAdmin, azure.AddOwner, azure.ManagedIdentity, azure.AKSContributor, azure.NodeResourceGroup, azure.WebsiteContributor, azure.LogicAppContributor, azure.AZMGAddMember, azure.AZMGAddOwner, azure.AZMGAddSecret, azure.AZMGGrantAppRoles, azure.AZMGGrantRole, azure.SyncedToADUser} + return []graph.Kind{ad.Owns, ad.GenericAll, ad.GenericWrite, ad.WriteOwner, ad.WriteDACL, ad.MemberOf, ad.ForceChangePassword, ad.AllExtendedRights, ad.AddMember, ad.HasSession, ad.GPLink, ad.AllowedToDelegate, ad.CoerceToTGT, ad.AllowedToAct, ad.AdminTo, ad.CanPSRemote, ad.CanRDP, ad.ExecuteDCOM, ad.HasSIDHistory, ad.AddSelf, ad.DCSync, ad.ReadLAPSPassword, ad.ReadGMSAPassword, ad.DumpSMSAPassword, ad.SQLAdmin, ad.AddAllowedToAct, ad.WriteSPN, ad.AddKeyCredentialLink, ad.SyncLAPSPassword, ad.WriteAccountRestrictions, ad.WriteGPLink, ad.GoldenCert, ad.ADCSESC1, ad.ADCSESC3, ad.ADCSESC4, ad.ADCSESC6a, ad.ADCSESC6b, ad.ADCSESC9a, ad.ADCSESC9b, ad.ADCSESC10a, ad.ADCSESC10b, ad.ADCSESC13, ad.SyncedToEntraUser, ad.CoerceAndRelayNTLMToSMB, ad.CoerceAndRelayNTLMToADCS, ad.Contains, ad.DCFor, azure.AvereContributor, azure.Contributor, azure.GetCertificates, azure.GetKeys, azure.GetSecrets, azure.HasRole, azure.MemberOf, azure.Owner, azure.RunsAs, azure.VMContributor, azure.AutomationContributor, azure.KeyVaultContributor, azure.VMAdminLogin, azure.AddMembers, azure.AddSecret, azure.ExecuteCommand, azure.GlobalAdmin, azure.PrivilegedAuthAdmin, azure.Grant, azure.GrantSelf, azure.PrivilegedRoleAdmin, azure.ResetPassword, azure.UserAccessAdministrator, azure.Owns, azure.CloudAppAdmin, azure.AppAdmin, azure.AddOwner, azure.ManagedIdentity, azure.AKSContributor, azure.NodeResourceGroup, azure.WebsiteContributor, azure.LogicAppContributor, azure.AZMGAddMember, azure.AZMGAddOwner, azure.AZMGAddSecret, azure.AZMGGrantAppRoles, azure.AZMGGrantRole, azure.SyncedToADUser} } type Property string diff --git a/packages/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts index 280aff109..e83b75c83 100644 --- a/packages/javascript/bh-shared-ui/src/graphSchema.ts +++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts @@ -140,6 +140,7 @@ export enum ActiveDirectoryRelationshipKind { ADCSESC13 = 'ADCSESC13', SyncedToEntraUser = 'SyncedToEntraUser', CoerceAndRelayNTLMToSMB = 'CoerceAndRelayNTLMToSMB', + CoerceAndRelayNTLMToADCS = 'CoerceAndRelayNTLMToADCS', } export function ActiveDirectoryRelationshipKindToDisplay(value: ActiveDirectoryRelationshipKind): string | undefined { switch (value) { @@ -281,6 +282,8 @@ export function ActiveDirectoryRelationshipKindToDisplay(value: ActiveDirectoryR return 'SyncedToEntraUser'; case ActiveDirectoryRelationshipKind.CoerceAndRelayNTLMToSMB: return 'CoerceAndRelayNTLMToSMB'; + case ActiveDirectoryRelationshipKind.CoerceAndRelayNTLMToADCS: + return 'CoerceAndRelayNTLMToADCS'; default: return undefined; } @@ -299,6 +302,7 @@ export const EdgeCompositionRelationships = [ 'ADCSESC10b', 'ADCSESC13', 'CoerceAndRelayNTLMToSMB', + 'CoerceAndRelayNTLMToADCS', ]; export enum ActiveDirectoryKindProperties { AdminCount = 'admincount', @@ -403,7 +407,11 @@ export enum ActiveDirectoryKindProperties { LockoutDuration = 'lockoutduration', LockoutObservationWindow = 'lockoutobservationwindow', SMBSigning = 'smbsigning', + WebClientRunning = 'webclientrunning', RestrictOutboundNTLM = 'restrictoutboundntlm', + ADCSWebEnrollmentHTTP = 'adcswebenrollmenthttp', + ADCSWebEnrollmentHTTPS = 'adcswebenrollmenthttps', + ADCSWebEnrollmentHTTPSEPA = 'adcswebenrollmenthttpsepa', } export function ActiveDirectoryKindPropertiesToDisplay(value: ActiveDirectoryKindProperties): string | undefined { switch (value) { @@ -611,8 +619,16 @@ export function ActiveDirectoryKindPropertiesToDisplay(value: ActiveDirectoryKin return 'Lockout Observation Window'; case ActiveDirectoryKindProperties.SMBSigning: return 'SMB Signing'; + case ActiveDirectoryKindProperties.WebClientRunning: + return 'WebClient Running'; case ActiveDirectoryKindProperties.RestrictOutboundNTLM: return 'Restrict Outbound NTLM'; + case ActiveDirectoryKindProperties.ADCSWebEnrollmentHTTP: + return 'ADCS Web Enrollment HTTP'; + case ActiveDirectoryKindProperties.ADCSWebEnrollmentHTTPS: + return 'ADCS Web Enrollment HTTPS'; + case ActiveDirectoryKindProperties.ADCSWebEnrollmentHTTPSEPA: + return 'ADCS Web Enrollment HTTPS EPA'; default: return undefined; } @@ -663,6 +679,7 @@ export function ActiveDirectoryPathfindingEdges(): ActiveDirectoryRelationshipKi ActiveDirectoryRelationshipKind.ADCSESC13, ActiveDirectoryRelationshipKind.SyncedToEntraUser, ActiveDirectoryRelationshipKind.CoerceAndRelayNTLMToSMB, + ActiveDirectoryRelationshipKind.CoerceAndRelayNTLMToADCS, ActiveDirectoryRelationshipKind.Contains, ActiveDirectoryRelationshipKind.DCFor, ActiveDirectoryRelationshipKind.TrustedBy,