Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CoerceAndRelayNTLMtoADCS Post Processing #1058

Merged
merged 32 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0a6a170
BED-5036 implement post processing for CoerceAndRelayNTLMToSMB
mvlipka Dec 13, 2024
e33b88d
BED-5036 initial integration test pass
mvlipka Dec 13, 2024
a165818
BED-5036 integration test written, working on race condition for test…
mvlipka Dec 13, 2024
1bf56be
BED-5036 fixed consistency in flapping test
mvlipka Dec 30, 2024
5199625
Merge branch 'main' into BED-5036/CoerceAndRelayNTLMToSMB-PostProcessing
mvlipka Dec 30, 2024
b778a2d
BED-5036 added check for restrict_outbound_ntlm on computers. Added t…
mvlipka Jan 3, 2025
fd31478
Merge branch 'main' into BED-5036/CoerceAndRelayNTLMToSMB-PostProcessing
mvlipka Jan 3, 2025
0591bd2
BED-5036 remove snake case on restrict_outbound_ntlm
mvlipka Jan 3, 2025
34cf583
BED-5036 remove snake case on restrict_outbound_ntlm
mvlipka Jan 3, 2025
6baeecb
BED-5036 address review feedback and cleaned up casing inconsistencies
mvlipka Jan 6, 2025
808ad2d
feat: initial NTLMtoADCS commit
rvazarkar Jan 7, 2025
0387987
Merge branch 'BED-5036/CoerceAndRelayNTLMToSMB-PostProcessing' into B…
rvazarkar Jan 7, 2025
22b9534
chore: regen schema, fix merge issues
rvazarkar Jan 7, 2025
17a4920
BED-5036 call operation.Done() when erroring in PostNTLM and reduced …
mvlipka Jan 7, 2025
2d83249
BED-5036 fix TestPostNTLM naming
mvlipka Jan 7, 2025
60f95c6
Merge branch 'main' into BED-5036/CoerceAndRelayNTLMToSMB-PostProcessing
mvlipka Jan 8, 2025
e67428b
Merge branch 'BED-5036/CoerceAndRelayNTLMToSMB-PostProcessing' into B…
rvazarkar Jan 8, 2025
f3b8b6c
Merge branch 'main' into BED-5029
rvazarkar Jan 8, 2025
69fe537
chore: regen schema, fix merge issues
rvazarkar Jan 8, 2025
65fc634
Merge branch 'main' into BED-5029
rvazarkar Jan 27, 2025
c6e4096
chore: regen schema
rvazarkar Jan 27, 2025
88ba55e
chore: missing return
rvazarkar Jan 27, 2025
bfbd507
Merge branch 'main' into BED-5029
rvazarkar Jan 29, 2025
a1600b2
feat: add tests, fix tests for CoerceToSMB
rvazarkar Jan 30, 2025
42df3f4
chore: add harnesses, update changed ones
rvazarkar Jan 30, 2025
3e1f837
chore: fix slog sca
rvazarkar Jan 30, 2025
0868fdc
chore: fix typo
rvazarkar Jan 30, 2025
30b0f10
chore: prepare for codereview
rvazarkar Jan 30, 2025
6e382ef
chore: use accessor
rvazarkar Jan 30, 2025
3ea75a3
chore: add some todos
rvazarkar Jan 30, 2025
e9c3052
chore: invert some logic for safety
rvazarkar Jan 30, 2025
ea4e1e3
chore: short circuit some logic
rvazarkar Jan 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
109 changes: 74 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,7 +21,7 @@ package ad_test

import (
"context"
"strings"
"github.com/specterops/bloodhound/dawgs/cardinality"
"testing"

"github.com/specterops/bloodhound/analysis"
Expand All @@ -33,11 +33,54 @@ 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) {
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) {
testContext := integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema())

testContext.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) error {
Expand All @@ -55,7 +98,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 +115,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
Loading