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

ESC10a Edge Composition #366

Merged
merged 3 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -1168,6 +1168,33 @@ func TestADCSESC10a(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.ADCSESC10a)
})); err != nil {
t.Fatalf("error fetching esc9a 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.ESC10aHarnessECA.Group1)
assert.Contains(t, nodes, harness.ESC10aHarnessECA.User1)
assert.Contains(t, nodes, harness.ESC10aHarnessECA.Domain1)
assert.Contains(t, nodes, harness.ESC10aHarnessECA.NTAuthStore1)
assert.Contains(t, nodes, harness.ESC10aHarnessECA.RootCA1)
assert.Contains(t, nodes, harness.ESC10aHarnessECA.DC1)
assert.Contains(t, nodes, harness.ESC10aHarnessECA.EnterpriseCA1)
assert.Contains(t, nodes, harness.ESC10aHarnessECA.CertTemplate1)
}
}

return nil
})
})

testContext.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) error {
Expand Down
37 changes: 19 additions & 18 deletions cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
//
// SPDX-License-Identifier: Apache-2.0

import { Box, Divider, Typography, useTheme } from '@mui/material';
import { EdgeInfoComponents, EdgeSections, SelectedEdge, apiClient } from 'bh-shared-ui';
import {Box, Divider, Typography, useTheme} from '@mui/material';
import {EdgeInfoComponents, EdgeSections, SelectedEdge, apiClient} from 'bh-shared-ui';
import isEmpty from 'lodash/isEmpty';
import { Dispatch, FC, Fragment } from 'react';
import { putGraphData, putGraphError, saveResponseForExport, setGraphLoading } from 'src/ducks/explore/actions';
import { addSnackbar } from 'src/ducks/global/actions';
import { useAppDispatch } from 'src/store';
import { transformToFlatGraphResponse } from 'src/utils';
import {Dispatch, FC, Fragment} from 'react';
import {putGraphData, putGraphError, saveResponseForExport, setGraphLoading} from 'src/ducks/explore/actions';
import {addSnackbar} from 'src/ducks/global/actions';
import {useAppDispatch} from 'src/store';
import {transformToFlatGraphResponse} from 'src/utils';
import EdgeInfoCollapsibleSection from 'src/views/Explore/EdgeInfo/EdgeInfoCollapsibleSection';
import EdgeObjectInformation from 'src/views/Explore/EdgeInfo/EdgeObjectInformation';

Expand Down Expand Up @@ -55,16 +55,16 @@ const getOnChange = (dispatch: Dispatch<any>, sourceNodeId: number, targetNodeId
};
};

const EdgeInfoContent: FC<{ selectedEdge: NonNullable<SelectedEdge> }> = ({ selectedEdge }) => {
const EdgeInfoContent: FC<{ selectedEdge: NonNullable<SelectedEdge> }> = ({selectedEdge}) => {
const theme = useTheme();
const dispatch = useAppDispatch();

const sections = EdgeInfoComponents[selectedEdge.name as keyof typeof EdgeInfoComponents];
const { sourceNode, targetNode } = selectedEdge;
const {sourceNode, targetNode} = selectedEdge;

return (
<Box>
<EdgeObjectInformation selectedEdge={selectedEdge} />
<EdgeObjectInformation selectedEdge={selectedEdge}/>
{sections ? (
<>
{Object.entries(sections).map((section, index) => {
Expand All @@ -75,24 +75,25 @@ const EdgeInfoContent: FC<{ selectedEdge: NonNullable<SelectedEdge> }> = ({ sele
selectedEdge.name === 'ADCSESC1' ||
selectedEdge.name === 'ADCSESC3' ||
selectedEdge.name === 'ADCSESC6a' ||
selectedEdge.name === 'ADCSESC9a') &&
selectedEdge.name === 'ADCSESC9a' ||
selectedEdge.name === 'ADCSESC10a') &&
rvazarkar marked this conversation as resolved.
Show resolved Hide resolved
section[0] === 'composition';

return (
<Fragment key={index}>
<Box padding={1}>
<Divider />
<Divider/>
</Box>
<EdgeInfoCollapsibleSection
section={section[0] as keyof typeof EdgeSections}
onChange={
sendOnChange
? getOnChange(
dispatch,
parseInt(`${sourceNode.id}`),
parseInt(`${targetNode.id}`),
selectedEdge.name
)
dispatch,
parseInt(`${sourceNode.id}`),
parseInt(`${targetNode.id}`),
selectedEdge.name
)
: undefined
}>
<Section
Expand All @@ -114,7 +115,7 @@ const EdgeInfoContent: FC<{ selectedEdge: NonNullable<SelectedEdge> }> = ({ sele
) : (
<>
<Box padding={1}>
<Divider />
<Divider/>
</Box>
<Box paddingLeft={theme.spacing(1)}>
<Typography variant='body1' fontSize={'0.75rem'}>
Expand Down
219 changes: 215 additions & 4 deletions packages/go/analysis/ad/ad.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,12 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph.
} else {
pathSet = results
}
} else if edge.Kind == ad.ADCSESC10a {
if results, err := GetADCSESC10aEdgeComposition(ctx, db, edge); err != nil {
return err
} else {
pathSet = results
}
}
return nil
})
Expand Down Expand Up @@ -1385,13 +1391,12 @@ func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *g
)

if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error {
if node, err := ops.FetchNode(tx, edge.StartID); err != nil {
var err error
if startNode, err = ops.FetchNode(tx, edge.StartID); err != nil {
return err
} else if eNode, err := ops.FetchNode(tx, edge.EndID); err != nil {
} else if endNode, err = ops.FetchNode(tx, edge.EndID); err != nil {
return err
} else {
startNode = node
endNode = eNode
return nil
}
}); err != nil {
Expand Down Expand Up @@ -1509,3 +1514,209 @@ func certTemplateValidForUserVictim(certTemplate *graph.Node) bool {
return true
}
}

func adcsESC10aPath1Pattern(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, ad.User),
),
).
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.EnrolleeSuppliesSubject.String()), false),
query.Or(
query.Equals(query.EndProperty(ad.SubjectAltRequireUPN.String()), true),
query.Equals(query.EndProperty(ad.SubjectAltRequireSPN.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 adcsESC10APath3Pattern(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.CanAbuseUPNCertMapping),
query.InIDs(query.StartID(), caIDs...),
))
}

func GetADCSESC10aEdgeComposition(ctx context.Context, db graph.Database, edge *graph.Relationship) (graph.PathSet, error) {
/*MATCH (n {objectid:'S-1-5-21-3933516454-2894985453-2515407000-500'})-[:ADCSESC10a]->(d:Domain {objectid:'S-1-5-21-3933516454-2894985453-2515407000'})
OPTIONAL MATCH p1 = (n)-[:GenericAll|GenericWrite|Owns|WriteOwner|WriteDacl]->(m)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor|RootCAFor*1..]->(d)
WHERE ct.requiresmanagerapproval = false
AND ct.authenticationenabled = true
AND ct.enrolleesuppliessubject = false
AND (ct.subjectaltrequireupn = true OR ct.subjectaltrequirespn = true)
AND (
(ct.schemaversion > 1 AND ct.authorizedsignatures = 0)
OR ct.schemaversion = 1
)
AND (
m:Computer
OR (m:User AND ct.subjectaltrequiredns = false AND ct.subjectaltrequiredomaindns = false)
)
OPTIONAL MATCH p2 = (m)-[:MemberOf*0..]->()-[:Enroll]->(ca)-[:TrustedForNTAuth]->(nt)-[:NTAuthStoreFor]->(d)
OPTIONAL MATCH p3 = (ca)-[:CanAbuseUPNCertMapping|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
}

//Fully manifest p1
if err := traversalInst.BreadthFirst(ctx, traversal.Plan{
Root: startNode,
Driver: adcsESC10aPath1Pattern(edge.EndID).Do(func(terminal *graph.PathSegment) error {
victimNode := terminal.Search(func(nextSegment *graph.PathSegment) bool {
return nextSegment.Depth() == 1
})

if victimNode.Kinds.ContainsOneOf(ad.User) {
certTemplate := terminal.Search(func(nextSegment *graph.PathSegment) bool {
return nextSegment.Node.Kinds.ContainsOneOf(ad.CertTemplate)
})

if !certTemplateValidForUserVictim(certTemplate) {
return nil
}
}

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
}

//We can re-use p2 from ESC9a, since they're the same
for victim, p1CANodes := range victimCANodes {
if err := traversalInst.BreadthFirst(ctx, traversal.Plan{
Root: nodeMap[victim],
Driver: adcsESC9APath2Pattern(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: adcsESC10APath3Pattern(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
}
3 changes: 3 additions & 0 deletions packages/go/analysis/ad/esc_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ func PostEnterpriseCAFor(operation analysis.StatTrackedOperation[analysis.Create

func processCertChainParent(node *graph.Node, tx graph.Transaction) ([]analysis.CreatePostRelationshipJob, error) {
if certChain, err := node.Properties.Get(ad.CertChain.String()).StringSlice(); err != nil {
if errors.Is(err, graph.ErrPropertyNotFound) {
return []analysis.CreatePostRelationshipJob{}, nil
}
return []analysis.CreatePostRelationshipJob{}, err
} else if len(certChain) > 1 {
parentCert := certChain[1]
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 ADCSESC10a = {
general: General,
windowsAbuse: WindowsAbuse,
linuxAbuse: LinuxAbuse,
opsec: Opsec,
references: References,
composition: Composition
};

export default ADCSESC10a;
Loading
Loading