0 {
+ if err := enterpriseCAs.Each(func(value uint32) (bool, error) {
+ for _, segment := range enterpriseCASegments[graph.ID(value)] {
+ paths.AddPath(segment.Path())
+ }
+ return true, nil
+
+ }); err != nil {
+ return paths, err
+ }
+ }
+
+ return paths, nil
+}
+
func GetADCSESC3EdgeComposition(ctx context.Context, db graph.Database, edge *graph.Relationship) (graph.PathSet, error) {
var (
startNode *graph.Node
@@ -807,7 +1098,7 @@ func getDelegatedEnrollmentAgentPath(ctx context.Context, startNode, certTemplat
func ADCSESC1Path1Pattern(domainID graph.ID) traversal.PatternContinuation {
return traversal.NewPattern().
- Outbound(query.And(
+ OutboundWithDepth(0, 0, query.And(
query.Kind(query.Relationship(), ad.MemberOf),
query.Kind(query.End(), ad.Group),
)).
@@ -846,7 +1137,7 @@ func ADCSESC1Path1Pattern(domainID graph.ID) traversal.PatternContinuation {
func ADCSESC1Path2Pattern(domainID graph.ID, enterpriseCAs cardinality.Duplex[uint32]) traversal.PatternContinuation {
return traversal.NewPattern().
- Outbound(query.And(
+ OutboundWithDepth(0, 0, query.And(
query.Kind(query.Relationship(), ad.MemberOf),
query.Kind(query.End(), ad.Group),
)).
@@ -931,8 +1222,6 @@ func GetADCSESC1EdgeComposition(ctx context.Context, db graph.Database, edge *gr
// 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())
}
@@ -975,3 +1264,248 @@ func getGoldenCertEdgeComposition(tx graph.Transaction, edge *graph.Relationship
return finalPaths, nil
}
}
+
+func adcsESC9aPath1Pattern(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.NoSecurityExtension.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 adcsESC9APath2Pattern(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 adcsESC9APath3Pattern(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 GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *graph.Relationship) (graph.PathSet, error) {
+ /*
+ MATCH (n {objectid:'S-1-5-21-3933516454-2894985453-2515407000-500'})-[:ADCSESC9a]->(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.nosecurityextension = 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)-[: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 {
+ if node, err := ops.FetchNode(tx, edge.StartID); err != nil {
+ return err
+ } else if eNode, err := ops.FetchNode(tx, edge.EndID); err != nil {
+ return err
+ } else {
+ startNode = node
+ endNode = eNode
+ return nil
+ }
+ }); err != nil {
+ return nil, err
+ }
+
+ //Fully manifest p1
+ if err := traversalInst.BreadthFirst(ctx, traversal.Plan{
+ Root: startNode,
+ Driver: adcsESC9aPath1Pattern(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
+ }
+
+ 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: adcsESC9APath3Pattern(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 certTemplateValidForUserVictim(certTemplate *graph.Node) bool {
+ if subjectAltRequireDNS, err := certTemplate.Properties.Get(ad.SubjectAltRequireDNS.String()).Bool(); err != nil {
+ return false
+ } else if subjectAltRequireDNS {
+ return false
+ } else if subjectAltRequireDomainDNS, err := certTemplate.Properties.Get(ad.SubjectAltRequireDomainDNS.String()).Bool(); err != nil {
+ return false
+ } else if subjectAltRequireDomainDNS {
+ return false
+ } else {
+ return true
+ }
+}
diff --git a/packages/go/analysis/ad/adcs.go b/packages/go/analysis/ad/adcs.go
index eb64a1537e..4dae7474c0 100644
--- a/packages/go/analysis/ad/adcs.go
+++ b/packages/go/analysis/ad/adcs.go
@@ -141,7 +141,13 @@ func PostADCSESC3(ctx context.Context, tx graph.Transaction, outC chan<- analysi
cache.EnterpriseCAEnrollers[eca1.ID],
cache.EnterpriseCAEnrollers[eca2.ID],
delegatedAgents.Slice())
- results.Or(tempResults)
+
+ // Add principals to result set unless it's a user and DNS is required
+ if filteredResults, err := filterUserDNSResults(tx, tempResults, certTemplateOne); err != nil {
+ log.Errorf("Error filtering user dns results: %v", err)
+ } else {
+ results.Or(filteredResults)
+ }
}
}
} else {
@@ -151,7 +157,12 @@ func PostADCSESC3(ctx context.Context, tx graph.Transaction, outC chan<- analysi
cache.CertTemplateControllers[certTemplateTwo.ID],
cache.EnterpriseCAEnrollers[eca1.ID],
cache.EnterpriseCAEnrollers[eca2.ID])
- results.Or(tempResults)
+
+ if filteredResults, err := filterUserDNSResults(tx, tempResults, certTemplateOne); err != nil {
+ log.Errorf("Error filtering user dns results: %v", err)
+ } else {
+ results.Or(filteredResults)
+ }
}
}
}
@@ -174,6 +185,32 @@ func PostADCSESC3(ctx context.Context, tx graph.Transaction, outC chan<- analysi
return nil
}
+func filterUserDNSResults(tx graph.Transaction, tempResults cardinality.Duplex[uint32], certTemplate *graph.Node) (cardinality.Duplex[uint32], error) {
+ if userNodes, err := ops.FetchNodeSet(tx.Nodes().Filterf(func() graph.Criteria {
+ return query.And(
+ query.KindIn(query.Node(), ad.User),
+ query.InIDs(query.NodeID(), cardinality.DuplexToGraphIDs(tempResults)...),
+ )
+ })); err != nil {
+ if !graph.IsErrNotFound(err) {
+ return nil, err
+ }
+ } else if len(userNodes) > 0 {
+ if subjRequireDns, err := certTemplate.Properties.Get(ad.SubjectAltRequireDNS.String()).Bool(); err != nil {
+ log.Debugf("Failed to retrieve subjectAltRequireDNS for template %d: %v", certTemplate.ID, err)
+ tempResults.Xor(cardinality.NodeSetToDuplex(userNodes))
+ } else if subjRequireDomainDns, err := certTemplate.Properties.Get(ad.SubjectAltRequireDomainDNS.String()).Bool(); err != nil {
+ log.Debugf("Failed to retrieve subjectAltRequireDomainDNS for template %d: %v", certTemplate.ID, err)
+ tempResults.Xor(cardinality.NodeSetToDuplex(userNodes))
+ } else if subjRequireDns || subjRequireDomainDns {
+ //If either of these properties is true, we need to remove all these users from our victims list
+ tempResults.Xor(cardinality.NodeSetToDuplex(userNodes))
+ }
+ }
+
+ return tempResults, nil
+}
+
func principalControlsCertTemplate(principal, certTemplate *graph.Node, groupExpansions impact.PathAggregator, cache ADCSCache) bool {
var (
expandedTemplateControllers = cache.ExpandedCertTemplateControllers[certTemplate.ID]
@@ -487,6 +524,14 @@ func PostADCS(ctx context.Context, db graph.Database, groupExpansions impact.Pat
return nil
})
+
+ operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error {
+ if err := PostADCSESC10a(ctx, tx, outC, groupExpansions, innerEnterpriseCA, innerDomain, cache); err != nil {
+ log.Errorf("failed post processing for %s: %v", ad.ADCSESC10a.String(), err)
+ }
+
+ return nil
+ })
}
}
diff --git a/packages/go/analysis/ad/esc10.go b/packages/go/analysis/ad/esc10.go
new file mode 100644
index 0000000000..233ff24a9f
--- /dev/null
+++ b/packages/go/analysis/ad/esc10.go
@@ -0,0 +1,150 @@
+// 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
+
+package ad
+
+import (
+ "context"
+ "errors"
+ "github.com/specterops/bloodhound/analysis"
+ "github.com/specterops/bloodhound/analysis/impact"
+ "github.com/specterops/bloodhound/dawgs/cardinality"
+ "github.com/specterops/bloodhound/dawgs/graph"
+ "github.com/specterops/bloodhound/dawgs/ops"
+ "github.com/specterops/bloodhound/dawgs/query"
+ "github.com/specterops/bloodhound/dawgs/util/channels"
+ "github.com/specterops/bloodhound/graphschema/ad"
+ "github.com/specterops/bloodhound/log"
+)
+
+func PostADCSESC10a(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob, groupExpansions impact.PathAggregator, eca, domain *graph.Node, cache ADCSCache) error {
+ results := cardinality.NewBitmap32()
+
+ if canAbuseUPNRels, err := FetchCanAbuseUPNCertMappingRels(tx, eca); err != nil {
+ if graph.IsErrNotFound(err) {
+ return nil
+ }
+
+ return err
+ } else if len(canAbuseUPNRels) == 0 {
+ return nil
+ } else if publishedCertTemplates, ok := cache.PublishedTemplateCache[eca.ID]; !ok {
+ return nil
+ } else {
+ for _, template := range publishedCertTemplates {
+ if valid, err := isCertTemplateValidForESC10a(template); err != nil {
+ if !errors.Is(err, graph.ErrPropertyNotFound) {
+ log.Errorf("Error checking cert template validity for template %d: %v", template.ID, err)
+ } else {
+ log.Debugf("Error checking cert template validity for template %d: %v", template.ID, err)
+ }
+ } else if !valid {
+ continue
+ } else if certTemplateControllers, ok := cache.CertTemplateControllers[template.ID]; !ok {
+ log.Debugf("Failed to retrieve controllers for cert template %d from cache", template.ID)
+ continue
+ } else if ecaControllers, ok := cache.EnterpriseCAEnrollers[eca.ID]; !ok {
+ log.Debugf("Failed to retrieve controllers for enterprise ca %d from cache", eca.ID)
+ continue
+ } else {
+ //Expand controllers for the eca + template completely because we don't do group shortcutting here
+ var (
+ victimBitmap = expandNodeSliceToBitmapWithoutGroups(certTemplateControllers, groupExpansions)
+ ecaBitmap = expandNodeSliceToBitmapWithoutGroups(ecaControllers, groupExpansions)
+ )
+ victimBitmap.And(ecaBitmap)
+ //Use our id list to filter down to users
+ if userNodes, err := ops.FetchNodeSet(tx.Nodes().Filterf(func() graph.Criteria {
+ return query.And(
+ query.KindIn(query.Node(), ad.User),
+ query.InIDs(query.NodeID(), cardinality.DuplexToGraphIDs(victimBitmap)...),
+ )
+ })); err != nil && !graph.IsErrNotFound(err) {
+ log.Warnf("Error getting user nodes for esc10a attacker nodes: %v", err)
+ continue
+ } else if len(userNodes) > 0 {
+ if subjRequireDns, err := template.Properties.Get(ad.SubjectAltRequireDNS.String()).Bool(); err != nil {
+ log.Debugf("Failed to retrieve subjectAltRequireDNS for template %d: %v", template.ID, err)
+ victimBitmap.Xor(cardinality.NodeSetToDuplex(userNodes))
+ } else if subjRequireDomainDns, err := template.Properties.Get(ad.SubjectAltRequireDomainDNS.String()).Bool(); err != nil {
+ log.Debugf("Failed to retrieve subjectAltRequireDomainDNS for template %d: %v", template.ID, err)
+ victimBitmap.Xor(cardinality.NodeSetToDuplex(userNodes))
+ } else if subjRequireDns || subjRequireDomainDns {
+ //If either of these properties is true, we need to remove all these users from our victims list
+ victimBitmap.Xor(cardinality.NodeSetToDuplex(userNodes))
+ }
+ }
+
+ if attackers, err := ops.FetchStartNodes(tx.Relationships().Filterf(func() graph.Criteria {
+ return query.And(
+ query.KindIn(query.Start(), ad.Group, ad.User, ad.Computer),
+ query.KindIn(query.Relationship(), ad.GenericAll, ad.GenericWrite, ad.Owns, ad.WriteOwner, ad.WriteDACL),
+ query.InIDs(query.EndID(), cardinality.DuplexToGraphIDs(victimBitmap)...),
+ )
+ })); err != nil {
+ log.Warnf("Error getting start nodes for esc10a attacker nodes: %v", err)
+ continue
+ } else {
+ results.Or(cardinality.NodeSetToDuplex(attackers))
+ }
+ }
+ }
+
+ results.Each(func(value uint32) (bool, error) {
+ if !channels.Submit(ctx, outC, analysis.CreatePostRelationshipJob{
+ FromID: graph.ID(value),
+ ToID: domain.ID,
+ Kind: ad.ADCSESC10a,
+ }) {
+ return false, nil
+ } else {
+ return true, nil
+ }
+ })
+
+ return nil
+ }
+}
+
+func isCertTemplateValidForESC10a(ct *graph.Node) (bool, error) {
+ if reqManagerApproval, err := ct.Properties.Get(ad.RequiresManagerApproval.String()).Bool(); err != nil {
+ return false, err
+ } else if reqManagerApproval {
+ return false, nil
+ } else if authenticationEnabled, err := ct.Properties.Get(ad.AuthenticationEnabled.String()).Bool(); err != nil {
+ return false, err
+ } else if !authenticationEnabled {
+ return false, nil
+ } else if enrolleeSuppliesSubject, err := ct.Properties.Get(ad.EnrolleeSuppliesSubject.String()).Bool(); err != nil {
+ return false, err
+ } else if enrolleeSuppliesSubject {
+ return false, nil
+ } else if schemaVersion, err := ct.Properties.Get(ad.SchemaVersion.String()).Float64(); err != nil {
+ return false, err
+ } else if authorizedSignatures, err := ct.Properties.Get(ad.AuthorizedSignatures.String()).Float64(); err != nil {
+ return false, err
+ } else if schemaVersion > 1 && authorizedSignatures > 0 {
+ return false, nil
+ } else if subjectAltRequireUPN, err := ct.Properties.Get(ad.SubjectAltRequireUPN.String()).Bool(); err != nil {
+ return false, err
+ } else if subjectAltRequireSPN, err := ct.Properties.Get(ad.SubjectAltRequireSPN.String()).Bool(); err != nil {
+ return false, err
+ } else if subjectAltRequireSPN || subjectAltRequireUPN {
+ return true, nil
+ } else {
+ return false, nil
+ }
+}
diff --git a/packages/go/analysis/ad/esc6.go b/packages/go/analysis/ad/esc6.go
index 1df859d799..fcf00ade3c 100644
--- a/packages/go/analysis/ad/esc6.go
+++ b/packages/go/analysis/ad/esc6.go
@@ -26,6 +26,7 @@ import (
"github.com/specterops/bloodhound/dawgs/util/channels"
"github.com/specterops/bloodhound/errors"
"github.com/specterops/bloodhound/graphschema/ad"
+ "github.com/specterops/bloodhound/log"
)
func PostCanAbuseUPNCertMapping(operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob], enterpriseCertAuthorities []*graph.Node) error {
@@ -49,7 +50,7 @@ func PostCanAbuseUPNCertMapping(operation analysis.StatTrackedOperation[analysis
} else {
for _, dcForNode := range dcForNodes {
if cmmrProperty, err := dcForNode.Properties.Get(ad.CertificateMappingMethodsRaw.String()).Int(); err != nil {
- collector.Collect(fmt.Errorf("error in PostCanAbuseUPNCertMapping: unable to fetch %v property for node ID %v: %v", ad.StrongCertificateBindingEnforcementRaw.String(), dcForNode.ID, err))
+ log.Warnf("error in PostCanAbuseUPNCertMapping: unable to fetch %v property for node ID %v: %v", ad.StrongCertificateBindingEnforcementRaw.String(), dcForNode.ID, err)
continue
} else if cmmrProperty&0x04 == 0x04 {
if !channels.Submit(ctx, outC, analysis.CreatePostRelationshipJob{
@@ -91,7 +92,7 @@ func PostCanAbuseWeakCertBinding(operation analysis.StatTrackedOperation[analysi
} else {
for _, dcForNode := range dcForNodes {
if strongCertBindingEnforcement, err := dcForNode.Properties.Get(ad.StrongCertificateBindingEnforcementRaw.String()).Int(); err != nil {
- collector.Collect(fmt.Errorf("error in PostCanAbuseWeakCertBinding: unable to fetch %v property for node ID %v: %v", ad.StrongCertificateBindingEnforcementRaw.String(), dcForNode.ID, err))
+ log.Warnf("error in PostCanAbuseWeakCertBinding: unable to fetch %v property for node ID %v: %v", ad.StrongCertificateBindingEnforcementRaw.String(), dcForNode.ID, err)
continue
} else if strongCertBindingEnforcement == 0 || strongCertBindingEnforcement == 1 {
if !channels.Submit(ctx, outC, analysis.CreatePostRelationshipJob{
diff --git a/packages/go/analysis/ad/post.go b/packages/go/analysis/ad/post.go
index 40a32db130..d25b48f695 100644
--- a/packages/go/analysis/ad/post.go
+++ b/packages/go/analysis/ad/post.go
@@ -55,6 +55,9 @@ func PostProcessedRelationships() []graph.Kind {
ad.ADCSESC5,
ad.ADCSESC6a,
ad.ADCSESC7,
+ ad.ADCSESC10a,
+ ad.ADCSESC10b,
+ ad.ADCSESC9a,
ad.EnrollOnBehalfOf,
}
}
diff --git a/packages/go/analysis/ad/queries.go b/packages/go/analysis/ad/queries.go
index 7c3dc6bbf2..ad6a6dc740 100644
--- a/packages/go/analysis/ad/queries.go
+++ b/packages/go/analysis/ad/queries.go
@@ -1439,6 +1439,20 @@ func FetchCanAbuseWeakCertBindingRels(tx graph.Transaction, node *graph.Node) ([
}
}
+func FetchCanAbuseUPNCertMappingRels(tx graph.Transaction, node *graph.Node) ([]*graph.Relationship, error) {
+ if rels, err := ops.FetchRelationships(tx.Relationships().Filterf(func() graph.Criteria {
+ return query.And(
+ query.Equals(query.StartID(), node.ID),
+ query.Kind(query.Relationship(), ad.CanAbuseUPNCertMapping),
+ query.Kind(query.End(), ad.Entity),
+ )
+ })); err != nil {
+ return nil, err
+ } else {
+ return rels, nil
+ }
+}
+
func FetchEnterpriseCAsCertChainPathToDomain(tx graph.Transaction, enterpriseCA, domain *graph.Node) (graph.PathSet, error) {
return ops.TraversePaths(tx, ops.TraversalPlan{
Root: enterpriseCA,
diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go
index 3ec0b72b44..dcfe83f106 100644
--- a/packages/go/dawgs/traversal/traversal.go
+++ b/packages/go/dawgs/traversal/traversal.go
@@ -49,7 +49,9 @@ type PatternMatchDelegate = func(terminal *graph.PathSegment) error
// The return value of the Do(...) function may be passed directly to a Traversal via a Plan as the Plan.Driver field.
type PatternContinuation interface {
Outbound(criteria ...graph.Criteria) PatternContinuation
+ OutboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation
Inbound(criteria ...graph.Criteria) PatternContinuation
+ InboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation
Do(delegate PatternMatchDelegate) Driver
}
@@ -57,6 +59,8 @@ type PatternContinuation interface {
type expansion struct {
criteria []graph.Criteria
direction graph.Direction
+ minDepth int
+ maxDepth int
}
func (s expansion) PrepareCriteria(segment *graph.PathSegment) (graph.Criteria, error) {
@@ -84,6 +88,7 @@ func (s expansion) PrepareCriteria(segment *graph.PathSegment) (graph.Criteria,
type patternTag struct {
patternIdx int
+ depth int
}
func popSegmentPatternTag(segment *graph.PathSegment) *patternTag {
@@ -95,6 +100,7 @@ func popSegmentPatternTag(segment *graph.PathSegment) *patternTag {
} else {
tag = &patternTag{
patternIdx: 0,
+ depth: 0,
}
}
@@ -112,26 +118,62 @@ func (s *pattern) Do(delegate PatternMatchDelegate) Driver {
return s.Driver
}
-// Outbound specifies the next outbound expansion step for this pattern.
-func (s *pattern) Outbound(criteria ...graph.Criteria) PatternContinuation {
+// OutboundWithDepth specifies the next outbound expansion step for this pattern with depth parameters.
+func (s *pattern) OutboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation {
+ if min < 0 {
+ min = 1
+ log.Warnf("Negative mindepth not allowed. Setting min depth for expansion to 1")
+ }
+
+ if max < 0 {
+ max = 0
+ log.Warnf("Negative maxdepth not allowed. Setting max depth for expansion to 0")
+ }
+
s.expansions = append(s.expansions, expansion{
criteria: criteria,
direction: graph.DirectionOutbound,
+ minDepth: min,
+ maxDepth: max,
})
return s
}
-// Inbound specifies the next inbound expansion step for this pattern.
-func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation {
+// Outbound specifies the next outbound expansion step for this pattern. By default, this expansion will use a minimum
+// depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely.
+func (s *pattern) Outbound(criteria ...graph.Criteria) PatternContinuation {
+ return s.OutboundWithDepth(1, 0, criteria...)
+}
+
+// InboundWithDepth specifies the next inbound expansion step for this pattern with depth parameters.
+func (s *pattern) InboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation {
+ if min < 0 {
+ min = 1
+ log.Warnf("Negative mindepth not allowed. Setting min depth for expansion to 1")
+ }
+
+ if max < 0 {
+ max = 0
+ log.Warnf("Negative maxdepth not allowed. Setting max depth for expansion to 0")
+ }
+
s.expansions = append(s.expansions, expansion{
criteria: criteria,
direction: graph.DirectionInbound,
+ minDepth: min,
+ maxDepth: max,
})
return s
}
+// Inbound specifies the next inbound expansion step for this pattern. By default, this expansion will use a minimum
+// depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely.
+func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation {
+ return s.InboundWithDepth(1, 0, criteria...)
+}
+
// NewPattern returns a new PatternContinuation for building a new pattern.
func NewPattern() PatternContinuation {
return &pattern{}
@@ -152,9 +194,9 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra
for next := range cursor.Chan() {
nextSegment := segment.Descend(next.Node, next.Relationship)
nextSegment.Tag = &patternTag{
- // Use the tag's patternIdx here since this is the reference that will see the increment when
- // the current expansion is exhausted
+ // Use the tag's patternIdx and depth since this is a continuation of the expansions
patternIdx: tag.patternIdx,
+ depth: tag.depth + 1,
}
nextSegments = append(nextSegments, nextSegment)
@@ -168,34 +210,51 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra
if fetchDirection, err := currentExpansion.direction.Reverse(); err != nil {
return nil, err
} else {
- // Perform the current expansion.
- if criteria, err := currentExpansion.PrepareCriteria(segment); err != nil {
- return nil, err
- } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil {
- return nil, err
- }
-
- // No further expansions means this pattern segment is complete. Increment the pattern index to select the
- // next pattern expansion.
- tag.patternIdx++
-
- // Perform the next expansion if there is one.
- if tag.patternIdx < len(s.expansions) {
- nextExpansion := s.expansions[tag.patternIdx]
-
- // Expand the next segments
- if criteria, err := nextExpansion.PrepareCriteria(segment); err != nil {
+ // If no max depth was set or if a max depth was set expand the current step further
+ if currentExpansion.maxDepth == 0 || tag.depth < currentExpansion.maxDepth {
+ // Perform the current expansion.
+ if criteria, err := currentExpansion.PrepareCriteria(segment); err != nil {
return nil, err
} else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil {
return nil, err
}
- } else if len(nextSegments) == 0 {
- // If there are no expanded segments and there are no remaining expansions, this is a terminal segment.
- // Hand it off to the delegate and handle any returned error.
- if err := s.delegate(segment); err != nil {
- return nil, err
+ }
+
+ // Check first if this current segment was fetched using the current expansion (i.e. non-optional)
+ if tag.depth > 0 && currentExpansion.minDepth == 0 || tag.depth >= currentExpansion.minDepth {
+ // No further expansions means this pattern segment is complete. Increment the pattern index to select the
+ // next pattern expansion. Additionally, set the depth back to zero for the tag since we are leaving the
+ // current expansion.
+ tag.patternIdx++
+ tag.depth = 0
+
+ // Perform the next expansion if there is one.
+ if tag.patternIdx < len(s.expansions) {
+ nextExpansion := s.expansions[tag.patternIdx]
+
+ // Expand the next segments
+ if criteria, err := nextExpansion.PrepareCriteria(segment); err != nil {
+ return nil, err
+ } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil {
+ return nil, err
+ }
+
+ // If the next expansion is optional, make sure to preserve the current traversal branch
+ if nextExpansion.minDepth == 0 {
+ // Reattach the tag to the segment before adding it to the returned segments for the next expansion
+ segment.Tag = tag
+ nextSegments = append(nextSegments, segment)
+ }
+ } else if len(nextSegments) == 0 {
+ // If there are no expanded segments and there are no remaining expansions, this is a terminal segment.
+ // Hand it off to the delegate and handle any returned error.
+ if err := s.delegate(segment); err != nil {
+ return nil, err
+ }
}
}
+
+ // If the above condition does not match then this current expansion is non-terminal and non-continuable
}
// Return any collected segments
diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go
index 1d02df59bb..5877ce4728 100644
--- a/packages/go/graphschema/ad/ad.go
+++ b/packages/go/graphschema/ad/ad.go
@@ -103,6 +103,8 @@ var (
ADCSESC7 = graph.StringKind("ADCSESC7")
ADCSESC9a = graph.StringKind("ADCSESC9a")
ADCSESC9b = graph.StringKind("ADCSESC9b")
+ ADCSESC10a = graph.StringKind("ADCSESC10a")
+ ADCSESC10b = graph.StringKind("ADCSESC10b")
)
type Property string
@@ -640,13 +642,13 @@ func Nodes() []graph.Kind {
return []graph.Kind{Entity, User, Computer, Group, GPO, OU, Container, Domain, LocalGroup, LocalUser, AIACA, RootCA, EnterpriseCA, NTAuthStore, CertTemplate}
}
func Relationships() []graph.Kind {
- return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, GetChanges, GetChangesAll, GetChangesInFilteredSet, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, LocalToComputer, MemberOfLocalGroup, RemoteInteractiveLogonPrivilege, SyncLAPSPassword, WriteAccountRestrictions, RootCAFor, DCFor, PublishedTo, ManageCertificates, ManageCA, DelegatedEnrollmentAgent, Enroll, HostsCAService, WritePKIEnrollmentFlag, WritePKINameFlag, NTAuthStoreFor, TrustedForNTAuth, EnterpriseCAFor, CanAbuseUPNCertMapping, CanAbuseWeakCertBinding, IssuedSignedBy, GoldenCert, EnrollOnBehalfOf, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6a, ADCSESC7, ADCSESC9a, ADCSESC9b}
+ return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, GetChanges, GetChangesAll, GetChangesInFilteredSet, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, LocalToComputer, MemberOfLocalGroup, RemoteInteractiveLogonPrivilege, SyncLAPSPassword, WriteAccountRestrictions, RootCAFor, DCFor, PublishedTo, ManageCertificates, ManageCA, DelegatedEnrollmentAgent, Enroll, HostsCAService, WritePKIEnrollmentFlag, WritePKINameFlag, NTAuthStoreFor, TrustedForNTAuth, EnterpriseCAFor, CanAbuseUPNCertMapping, CanAbuseWeakCertBinding, IssuedSignedBy, GoldenCert, EnrollOnBehalfOf, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6a, ADCSESC7, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b}
}
func ACLRelationships() []graph.Kind {
return []graph.Kind{AllExtendedRights, ForceChangePassword, AddMember, AddAllowedToAct, GenericAll, WriteDACL, WriteOwner, GenericWrite, ReadLAPSPassword, ReadGMSAPassword, Owns, AddSelf, WriteSPN, AddKeyCredentialLink, GetChanges, GetChangesAll, GetChangesInFilteredSet, WriteAccountRestrictions, SyncLAPSPassword, DCSync, ManageCertificates, ManageCA, Enroll, WritePKIEnrollmentFlag, WritePKINameFlag}
}
func PathfindingRelationships() []graph.Kind {
- return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6a, ADCSESC7, ADCSESC9a, ADCSESC9b, DCFor}
+ return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC5, ADCSESC6a, ADCSESC7, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, DCFor}
}
func IsACLKind(s graph.Kind) bool {
for _, acl := range ACLRelationships() {
diff --git a/packages/javascript/bh-shared-ui/src/commonSearches.tsx b/packages/javascript/bh-shared-ui/src/commonSearches.tsx
index e3b7dc7d72..cf7de1eaee 100644
--- a/packages/javascript/bh-shared-ui/src/commonSearches.tsx
+++ b/packages/javascript/bh-shared-ui/src/commonSearches.tsx
@@ -155,6 +155,52 @@ export const CommonSearches: CommonSearchType[] = [
},
],
},
+ {
+ subheader: 'Active Directory Certificate Services',
+ category: categoryAD,
+ queries: [
+ {
+ description: 'PKI hierarchy',
+ cypher: `MATCH p=()-[:HostsCAService|IssuedSignedBy|EnterpriseCAFor|RootCAFor|TrustedForNTAuth|NTAuthStoreFor*..]->()\nRETURN p`,
+ },
+ {
+ description: 'Public Key Services container',
+ cypher: `MATCH p = (c:Container)-[:Contains*..]->()\nWHERE c.distinguishedname starts with "CN=PUBLIC KEY SERVICES,CN=SERVICES,CN=CONFIGURATION,DC="\nRETURN p`,
+ },
+ {
+ description: 'Enrollment rights on published certificate templates',
+ cypher: `MATCH p = ()-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(:EnterpriseCA)\nRETURN p`,
+ },
+ {
+ description: 'Enrollment rights on published ESC1 certificate templates',
+ cypher: `MATCH p = ()-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(:EnterpriseCA)\nWHERE ct.enrolleesuppliessubject = True\nAND ct.authenticationenabled = True\nAND ct.requiresmanagerapproval = False\nRETURN p`,
+ },
+ {
+ description: 'Enrollment rights on published enrollment agent certificate templates',
+ cypher: `MATCH p = ()-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(:EnterpriseCA)\nWHERE ct.effectiveekus CONTAINS "1.3.6.1.4.1.311.20.2.1"\nOR ct.effectiveekus CONTAINS "2.5.29.37.0"\nOR SIZE(ct.effectiveekus) = 0\nRETURN p`,
+ },
+ {
+ description: 'Enrollment rights on published certificate templates with no security extension',
+ cypher: `MATCH p = ()-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(:EnterpriseCA)\nnWHERE ct.nosecurityextension = true\nRETURN p`,
+ },
+ {
+ description: 'Enrollment rights on certificate templates published to Enterprise CA with User Specified SAN enabled',
+ cypher: `MATCH p = ()-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(eca:EnterpriseCA)\nWHERE eca.isuserspecifiessanenabled = True\nRETURN p`,
+ },
+ {
+ description: 'CA administrators and CA managers',
+ cypher: `MATCH p = ()-[:ManageCertificates|ManageCA]->(:EnterpriseCA)\nRETURN p`,
+ },
+ {
+ description: 'Domain controllers with weak certificate binding enabled',
+ cypher: `MATCH p = (dc:Computer)-[:DCFor]->(d)\nWHERE dc.strongcertificatebindingenforcementraw = 0 OR dc.strongcertificatebindingenforcementraw = 1\nRETURN p`,
+ },
+ {
+ description: 'Domain controllers with UPN certificate mapping enabled',
+ cypher: `MATCH p = (dc:Computer)-[:DCFor]->(d)\nWHERE dc.certificatemappingmethodsraw IN [4, 5, 6, 7, 12, 13, 14, 15, 20, 21, 22, 23, 28, 29, 30, 31]\nRETURN p`,
+ }
+ ],
+ },
{
subheader: 'General',
category: categoryAzure,
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/General.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/General.tsx
index 25356805b2..73d474ef4c 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/General.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/General.tsx
@@ -27,15 +27,14 @@ const General: FC
= ({ sourceName, sourceType, targetName }) => {
domain {targetName}.
- The principal has permission to enroll on a certificate template with the Certificate Request Agent EKU
- (OID 1.3.6.1.4.1.311.20.2.1), allowing them to obtain an enrollment agent certificate. They also have
- permission to enroll for a certificate template that permits enrollment by enrollment agents and can be
- used for authentication. Additionally, they also have enrollment permissions for an enterprise CA with
- the necessary templates published. This enterprise CA is trusted for NT authentication in the forest,
- along with the CA certificate chain up to the root CA certificate. This setup lets the principal enroll
- certificates for any AD forest user or computer, enabling authentication and impersonation of any AD
- forest user or computer without their credentials, unless the target user or computer is protected by
- enrollment agent restrictions on the enterprise CA.
+ The principal has permission to enroll on a certificate allowing them to obtain an enrollment agent
+ certificate. They also have permission to enroll for a certificate template that permits enrollment by
+ enrollment agents and can be used for authentication. Additionally, they also have enrollment
+ permissions for an enterprise CA with the necessary templates published. This enterprise CA is trusted
+ for NT authentication in the forest, along with the CA certificate chain up to the root CA certificate.
+ This setup lets the principal enroll certificates for any AD forest user or computer, enabling
+ authentication and impersonation of any AD forest user or computer without their credentials, unless the
+ target user or computer is protected by enrollment agent restrictions on the enterprise CA.
>
);
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/LinuxAbuse.tsx
index 5b14384420..5a555d739b 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/LinuxAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/LinuxAbuse.tsx
@@ -32,6 +32,15 @@ const LinuxAbuse: FC = () => {
"certipy req -u 'user@corp.local' -p 'password' -dc-ip 'DC_IP' -target 'ca_host' -ca 'ca_name' -template 'vulnerable template'"
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2:
@@ -44,6 +53,12 @@ const LinuxAbuse: FC = () => {
"certipy req -u 'user@corp.local' -p 'password' -dc-ip 'DC_IP' -target 'ca_host' -ca 'ca_name' -template 'User' -on-behalf-of 'contoso\\administrator' -pfx 'user.pfx'"
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the target principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template.
+ Choose another target with the given attribute set.
+
Step 3:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/WindowsAbuse.tsx
index fc9c11c595..e6bd090402 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/WindowsAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/WindowsAbuse.tsx
@@ -30,6 +30,15 @@ const WindowsAbuse: FC = () => {
{'Certify.exe request /ca:CORPDC01.CORP.LOCAL\\CORP-CORPDC01-CA /template:Vuln-EnrollmentAgent'}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2:
@@ -55,6 +64,12 @@ const WindowsAbuse: FC = () => {
Save the certificate as itadminenrollment.pem and the private key as{' '}
itadminenrollment.key.
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the target principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template.
+ Choose another target with the given attribute set.
+
Step 4:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/ADCSESC6a.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/ADCSESC6a.tsx
index 35d0f7205c..a2013b7bc9 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/ADCSESC6a.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/ADCSESC6a.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 ADCSESC6a = {
general: General,
@@ -26,6 +27,7 @@ const ADCSESC6a = {
linuxAbuse: LinuxAbuse,
opsec: Opsec,
references: References,
+ composition: Composition,
};
export default ADCSESC6a;
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/Composition.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/Composition.tsx
new file mode 100644
index 0000000000..4a24ff5eab
--- /dev/null
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/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/components/HelpTexts/ADCSESC6a/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/LinuxAbuse.tsx
index 874627307e..83ce889f64 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/LinuxAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/LinuxAbuse.tsx
@@ -30,6 +30,15 @@ const LinuxAbuse: FC = () => {
'certipy req -u john@corp.local -p Passw0rd -ca corp-DC-CA -target ca.corp.local -template ESC6 -upn administrator@corp.local'
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2: Request a ticket granting ticket (TGT) from the domain, specifying the certificate
created in Step 1 and the IP of a domain controller:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/WindowsAbuse.tsx
index 12e36054d6..296249a25f 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/WindowsAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/WindowsAbuse.tsx
@@ -30,6 +30,15 @@ const WindowsAbuse: FC = () => {
'.\\Certify.exe request /ca:rootdomaindc.forestroot.com\\forestroot-RootDomainDC-CA /template:ESC6 /altname:forestroot\\ForestRootDA'
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2: Convert the emitted certificate to PFX format:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/LinuxAbuse.tsx
index 275e0334c2..2c91fa3bf5 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/LinuxAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/LinuxAbuse.tsx
@@ -33,6 +33,15 @@ const LinuxAbuse: FC = () => {
'certipy req -u john@corp.local -p Passw0rd -ca corp-DC-CA -target ca.corp.local -template ESC6 -upn administrator@corp.local'
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/WindowsAbuse.tsx
index a9b6f1bef5..0d81e85a18 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/WindowsAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/WindowsAbuse.tsx
@@ -33,6 +33,15 @@ const WindowsAbuse: FC = () => {
'.\\Certify.exe request /ca:rootdomaindc.forestroot.com\\forestroot-RootDomainDC-CA /template:ESC6 /altname:forestroot\\ForestRootDA'
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx
index 9de2b72c5f..7dcde37b44 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.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 ADCSESC9a = {
general: General,
@@ -26,6 +27,7 @@ const ADCSESC9a = {
linuxAbuse: LinuxAbuse,
opsec: Opsec,
references: References,
+ composition: Composition
};
export default ADCSESC9a;
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx
new file mode 100644
index 0000000000..4a24ff5eab
--- /dev/null
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/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/components/HelpTexts/utils.ts b/packages/javascript/bh-shared-ui/src/components/HelpTexts/utils.ts
index 7a6caf3abe..afa0cf9ccd 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/utils.ts
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/utils.ts
@@ -14,7 +14,7 @@
//
// SPDX-License-Identifier: Apache-2.0
-import { makeStyles } from "@mui/styles";
+import { makeStyles } from '@mui/styles';
export const groupSpecialFormat = (sourceType: string | undefined, sourceName: string | undefined) => {
if (!sourceType || !sourceName) return 'This entity has';
@@ -44,7 +44,6 @@ export const typeFormat = (type: string | undefined): string => {
}
};
-
export const useHelpTextStyles = makeStyles((theme) => ({
containsCodeEl: {
'& code': {
@@ -59,4 +58,4 @@ export const useHelpTextStyles = makeStyles((theme) => ({
whiteSpace: 'pre-wrap',
},
},
-}));
\ No newline at end of file
+}));
diff --git a/packages/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts
index 3759576a63..f475267d94 100644
--- a/packages/javascript/bh-shared-ui/src/graphSchema.ts
+++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts
@@ -131,6 +131,8 @@ export enum ActiveDirectoryRelationshipKind {
ADCSESC7 = 'ADCSESC7',
ADCSESC9a = 'ADCSESC9a',
ADCSESC9b = 'ADCSESC9b',
+ ADCSESC10a = 'ADCSESC10a',
+ ADCSESC10b = 'ADCSESC10b',
}
export function ActiveDirectoryRelationshipKindToDisplay(value: ActiveDirectoryRelationshipKind): string | undefined {
switch (value) {
@@ -260,6 +262,10 @@ export function ActiveDirectoryRelationshipKindToDisplay(value: ActiveDirectoryR
return 'ADCSESC9a';
case ActiveDirectoryRelationshipKind.ADCSESC9b:
return 'ADCSESC9b';
+ case ActiveDirectoryRelationshipKind.ADCSESC10a:
+ return 'ADCSESC10a';
+ case ActiveDirectoryRelationshipKind.ADCSESC10b:
+ return 'ADCSESC10b';
default:
return undefined;
}
@@ -528,6 +534,8 @@ export function ActiveDirectoryPathfindingEdges(): ActiveDirectoryRelationshipKi
ActiveDirectoryRelationshipKind.ADCSESC7,
ActiveDirectoryRelationshipKind.ADCSESC9a,
ActiveDirectoryRelationshipKind.ADCSESC9b,
+ ActiveDirectoryRelationshipKind.ADCSESC10a,
+ ActiveDirectoryRelationshipKind.ADCSESC10b,
ActiveDirectoryRelationshipKind.DCFor,
];
}