Skip to content

Commit

Permalink
BED-5036 initial integration test pass
Browse files Browse the repository at this point in the history
  • Loading branch information
mvlipka committed Dec 13, 2024
1 parent 0a6a170 commit e33b88d
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 19 deletions.
105 changes: 105 additions & 0 deletions cmd/api/src/analysis/ad/ntlm_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2024 Specter Ops, Inc.
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

//go:build integration
// +build integration

package ad_test

import (
"context"
"testing"

"github.com/specterops/bloodhound/analysis"
ad2 "github.com/specterops/bloodhound/analysis/ad"
"github.com/specterops/bloodhound/analysis/impact"
"github.com/specterops/bloodhound/dawgs/graph"
"github.com/specterops/bloodhound/dawgs/ops"
"github.com/specterops/bloodhound/dawgs/query"
"github.com/specterops/bloodhound/graphschema"
"github.com/specterops/bloodhound/graphschema/ad"
"github.com/specterops/bloodhound/src/test/integration"
"github.com/stretchr/testify/require"
)

func TestPostNtlm(t *testing.T) {
testContex := integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema())

testContex.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) error {
harness.NtlmCoerceAndRelayNtlmToSmb.Setup(testContex)
return nil
}, func(harness integration.HarnessDetails, db graph.Database) {
operation := analysis.NewPostRelationshipOperation(context.Background(), db, "NTLM Post Process Test - CoerceAndRelayNtlmToSmb")

groupExpansions, computers, domains, authenticatedUsers, err := fetchNtlmPrereqs(db)
require.NoError(t, err)

for _, domain := range domains {
innerDomain := domain

err = operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error {
for _, computer := range computers {
innerComputer := computer

if err = ad2.PostCoerceAndRelayNtlmToSmb(tx, outC, groupExpansions, innerComputer, innerDomain.ID.String(), authenticatedUsers); err != nil {
t.Logf("failed post processig for %s: %v", ad.CoerceAndRelayNTLMToSMB.String(), err)
}
}
return nil
})
require.NoError(t, err)
}

db.ReadTransaction(context.Background(), func(tx graph.Transaction) error {
if results, err := ops.FetchStartNodes(tx.Relationships().Filterf(func() graph.Criteria {
return query.Kind(query.Relationship(), ad.CoerceAndRelayNTLMToSMB)
})); err != nil {
t.Fatalf("error fetching ntlm to smb edges in integration test; %v", err)
} else {
require.Equal(t, 1, len(results))

require.True(t, results.Contains(harness.NtlmCoerceAndRelayNtlmToSmb.DomainAdminsUser))
require.True(t, results.Contains(harness.NtlmCoerceAndRelayNtlmToSmb.AuthenticatedUsers))
require.True(t, results.Contains(harness.NtlmCoerceAndRelayNtlmToSmb.ServerAdmins))

}
return nil
})

err = operation.Done()
require.NoError(t, err)
})
}

func fetchNtlmPrereqs(db graph.Database) (expansions impact.PathAggregator, computers []*graph.Node, domains []*graph.Node, authenticatedUsers map[string]graph.ID, err error) {
cache := make(map[string]graph.ID)
if expansions, err = ad2.ExpandAllRDPLocalGroups(context.Background(), db); err != nil {
return nil, nil, nil, cache, err
} else if computers, err = ad2.FetchNodesByKind(context.Background(), db, ad.Computer); err != nil {
return nil, nil, nil, cache, err
} else if err = db.ReadTransaction(context.Background(), func(tx graph.Transaction) error {
if cache, err = ad2.FetchAuthUsersMappedToDomains(tx); err != nil {
return err
}
return nil
}); err != nil {
return nil, nil, nil, cache, err
} else if domains, err = ad2.FetchNodesByKind(context.Background(), db, ad.Domain); err != nil {
return nil, nil, nil, cache, err
} else {
return expansions, computers, domains, cache, nil
}
}
22 changes: 22 additions & 0 deletions cmd/api/src/test/integration/harnesses.go
Original file line number Diff line number Diff line change
Expand Up @@ -8402,6 +8402,27 @@ func (s *ESC10bHarnessDC2) Setup(graphTestContext *GraphTestContext) {
graphTestContext.UpdateNode(s.DC1)
}

type NtlmCoerceAndRelayNtlmToSmb struct {
AuthenticatedUsers *graph.Node
DomainAdminsUser *graph.Node
ServerAdmins *graph.Node
computer3 *graph.Node
computer8 *graph.Node
}

func (s *NtlmCoerceAndRelayNtlmToSmb) Setup(graphTestContext *GraphTestContext) {
domainSid := RandomDomainSID()
s.AuthenticatedUsers = graphTestContext.NewActiveDirectoryUser("Authenticated Users", domainSid)
s.DomainAdminsUser = graphTestContext.NewActiveDirectoryUser("Domain Admins User", domainSid)
s.ServerAdmins = graphTestContext.NewActiveDirectoryDomain("Server Admins", domainSid, false, true)
s.computer3 = graphTestContext.NewActiveDirectoryComputer("computer3", domainSid)
s.computer8 = graphTestContext.NewActiveDirectoryComputer("computer8", domainSid)
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)
}

type HarnessDetails struct {
RDP RDPHarness
RDPB RDPHarness2
Expand Down Expand Up @@ -8500,4 +8521,5 @@ type HarnessDetails struct {
DCSyncHarness DCSyncHarness
SyncLAPSPasswordHarness SyncLAPSPasswordHarness
HybridAttackPaths HybridAttackPaths
NtlmCoerceAndRelayNtlmToSmb NtlmCoerceAndRelayNtlmToSmb
}
148 changes: 148 additions & 0 deletions cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
{
"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": "outside",
"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": "n0",
"position": {
"x": 0,
"y": 0
},
"caption": "computer3",
"style": {},
"labels": [],
"properties": {}
},
{
"id": "n1",
"position": {
"x": 284.5,
"y": 0
},
"caption": "Server Admins",
"style": {},
"labels": [],
"properties": {}
},
{
"id": "n2",
"position": {
"x": 485.67187924757275,
"y": -201.17187924757275
},
"caption": "computer8",
"style": {},
"labels": [],
"properties": {
"smb_signing": "false"
}
},
{
"id": "n3",
"position": {
"x": 0,
"y": -201.17187924757275
},
"caption": "Authenticated Users",
"style": {},
"labels": [],
"properties": {}
},
{
"id": "n4",
"position": {
"x": 665.8359396237863,
"y": -381.3359396237863
},
"caption": "Domain Admins User",
"style": {},
"labels": [],
"properties": {}
}
],
"relationships": [
{
"id": "n0",
"type": "MemberOf",
"style": {},
"properties": {},
"fromId": "n0",
"toId": "n1"
},
{
"id": "n1",
"type": "AdminTo",
"style": {},
"properties": {},
"fromId": "n1",
"toId": "n2"
},
{
"id": "n2",
"type": "CoerceAndRelayNTLMToSMB",
"style": {},
"properties": {},
"fromId": "n3",
"toId": "n2"
},
{
"id": "n3",
"type": "HasSession",
"style": {},
"properties": {},
"fromId": "n2",
"toId": "n4"
}
]
}
18 changes: 18 additions & 0 deletions cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 26 additions & 19 deletions packages/go/analysis/ad/ntlm.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,8 @@ func PostNTLM(ctx context.Context, db graph.Database, groupExpansions impact.Pat
// 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 {

authenticatedUsersCache := make(map[string]graph.ID)

// Fetch all nodes where the node is a Group and is an Authenticated User
if err := tx.Nodes().Filter(
query.And(
query.Kind(query.Node(), ad.Group),
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 {
continue
} else {
authenticatedUsersCache[domain] = authenticatedUser.ID
}
}

return cursor.Error()
},
); err != nil {
if authenticatedUsersCache, err := FetchAuthUsersMappedToDomains(tx); err != nil {
return err
} else {
// Fetch all nodes where the type is Computer
Expand Down Expand Up @@ -89,7 +72,7 @@ func PostNTLM(ctx context.Context, db graph.Database, groupExpansions impact.Pat
}

// PostCoerceAndRelayNtlmToSmb creates edges that allow a computer with unrolled admin access to one or more computers where SMB signing is disabled.
// Comprised solely oof adminTo and memberOf edges
// Comprised solely of adminTo and memberOf edges
func PostCoerceAndRelayNtlmToSmb(tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob, expandedGroups impact.PathAggregator, computer *graph.Node, domain string, authenticaedUserNodes map[string]graph.ID) error {
if authenticatedUserID, ok := authenticaedUserNodes[domain]; !ok {
return nil
Expand Down Expand Up @@ -138,3 +121,27 @@ func PostCoerceAndRelayNtlmToSmb(tx graph.Transaction, outC chan<- analysis.Crea

return nil
}

// FetchAuthUsersMappedToDomains Fetch all nodes where the node is a Group and is an Authenticated User
func FetchAuthUsersMappedToDomains(tx graph.Transaction) (map[string]graph.ID, error) {
authenticatedUsers := make(map[string]graph.ID)

err := tx.Nodes().Filter(
query.And(
query.Kind(query.Node(), ad.Group),
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 {
continue
} else {
authenticatedUsers[domain] = authenticatedUser.ID
}
}

return cursor.Error()
},
)

return authenticatedUsers, err
}

0 comments on commit e33b88d

Please sign in to comment.