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 @@
-
-
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 @@
+
+
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 @@
+
+
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,