Skip to content

Commit

Permalink
CoerceAndRelayNTLMtoADCS Post Processing (#1058)
Browse files Browse the repository at this point in the history
* BED-5036 implement post processing for CoerceAndRelayNTLMToSMB

* BED-5036 initial integration test pass

* BED-5036 integration test written, working on race condition for test harness

* BED-5036 fixed consistency in flapping test

* BED-5036 added check for restrict_outbound_ntlm on computers. Added testing of end nodes in NTLM integration test

* BED-5036 remove snake case on restrict_outbound_ntlm

* BED-5036 remove snake case on restrict_outbound_ntlm

* BED-5036 address review feedback and cleaned up casing inconsistencies

* feat: initial NTLMtoADCS commit

* chore: regen schema, fix merge issues

* BED-5036 call operation.Done() when erroring in PostNTLM and reduced tab depth in if blocks

* BED-5036 fix TestPostNTLM naming

* chore: regen schema, fix merge issues

* chore: regen schema

* chore: missing return

* feat: add tests, fix tests for CoerceToSMB

* chore: add harnesses, update changed ones

* chore: fix slog sca

* chore: fix typo

* chore: prepare for codereview

* chore: use accessor

* chore: add some todos

* chore: invert some logic for safety

* chore: short circuit some logic

---------

Co-authored-by: Michael Lipka <[email protected]>
  • Loading branch information
rvazarkar and mvlipka authored Jan 30, 2025
1 parent 4a0d4f7 commit 78bd0da
Show file tree
Hide file tree
Showing 16 changed files with 731 additions and 171 deletions.
14 changes: 4 additions & 10 deletions cmd/api/src/analysis/ad/adcs_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
}

Expand Down
112 changes: 77 additions & 35 deletions cmd/api/src/analysis/ad/ntlm_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
83 changes: 64 additions & 19 deletions cmd/api/src/test/integration/harnesses.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -8635,4 +8679,5 @@ type HarnessDetails struct {
SyncLAPSPasswordHarness SyncLAPSPasswordHarness
HybridAttackPaths HybridAttackPaths
NTLMCoerceAndRelayNTLMToSMB NTLMCoerceAndRelayNTLMToSMB
NTLMCoerceAndRelayNTLMToADCS CoerceAndRelayNTLMtoADCS
}
Loading

0 comments on commit 78bd0da

Please sign in to comment.