Skip to content

Commit

Permalink
Make ESC1 Edge Composition significantly more performant (#273)
Browse files Browse the repository at this point in the history
* fix: rewrite esc1 edge composition to be much more performant

* author esc 1 to avoid unnecessary expansions

* fix: attach path segment

* fix: attach path segment

* fix: attach path segment

* feat: make Rohan's life easier

* fix: ensure cert template properties on expansion for ESC1 composition

* chore: delete unused func

* chore: delete integration test

* chore: delete misleading cypher

* fix: remove unnecessary rels from p3

---------

Co-authored-by: John Hopper <[email protected]>
  • Loading branch information
rvazarkar and zinic authored Dec 15, 2023
1 parent d736b77 commit ec60c6e
Show file tree
Hide file tree
Showing 4 changed files with 343 additions and 86 deletions.
205 changes: 132 additions & 73 deletions packages/go/analysis/ad/ad.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ package ad
import (
"context"
"fmt"
"github.com/specterops/bloodhound/analysis"
"github.com/specterops/bloodhound/dawgs/traversal"
"sort"
"strings"
"sync"
"time"

"github.com/specterops/bloodhound/analysis/impact"
Expand Down Expand Up @@ -474,7 +477,7 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph.
pathSet = results
}
} else if edge.Kind == ad.ADCSESC1 {
if results, err := getADCSESC1EdgeComposition(tx, edge); err != nil {
if results, err := GetADCSESC1EdgeComposition(ctx, db, edge); err != nil {
return err
} else {
pathSet = results
Expand All @@ -484,83 +487,139 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph.
})
}

func getADCSESC1EdgeComposition(tx graph.Transaction, edge *graph.Relationship) (graph.PathSet, error) {
finalPaths := graph.NewPathSet()
//Grab the start node as well as the target domain node
if startNode, targetDomainNode, err := ops.FetchRelationshipNodes(tx, edge); err != nil {
return finalPaths, err
} else {
//Find cert templates that we have control over using enroll/acls
if pathsToTemplates, err := ops.TraversePaths(tx, ops.TraversalPlan{
Root: startNode,
Direction: graph.DirectionOutbound,
BranchQuery: func() graph.Criteria {
return query.KindIn(query.Relationship(), ad.Enroll, ad.GenericAll, ad.AllExtendedRights, ad.MemberOf)
},
DescentFilter: OutboundControlDescentFilter,
PathFilter: func(ctx *ops.TraversalContext, segment *graph.PathSegment) bool {
node := segment.Node
if !node.Kinds.ContainsOneOf(ad.CertTemplate) {
return false
} else if props, err := getValidatePublishedCertTemplateForEsc1PropertyValues(node); err != nil {
log.Errorf("Error getting props for certtemplate %d: %w", node.ID, err)
} else if !validatePublishedCertTemplateForEsc1(props) {
return false
}
func ADCSESC1Path1Pattern(domainID graph.ID) traversal.PatternContinuation {
return traversal.NewPattern().
Outbound(query.And(
query.Kind(query.Relationship(), ad.MemberOf),
query.Kind(query.End(), ad.Group),
)).
Outbound(query.And(
query.KindIn(query.Relationship(), ad.GenericAll, ad.Enroll, ad.AllExtendedRights),
query.Kind(query.End(), ad.CertTemplate),
query.Or(
query.And(
query.Equals(query.EndProperty(ad.RequiresManagerApproval.String()), false),
query.GreaterThan(query.EndProperty(ad.SchemaVersion.String()), 1),
query.Equals(query.EndProperty(ad.AuthorizedSignatures.String()), 0),
query.Equals(query.EndProperty(ad.AuthenticationEnabled.String()), true),
query.Equals(query.EndProperty(ad.EnrolleeSuppliesSubject.String()), true),
),
query.And(
query.Equals(query.EndProperty(ad.RequiresManagerApproval.String()), false),
query.Equals(query.EndProperty(ad.SchemaVersion.String()), 1),
query.Equals(query.EndProperty(ad.AuthenticationEnabled.String()), true),
query.Equals(query.EndProperty(ad.EnrolleeSuppliesSubject.String()), true),
),
),
)).
Outbound(query.And(
query.KindIn(query.Relationship(), ad.PublishedTo),
query.Kind(query.End(), ad.EnterpriseCA),
)).
Outbound(query.And(
query.KindIn(query.Relationship(), ad.IssuedSignedBy, ad.EnterpriseCAFor),
query.Kind(query.End(), ad.RootCA),
)).
Outbound(query.And(
query.KindIn(query.Relationship(), ad.RootCAFor),
query.Equals(query.EndID(), domainID),
))
}

return true
},
}); err != nil {
log.Errorf("Error getting paths from start node %d to templates: %w", startNode.ID, err)
return finalPaths, err
func ADCSESC1Path2Pattern(domainID graph.ID, enterpriseCAs cardinality.Duplex[uint32]) traversal.PatternContinuation {
return traversal.NewPattern().
Outbound(query.And(
query.Kind(query.Relationship(), ad.MemberOf),
query.Kind(query.End(), ad.Group),
)).
Outbound(query.And(
query.KindIn(query.Relationship(), ad.Enroll),
query.InIDs(query.EndID(), cardinality.DuplexToGraphIDs(enterpriseCAs)...),
)).
Outbound(query.And(
query.KindIn(query.Relationship(), ad.TrustedForNTAuth),
query.Kind(query.End(), ad.NTAuthStore),
)).
Outbound(query.And(
query.KindIn(query.Relationship(), ad.NTAuthStoreFor),
query.Equals(query.EndID(), domainID),
))
}

func GetADCSESC1EdgeComposition(ctx context.Context, db graph.Database, edge *graph.Relationship) (graph.PathSet, error) {
var (
startNode *graph.Node

traversalInst = traversal.New(db, analysis.MaximumDatabaseParallelWorkers)
paths = graph.PathSet{}
candidateSegments = map[graph.ID][]*graph.PathSegment{}
path1EnterpriseCAs = cardinality.NewBitmap32()
path2EnterpriseCAs = cardinality.NewBitmap32()
lock = &sync.Mutex{}
)

if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error {
if node, err := ops.FetchNode(tx, edge.StartID); err != nil {
return err
} else {
for _, path := range pathsToTemplates {
certTemplate := path.Terminal()
if ecaPaths, err := ops.FetchPathSet(tx, tx.Relationships().Filter(query.And(
query.Equals(query.StartID(), certTemplate.ID),
query.KindIn(query.End(), ad.EnterpriseCA),
query.KindIn(query.Relationship(), ad.PublishedTo),
))); err != nil {
log.Errorf("error getting eca published to for cert template %d : %w", certTemplate.ID, err)
} else {
for _, ecaPath := range ecaPaths {
eca := ecaPath.Terminal()
if domainPaths, err := FetchEnterpriseCAsCertChainPathToDomain(tx, eca, targetDomainNode); err != nil {
log.Errorf("error getting eca %d path to domain %d: %w", eca.ID, targetDomainNode.ID, err)
} else if domainPaths.Len() == 0 {
continue
} else if trustedForAuthPaths, err := FetchEnterpriseCAsTrustedForAuthPathToDomain(tx, eca, targetDomainNode); err != nil {
log.Errorf("error getting eca %d path to domain %d via trusted for auth: %w", eca.ID, targetDomainNode.ID, err)
} else if trustedForAuthPaths.Len() == 0 {
continue
} else if userPathsToCa, err := ops.TraversePaths(tx, ops.TraversalPlan{
Root: startNode,
Direction: graph.DirectionOutbound,
BranchQuery: func() graph.Criteria {
return query.KindIn(query.Relationship(), ad.Enroll, ad.MemberOf)
},
DescentFilter: OutboundControlDescentFilter,
PathFilter: func(ctx *ops.TraversalContext, segment *graph.PathSegment) bool {
return segment.Node.ID == eca.ID
},
}); err != nil {
log.Errorf("Error getting paths from start node %d to enterprise ca: %w", startNode.ID, err)
} else if userPathsToCa.Len() == 0 {
continue
} else {
finalPaths.AddPath(path)
finalPaths.AddPath(ecaPath)
finalPaths.AddPathSet(domainPaths)
finalPaths.AddPathSet(trustedForAuthPaths)
finalPaths.AddPathSet(userPathsToCa)
}
}
}
}
startNode = node
return nil
}
}); err != nil {
return nil, err
}

return finalPaths, nil
if err := traversalInst.BreadthFirst(ctx, traversal.Plan{
Root: startNode,
Driver: ADCSESC1Path1Pattern(edge.EndID).Do(func(terminal *graph.PathSegment) error {
// Find the CA and track it before stuffing this path into the candidates
enterpriseCANode := terminal.Search(func(nextSegment *graph.PathSegment) bool {
return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA)
})

lock.Lock()
candidateSegments[enterpriseCANode.ID] = append(candidateSegments[enterpriseCANode.ID], terminal)
path1EnterpriseCAs.Add(enterpriseCANode.ID.Uint32())
lock.Unlock()

return nil
}),
}); err != nil {
return nil, err
}

if err := traversalInst.BreadthFirst(ctx, traversal.Plan{
Root: startNode,
Driver: ADCSESC1Path2Pattern(edge.EndID, path1EnterpriseCAs).Do(func(terminal *graph.PathSegment) error {
// Find the CA and track it before stuffing this path into the candidates
enterpriseCANode := terminal.Search(func(nextSegment *graph.PathSegment) bool {
return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA)
})

lock.Lock()
candidateSegments[enterpriseCANode.ID] = append(candidateSegments[enterpriseCANode.ID], terminal)
path2EnterpriseCAs.Add(enterpriseCANode.ID.Uint32())
lock.Unlock()

return nil
}),
}); err != nil {
return nil, err
}

// Intersect the CAs and take only those seen in both paths
path1EnterpriseCAs.And(path2EnterpriseCAs)

// Render paths from the segments
return paths, path1EnterpriseCAs.Each(func(value uint32) (bool, error) {
for _, segment := range candidateSegments[graph.ID(value)] {
log.Infof("Found ESC1 Path: %s", graph.FormatPathSegment(segment))

paths.AddPath(segment.Path())
}

return true, nil
})
}

func getGoldenCertEdgeComposition(tx graph.Transaction, edge *graph.Relationship) (graph.PathSet, error) {
Expand Down
11 changes: 6 additions & 5 deletions packages/go/dawgs/graph/kind.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// Copyright 2023 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

package graph
Expand Down Expand Up @@ -91,7 +91,7 @@ func (s Kinds) Add(kinds ...Kind) Kinds {
ref = append(ref, kind)
}
}

return ref
}

Expand Down Expand Up @@ -127,6 +127,7 @@ func (s Kinds) ContainsOneOf(others ...Kind) bool {

var (
kindCache = &sync.Map{}
EmptyKind = StringKind("")
)

func StringKind(str string) Kind {
Expand Down
29 changes: 27 additions & 2 deletions packages/go/dawgs/graph/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ type PathSegment struct {
Trunk *PathSegment
Edge *Relationship
Branches []*PathSegment
Tag any
size size.Size
}

Expand Down Expand Up @@ -340,6 +341,20 @@ func (s *PathSegment) Path() Path {
return path
}

func (s *PathSegment) Slice() []*PathSegment {
var (
containerIdx = s.Depth()
container = make([]*PathSegment, containerIdx)
)

for cursor := s; cursor != nil; cursor = cursor.Trunk {
containerIdx--
container[containerIdx] = cursor
}

return container
}

func (s *PathSegment) WalkReverse(delegate func(nextSegment *PathSegment) bool) {
for cursor := s; cursor != nil; cursor = cursor.Trunk {
if !delegate(cursor) {
Expand All @@ -348,6 +363,16 @@ func (s *PathSegment) WalkReverse(delegate func(nextSegment *PathSegment) bool)
}
}

func (s *PathSegment) Search(delegate func(nextSegment *PathSegment) bool) *Node {
for cursor := s; cursor != nil; cursor = cursor.Trunk {
if delegate(cursor) {
return cursor.Node
}
}

return nil
}

func (s *PathSegment) Detach() {
var (
sizeDetached = s.SizeOf()
Expand Down Expand Up @@ -410,9 +435,9 @@ func FormatPathSegment(segment *PathSegment) string {
formatted.WriteString(")")

if nextSegment.Trunk != nil {
formatted.WriteString("-[")
formatted.WriteString("<-[")
formatted.WriteString(nextSegment.Edge.Kind.String())
formatted.WriteString("]->")
formatted.WriteString("]-")
}

return true
Expand Down
Loading

0 comments on commit ec60c6e

Please sign in to comment.