Skip to content

Commit

Permalink
Merge branch 'main' into 20240209-README-Update
Browse files Browse the repository at this point in the history
  • Loading branch information
superlinkx authored Feb 9, 2024
2 parents d3e0c55 + bb4ec2c commit cb69b5c
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 0 deletions.
27 changes: 27 additions & 0 deletions cmd/api/src/analysis/ad/adcs_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
})
}

Expand Down
1 change: 1 addition & 0 deletions packages/cue/bh/ad/ad.cue
Original file line number Diff line number Diff line change
Expand Up @@ -1181,5 +1181,6 @@ EdgeCompositionRelationships: [
ADCSESC6a,
ADCSESC6b,
ADCSESC9a,
ADCSESC9b,
ADCSESC10a,
]
218 changes: 218 additions & 0 deletions packages/go/analysis/ad/ad.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import WindowsAbuse from './WindowsAbuse';
import LinuxAbuse from './LinuxAbuse';
import Opsec from './Opsec';
import References from './References';
import Composition from './Composition';

const ADCSESC9b = {
general: General,
windowsAbuse: WindowsAbuse,
linuxAbuse: LinuxAbuse,
opsec: Opsec,
references: References,
composition: Composition,
};

export default ADCSESC9b;
Original file line number Diff line number Diff line change
@@ -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<EdgeInfoProps> = ({ 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 (
<>
<Typography variant='body2'>
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:
</Typography>
<Box py={1}>
{isLoading ? (
<Skeleton variant='rounded' />
) : isError ? (
<Alert severity='error'>Couldn't load edge composition</Alert>
) : (
<VirtualizedNodeList nodes={nodesArray} />
)}
</Box>
</>
);
};

export default Composition;
1 change: 1 addition & 0 deletions packages/javascript/bh-shared-ui/src/graphSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export const EdgeCompositionRelationships = [
'ADCSESC6a',
'ADCSESC6b',
'ADCSESC9a',
'ADCSESC9b',
'ADCSESC10a',
];
export enum ActiveDirectoryKindProperties {
Expand Down

0 comments on commit cb69b5c

Please sign in to comment.