From bb4ec2c01cd7ec594d1f6b6fd06493aa7ad176c1 Mon Sep 17 00:00:00 2001 From: Wesley Maffly-Kipp Date: Fri, 9 Feb 2024 10:14:54 -0800 Subject: [PATCH] ESC9b edge composition (#389) * added edge composition * updated error logging --------- Co-authored-by: Rohan Vazarkar --- .../src/analysis/ad/adcs_integration_test.go | 27 +++ packages/cue/bh/ad/ad.cue | 1 + packages/go/analysis/ad/ad.go | 218 ++++++++++++++++++ .../HelpTexts/ADCSESC9b/ADCSESC9b.tsx | 2 + .../HelpTexts/ADCSESC9b/Composition.tsx | 54 +++++ .../bh-shared-ui/src/graphSchema.ts | 1 + 6 files changed, 303 insertions(+) create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Composition.tsx diff --git a/cmd/api/src/analysis/ad/adcs_integration_test.go b/cmd/api/src/analysis/ad/adcs_integration_test.go index 24b44a6086..2160e050ac 100644 --- a/cmd/api/src/analysis/ad/adcs_integration_test.go +++ b/cmd/api/src/analysis/ad/adcs_integration_test.go @@ -1135,6 +1135,33 @@ func TestADCSESC9b(t *testing.T) { } return nil }) + + 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.ADCSESC9b) + })); err != nil { + t.Fatalf("error fetching esc9b edges in integration test; %v", err) + } else { + assert.Equal(t, 1, len(results)) + edge := results[0] + + if edgeComp, err := ad2.GetEdgeCompositionPath(context.Background(), db, edge); err != nil { + t.Fatalf("error getting edge composition for esc9: %v", err) + } else { + nodes := edgeComp.AllNodes().Slice() + assert.Contains(t, nodes, harness.ESC9bHarnessECA.Group1) + assert.Contains(t, nodes, harness.ESC9bHarnessECA.Domain1) + assert.Contains(t, nodes, harness.ESC9bHarnessECA.Computer1) + assert.Contains(t, nodes, harness.ESC9bHarnessECA.CertTemplate1) + assert.Contains(t, nodes, harness.ESC9bHarnessECA.EnterpriseCA1) + assert.Contains(t, nodes, harness.ESC9bHarnessECA.DC1) + assert.Contains(t, nodes, harness.ESC9bHarnessECA.NTAuthStore1) + assert.Contains(t, nodes, harness.ESC9bHarnessECA.RootCA1) + } + } + + return nil + }) }) } diff --git a/packages/cue/bh/ad/ad.cue b/packages/cue/bh/ad/ad.cue index 357a4412ad..5c1bdcab0c 100644 --- a/packages/cue/bh/ad/ad.cue +++ b/packages/cue/bh/ad/ad.cue @@ -1181,5 +1181,6 @@ EdgeCompositionRelationships: [ ADCSESC6a, ADCSESC6b, ADCSESC9a, + ADCSESC9b, ADCSESC10a, ] diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go index 9d292c09d5..560e7d9f58 100644 --- a/packages/go/analysis/ad/ad.go +++ b/packages/go/analysis/ad/ad.go @@ -568,6 +568,12 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph. } else { pathSet = results } + } else if edge.Kind == ad.ADCSESC9b { + if results, err := GetADCSESC9bEdgeComposition(ctx, db, edge); err != nil { + return err + } else { + pathSet = results + } } else if edge.Kind == ad.ADCSESC10a { if results, err := GetADCSESC10aEdgeComposition(ctx, db, edge); err != nil { return err @@ -1520,6 +1526,218 @@ func certTemplateValidForUserVictim(certTemplate *graph.Node) bool { } } +func adcsESC9bPath1Pattern(domainID graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + OutboundWithDepth( + 1, 1, + query.And( + query.KindIn(query.Relationship(), ad.GenericWrite, ad.GenericAll, ad.Owns, ad.WriteOwner, ad.WriteDACL), + query.KindIn(query.End(), ad.Computer), + ), + ). + OutboundWithDepth( + 0, 0, + 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.Equals(query.EndProperty(ad.RequiresManagerApproval.String()), false), + query.Equals(query.EndProperty(ad.AuthenticationEnabled.String()), true), + query.Equals(query.EndProperty(ad.NoSecurityExtension.String()), true), + query.Equals(query.EndProperty(ad.EnrolleeSuppliesSubject.String()), false), + query.Equals(query.EndProperty(ad.SubjectAltRequireDNS.String()), true), + query.Or( + query.Equals(query.EndProperty(ad.SchemaVersion.String()), 1), + query.And( + query.GreaterThan(query.EndProperty(ad.SchemaVersion.String()), 1), + query.Equals(query.EndProperty(ad.AuthorizedSignatures.String()), 0), + ), + ), + ), + ). + Outbound(query.And( + query.KindIn(query.Relationship(), ad.PublishedTo, ad.IssuedSignedBy), + 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), + )) +} + +func adcsESC9bPath2Pattern(caNodes []graph.ID, domainId graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + OutboundWithDepth(0, 0, query.And( + query.Kind(query.Relationship(), ad.MemberOf), + query.Kind(query.End(), ad.Group), + )). + Outbound(query.And( + query.Kind(query.Relationship(), ad.Enroll), + query.InIDs(query.End(), caNodes...), + )). + 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 adcsESC9bPath3Pattern(caIDs []graph.ID) traversal.PatternContinuation { + return traversal.NewPattern(). + Inbound( + query.KindIn(query.Relationship(), ad.DCFor, ad.TrustedBy), + ). + Inbound(query.And( + query.Kind(query.Relationship(), ad.CanAbuseWeakCertBinding), + query.InIDs(query.StartID(), caIDs...), + )) +} + +func GetADCSESC9bEdgeComposition(ctx context.Context, db graph.Database, edge *graph.Relationship) (graph.PathSet, error) { + /* + MATCH (n {objectid:'S-1-5-21-3933516454-2894985453-2515407000-500'})-[:ADCSESC9b]->(d:Domain {objectid:'S-1-5-21-3933516454-2894985453-2515407000'}) + OPTIONAL MATCH p1 = (n)-[:GenericAll|GenericWrite|Owns|WriteOwner|WriteDacl]->(m:Computer)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor|RootCAFor*1..]->(d) + WHERE ct.requiresmanagerapproval = false + AND ct.authenticationenabled = true + AND ct.nosecurityextension = true + AND ct.enrolleesuppliessubject = False + AND ct.subjectaltrequiredns = true + AND ( + (ct.schemaversion > 1 AND ct.authorizedsignatures = 0) + OR ct.schemaversion = 1 + ) + OPTIONAL MATCH p2 = (m)-[:MemberOf*0..]->()-[:Enroll]->(ca)-[:TrustedForNTAuth]->(nt)-[:NTAuthStoreFor]->(d) + OPTIONAL MATCH p3 = (ca)-[:CanAbuseWeakCertBinding|DCFor|TrustedBy*1..]->(d) + RETURN p1,p2,p3 + */ + + var ( + startNode *graph.Node + endNode *graph.Node + + traversalInst = traversal.New(db, analysis.MaximumDatabaseParallelWorkers) + paths = graph.PathSet{} + path1CandidateSegments = map[graph.ID][]*graph.PathSegment{} + victimCANodes = map[graph.ID][]graph.ID{} + path2CandidateSegments = map[graph.ID][]*graph.PathSegment{} + path3CandidateSegments = map[graph.ID][]*graph.PathSegment{} + p2canodes = make([]graph.ID, 0) + nodeMap = map[graph.ID]*graph.Node{} + lock = &sync.Mutex{} + ) + + if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { + var err error + if startNode, err = ops.FetchNode(tx, edge.StartID); err != nil { + return err + } else if endNode, err = ops.FetchNode(tx, edge.EndID); err != nil { + return err + } else { + return nil + } + }); err != nil { + return nil, err + } + + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: startNode, + Driver: adcsESC9bPath1Pattern(edge.EndID).Do(func(terminal *graph.PathSegment) error { + victimNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Depth() == 1 + }) + + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + path1CandidateSegments[victimNode.ID] = append(path1CandidateSegments[victimNode.ID], terminal) + nodeMap[victimNode.ID] = victimNode + victimCANodes[victimNode.ID] = append(victimCANodes[victimNode.ID], caNode.ID) + lock.Unlock() + + return nil + }), + }); err != nil { + return nil, err + } + + for victim, p1CANodes := range victimCANodes { + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: nodeMap[victim], + Driver: adcsESC9bPath2Pattern(p1CANodes, edge.EndID).Do(func(terminal *graph.PathSegment) error { + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + path2CandidateSegments[caNode.ID] = append(path2CandidateSegments[caNode.ID], terminal) + p2canodes = append(p2canodes, caNode.ID) + lock.Unlock() + + return nil + }), + }); err != nil { + return nil, err + } + } + + if len(p2canodes) > 0 { + if err := traversalInst.BreadthFirst(ctx, traversal.Plan{ + Root: endNode, + Driver: adcsESC9bPath3Pattern(p2canodes).Do(func(terminal *graph.PathSegment) error { + caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + lock.Lock() + path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal) + lock.Unlock() + return nil + }), + }); err != nil { + return nil, err + } + } + + for _, p1paths := range path1CandidateSegments { + for _, p1path := range p1paths { + caNode := p1path.Search(func(nextSegment *graph.PathSegment) bool { + return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA) + }) + + if p2segments, ok := path2CandidateSegments[caNode.ID]; !ok { + continue + } else if p3segments, ok := path3CandidateSegments[caNode.ID]; !ok { + continue + } else { + paths.AddPath(p1path.Path()) + for _, p2 := range p2segments { + paths.AddPath(p2.Path()) + } + + for _, p3 := range p3segments { + paths.AddPath(p3.Path()) + } + } + } + } + + return paths, nil +} + func adcsESC10aPath1Pattern(domainID graph.ID) traversal.PatternContinuation { return traversal.NewPattern(). OutboundWithDepth( diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx index 724cfe762e..d4f1f6d6ad 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx @@ -19,6 +19,7 @@ import WindowsAbuse from './WindowsAbuse'; import LinuxAbuse from './LinuxAbuse'; import Opsec from './Opsec'; import References from './References'; +import Composition from './Composition'; const ADCSESC9b = { general: General, @@ -26,6 +27,7 @@ const ADCSESC9b = { linuxAbuse: LinuxAbuse, opsec: Opsec, references: References, + composition: Composition, }; export default ADCSESC9b; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Composition.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Composition.tsx new file mode 100644 index 0000000000..4a24ff5eab --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Composition.tsx @@ -0,0 +1,54 @@ +// 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 + +import { FC } from 'react'; +import { Alert, Box, Skeleton, Typography } from '@mui/material'; +import { apiClient } from '../../../utils/api'; +import { EdgeInfoProps } from '..'; +import { useQuery } from 'react-query'; +import VirtualizedNodeList, { VirtualizedNodeListItem } from '../../VirtualizedNodeList'; + +const Composition: FC = ({ sourceDBId, targetDBId, edgeName }) => { + const { data, isLoading, isError } = useQuery(['edgeComposition', sourceDBId, targetDBId, edgeName], ({ signal }) => + apiClient.getEdgeComposition(sourceDBId!, targetDBId!, edgeName!).then((result) => result.data) + ); + + const nodesArray: VirtualizedNodeListItem[] = Object.values(data?.data.nodes || {}).map((node) => ({ + name: node.label, + objectId: node.objectId, + kind: node.kind, + })); + + return ( + <> + + The relationship represents the effective outcome of the configuration and relationships between several + different objects. All objects involved in the creation of this relationship are listed here: + + + {isLoading ? ( + + ) : isError ? ( + Couldn't load edge composition + ) : ( + + )} + + + ); +}; + +export default Composition; diff --git a/packages/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts index 60eb82cb4c..48f6a1c9fd 100644 --- a/packages/javascript/bh-shared-ui/src/graphSchema.ts +++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts @@ -281,6 +281,7 @@ export const EdgeCompositionRelationships = [ 'ADCSESC6a', 'ADCSESC6b', 'ADCSESC9a', + 'ADCSESC9b', 'ADCSESC10a', ]; export enum ActiveDirectoryKindProperties {