diff --git a/cmd/api/src/analysis/ad/ntlm_integration_test.go b/cmd/api/src/analysis/ad/ntlm_integration_test.go new file mode 100644 index 0000000000..66280ea64b --- /dev/null +++ b/cmd/api/src/analysis/ad/ntlm_integration_test.go @@ -0,0 +1,138 @@ +// 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 + +//go:build integration +// +build integration + +package ad_test + +import ( + "context" + "strings" + "testing" + + "github.com/specterops/bloodhound/analysis" + ad2 "github.com/specterops/bloodhound/analysis/ad" + "github.com/specterops/bloodhound/analysis/impact" + "github.com/specterops/bloodhound/dawgs/graph" + "github.com/specterops/bloodhound/dawgs/ops" + "github.com/specterops/bloodhound/dawgs/query" + "github.com/specterops/bloodhound/graphschema" + "github.com/specterops/bloodhound/graphschema/ad" + "github.com/specterops/bloodhound/src/test/integration" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPostNTLM(t *testing.T) { + testContext := integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema()) + + testContext.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) error { + harness.NTLMCoerceAndRelayNTLMToSMB.Setup(testContext) + return nil + }, func(harness integration.HarnessDetails, db graph.Database) { + operation := analysis.NewPostRelationshipOperation(context.Background(), db, "NTLM Post Process Test - CoerceAndRelayNTLMToSMB") + + groupExpansions, computers, domains, authenticatedUsers, err := fetchNTLMPrereqs(db) + require.NoError(t, err) + + for _, domain := range domains { + innerDomain := domain + + err = operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + for _, computer := range computers { + innerComputer := computer + domainSid, _ := innerDomain.Properties.Get(ad.Domain.String()).String() + authenticatedUserID := authenticatedUsers[domainSid] + + if err = ad2.PostCoerceAndRelayNTLMToSMB(tx, outC, groupExpansions, innerComputer, authenticatedUserID); err != nil { + t.Logf("failed post processig for %s: %v", ad.CoerceAndRelayNTLMToSMB.String(), err) + } + } + return nil + }) + require.NoError(t, err) + } + + err = operation.Done() + require.NoError(t, err) + + // Test start node + db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { + if results, err := ops.FetchStartNodes(tx.Relationships().Filterf(func() graph.Criteria { + return query.Kind(query.Relationship(), ad.CoerceAndRelayNTLMToSMB) + })); err != nil { + t.Fatalf("error fetching ntlm to smb edges in integration test; %v", err) + } else { + require.Len(t, results, 1) + resultIds := results.IDs() + + objectId := results.Get(resultIds[0]).Properties.Get("objectid") + require.False(t, objectId.IsNil()) + + objectIdStr, err := objectId.String() + require.NoError(t, err) + assert.True(t, strings.HasSuffix(objectIdStr, ad2.AuthenticatedUsersSuffix)) + } + return nil + }) + + // Test end node + db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { + if results, err := ops.FetchEndNodes(tx.Relationships().Filterf(func() graph.Criteria { + return query.Kind(query.Relationship(), ad.CoerceAndRelayNTLMToSMB) + })); err != nil { + t.Fatalf("error fetching ntlm to smb edges in integration test; %v", err) + } else { + require.Len(t, results, 1) + resultIds := results.IDs() + + objectId := results.Get(resultIds[0]).Properties.Get("objectid") + require.False(t, objectId.IsNil()) + + smbSigning, err := results.Get(resultIds[0]).Properties.Get(ad.SMBSigning.String()).Bool() + require.NoError(t, err) + + restrictOutbountNtlm, err := results.Get(resultIds[0]).Properties.Get(ad.RestrictOutboundNTLM.String()).Bool() + require.NoError(t, err) + + assert.False(t, smbSigning) + assert.False(t, restrictOutbountNtlm) + } + return nil + }) + }) +} + +func fetchNTLMPrereqs(db graph.Database) (expansions impact.PathAggregator, computers []*graph.Node, domains []*graph.Node, authenticatedUsers map[string]graph.ID, err error) { + cache := make(map[string]graph.ID) + if expansions, err = ad2.ExpandAllRDPLocalGroups(context.Background(), db); err != nil { + return nil, nil, nil, cache, err + } else if computers, err = ad2.FetchNodesByKind(context.Background(), db, ad.Computer); err != nil { + return nil, nil, nil, cache, err + } else if err = db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { + if cache, err = ad2.FetchAuthUsersMappedToDomains(tx); err != nil { + return err + } + return nil + }); err != nil { + return nil, nil, nil, cache, err + } else if domains, err = ad2.FetchNodesByKind(context.Background(), db, ad.Domain); err != nil { + return nil, nil, nil, cache, err + } else { + return expansions, computers, domains, cache, nil + } +} diff --git a/cmd/api/src/api/v2/auth/oidc.go b/cmd/api/src/api/v2/auth/oidc.go index 0603191ed1..d80d3fc12e 100644 --- a/cmd/api/src/api/v2/auth/oidc.go +++ b/cmd/api/src/api/v2/auth/oidc.go @@ -34,7 +34,7 @@ import ( "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/mediatypes" "github.com/specterops/bloodhound/src/api" - "github.com/specterops/bloodhound/src/api/v2" + v2 "github.com/specterops/bloodhound/src/api/v2" "github.com/specterops/bloodhound/src/config" "github.com/specterops/bloodhound/src/ctx" "github.com/specterops/bloodhound/src/database" diff --git a/cmd/api/src/api/v2/auth/saml.go b/cmd/api/src/api/v2/auth/saml.go index 15c7bdf974..18b7a6a984 100644 --- a/cmd/api/src/api/v2/auth/saml.go +++ b/cmd/api/src/api/v2/auth/saml.go @@ -35,7 +35,7 @@ import ( "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/mediatypes" "github.com/specterops/bloodhound/src/api" - "github.com/specterops/bloodhound/src/api/v2" + v2 "github.com/specterops/bloodhound/src/api/v2" "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/ctx" "github.com/specterops/bloodhound/src/database" diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go index dede7f384a..932732655d 100644 --- a/cmd/api/src/test/integration/harnesses.go +++ b/cmd/api/src/test/integration/harnesses.go @@ -8456,6 +8456,42 @@ func (s *ESC10bHarnessDC2) Setup(graphTestContext *GraphTestContext) { graphTestContext.UpdateNode(s.DC1) } +type NTLMCoerceAndRelayNTLMToSMB struct { + AuthenticatedUsers *graph.Node + DomainAdminsUser *graph.Node + ServerAdmins *graph.Node + computer3 *graph.Node + computer8 *graph.Node +} + +func (s *NTLMCoerceAndRelayNTLMToSMB) Setup(graphTestContext *GraphTestContext) { + domainSid := RandomDomainSID() + s.AuthenticatedUsers = graphTestContext.NewActiveDirectoryGroup("Authenticated Users", domainSid) + s.AuthenticatedUsers.Properties.Set("objectid", fmt.Sprintf("authenticated-users%s", adAnalysis.AuthenticatedUsersSuffix)) + s.AuthenticatedUsers.Properties.Set("Domain", domainSid) + graphTestContext.UpdateNode(s.AuthenticatedUsers) + + s.DomainAdminsUser = graphTestContext.NewActiveDirectoryUser("Domain Admins User", domainSid) + + s.ServerAdmins = graphTestContext.NewActiveDirectoryDomain("Server Admins", domainSid, false, true) + s.ServerAdmins.Properties.Set("objectid", fmt.Sprintf("server-admins%s", adAnalysis.AuthenticatedUsersSuffix)) + s.ServerAdmins.Properties.Set("Domain", domainSid) + graphTestContext.UpdateNode(s.ServerAdmins) + + s.DomainAdminsUser.Properties.Set("objectid", fmt.Sprintf("domainadminuser-users%s", adAnalysis.AuthenticatedUsersSuffix)) + s.computer3 = graphTestContext.NewActiveDirectoryComputer("computer3", domainSid) + + s.computer8 = graphTestContext.NewActiveDirectoryComputer("computer8", domainSid) + s.computer8.Properties.Set(ad.SMBSigning.String(), false) + s.computer8.Properties.Set(ad.RestrictOutboundNTLM.String(), false) + graphTestContext.UpdateNode(s.computer8) + + graphTestContext.NewRelationship(s.computer3, s.ServerAdmins, ad.MemberOf) + graphTestContext.NewRelationship(s.ServerAdmins, s.computer8, ad.AdminTo) + graphTestContext.NewRelationship(s.AuthenticatedUsers, s.computer8, ad.CoerceAndRelayNTLMToSMB) + graphTestContext.NewRelationship(s.computer8, s.DomainAdminsUser, ad.HasSession) +} + type HarnessDetails struct { RDP RDPHarness RDPB RDPHarness2 @@ -8555,4 +8591,5 @@ type HarnessDetails struct { DCSyncHarness DCSyncHarness SyncLAPSPasswordHarness SyncLAPSPasswordHarness HybridAttackPaths HybridAttackPaths + NTLMCoerceAndRelayNTLMToSMB NTLMCoerceAndRelayNTLMToSMB } diff --git a/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.json b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.json new file mode 100644 index 0000000000..7b37bf3018 --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.json @@ -0,0 +1,150 @@ +{ + "style": { + "font-family": "sans-serif", + "background-color": "#ffffff", + "background-image": "", + "background-size": "100%", + "node-color": "#ffffff", + "border-width": 4, + "border-color": "#000000", + "radius": 50, + "node-padding": 5, + "node-margin": 2, + "outside-position": "auto", + "node-icon-image": "", + "node-background-image": "", + "icon-position": "outside", + "icon-size": 64, + "caption-position": "inside", + "caption-max-width": 200, + "caption-color": "#000000", + "caption-font-size": 50, + "caption-font-weight": "normal", + "label-position": "inside", + "label-display": "pill", + "label-color": "#000000", + "label-background-color": "#ffffff", + "label-border-color": "#000000", + "label-border-width": 4, + "label-font-size": 40, + "label-padding": 5, + "label-margin": 4, + "directionality": "directed", + "detail-position": "inline", + "detail-orientation": "parallel", + "arrow-width": 5, + "arrow-color": "#000000", + "margin-start": 5, + "margin-end": 5, + "margin-peer": 20, + "attachment-start": "normal", + "attachment-end": "normal", + "relationship-icon-image": "", + "type-color": "#000000", + "type-background-color": "#ffffff", + "type-border-color": "#000000", + "type-border-width": 0, + "type-font-size": 16, + "type-padding": 5, + "property-position": "outside", + "property-alignment": "colon", + "property-color": "#000000", + "property-font-size": 16, + "property-font-weight": "normal" + }, + "nodes": [ + { + "id": "n0", + "position": { + "x": 0, + "y": 0 + }, + "caption": "computer3", + "style": {}, + "labels": [], + "properties": {} + }, + { + "id": "n1", + "position": { + "x": 284.5, + "y": 0 + }, + "caption": "Server Admins", + "style": {}, + "labels": [], + "properties": {} + }, + { + "id": "n2", + "position": { + "x": 485.67187924757275, + "y": -201.17187924757275 + }, + "caption": "computer8", + "style": {}, + "labels": [], + "properties": { + "smb_signing": "false" + } + }, + { + "id": "n3", + "position": { + "x": 0, + "y": -201.17187924757275 + }, + "caption": "Authenticated Users", + "style": {}, + "labels": [], + "properties": { + "objectid": "authenticatedusers-S-1-5-11" + } + }, + { + "id": "n4", + "position": { + "x": 665.8359396237863, + "y": -381.3359396237863 + }, + "caption": "Domain Admins User", + "style": {}, + "labels": [], + "properties": {} + } + ], + "relationships": [ + { + "id": "n0", + "type": "MemberOf", + "style": {}, + "properties": {}, + "fromId": "n0", + "toId": "n1" + }, + { + "id": "n1", + "type": "AdminTo", + "style": {}, + "properties": {}, + "fromId": "n1", + "toId": "n2" + }, + { + "id": "n2", + "type": "CoerceAndRelayNTLMToSMB", + "style": {}, + "properties": {}, + "fromId": "n3", + "toId": "n2" + }, + { + "id": "n3", + "type": "HasSession", + "style": {}, + "properties": {}, + "fromId": "n2", + "toId": "n4" + } + ] +} \ No newline at end of file diff --git a/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.svg b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.svg new file mode 100644 index 0000000000..acbab20923 --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/CoerceAndRelayNTLMToSMB.svg @@ -0,0 +1,18 @@ + +MemberOfAdminToCoerceAndRelayNTLMToSMBHasSessioncomputer3ServerAdminscomputer8smb_signing:falseAuthenticatedUsersDomainAdminsUser diff --git a/packages/cue/bh/ad/ad.cue b/packages/cue/bh/ad/ad.cue index 248cac332a..bfdc0a44bc 100644 --- a/packages/cue/bh/ad/ad.cue +++ b/packages/cue/bh/ad/ad.cue @@ -735,6 +735,20 @@ MinPwdLength: types.#StringEnum & { representation: "minpwdlength" } +SMBSigning: types.#StringEnum & { + symbol: "SMBSigning" + schema: "ad" + name: "SMB Signing" + representation: "smbsigning" +} + +RestrictOutboundNTLM: types.#StringEnum & { + symbol: "RestrictOutboundNTLM" + schema: "ad" + name: "Restrict Outbound NTLM" + representation: "restrictoutboundntlm" +} + Properties: [ AdminCount, CASecurityCollected, @@ -837,6 +851,8 @@ Properties: [ MaxPwdAge, LockoutDuration, LockoutObservationWindow, + SMBSigning, + RestrictOutboundNTLM ] // Kinds @@ -1288,6 +1304,11 @@ SyncedToEntraUser: types.#Kind & { schema: "active_directory" } +CoerceAndRelayNTLMToSMB: types.#Kind & { + symbol: "CoerceAndRelayNTLMToSMB" + schema: "active_directory" +} + // Relationship Kinds RelationshipKinds: [ Owns, @@ -1358,6 +1379,7 @@ RelationshipKinds: [ ADCSESC10b, ADCSESC13, SyncedToEntraUser, + CoerceAndRelayNTLMToSMB, ] // ACL Relationships @@ -1438,6 +1460,7 @@ PathfindingRelationships: [ ADCSESC13, DCFor, SyncedToEntraUser, + CoerceAndRelayNTLMToSMB, ] EdgeCompositionRelationships: [ @@ -1452,4 +1475,5 @@ EdgeCompositionRelationships: [ ADCSESC10a, ADCSESC10b, ADCSESC13, + CoerceAndRelayNTLMToSMB ] diff --git a/packages/go/analysis/ad/ntlm.go b/packages/go/analysis/ad/ntlm.go new file mode 100644 index 0000000000..22226e77c5 --- /dev/null +++ b/packages/go/analysis/ad/ntlm.go @@ -0,0 +1,146 @@ +// 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/graphschema/ad" + "github.com/specterops/bloodhound/graphschema/common" + "github.com/specterops/bloodhound/log" +) + +// PostNTLM is the initial function used to execute our NTLM analysis +func PostNTLM(ctx context.Context, db graph.Database, groupExpansions impact.PathAggregator) (*analysis.AtomicPostProcessingStats, error) { + operation := analysis.NewPostRelationshipOperation(ctx, db, "PostNTLM") + + // TODO: after adding all of our new NTLM edges, benchmark performance between submitting multiple readers per computer or single reader per computer + err := db.ReadTransaction(ctx, func(tx graph.Transaction) error { + + // Fetch all nodes where the node is a Group and is an Authenticated User + if authenticatedUsersCache, err := FetchAuthUsersMappedToDomains(tx); err != nil { + return err + } else { + // Fetch all nodes where the type is Computer + return tx.Nodes().Filter(query.Kind(query.Node(), ad.Computer)).Fetch(func(cursor graph.Cursor[*graph.Node]) error { + for computer := range cursor.Chan() { + innerComputer := computer + + if domain, err := innerComputer.Properties.Get(ad.Domain.String()).String(); err != nil { + continue + } else if authenticatedUserID, ok := authenticatedUsersCache[domain]; !ok { + continue + } else if err := operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + return PostCoerceAndRelayNTLMToSMB(tx, outC, groupExpansions, innerComputer, authenticatedUserID) + }); err != nil { + log.Warnf("Post processing failed for %s: %v", ad.CoerceAndRelayNTLMToSMB, err) + // Additional analysis may occur if one of our analysis errors + continue + } + } + + return cursor.Error() + }) + } + }) + if err != nil { + operation.Done() + return nil, err + } + + return &operation.Stats, operation.Done() +} + +// PostCoerceAndRelayNTLMToSMB creates edges that allow a computer with unrolled admin access to one or more computers where SMB signing is disabled. +// Comprised solely of adminTo and memberOf edges +func PostCoerceAndRelayNTLMToSMB(tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob, expandedGroups impact.PathAggregator, computer *graph.Node, authenticatedUserID graph.ID) error { + if smbSigningEnabled, err := computer.Properties.Get(ad.SMBSigning.String()).Bool(); errors.Is(err, graph.ErrPropertyNotFound) { + return nil + } else if err != nil { + return err + } else if restrictOutboundNtlm, err := computer.Properties.Get(ad.RestrictOutboundNTLM.String()).Bool(); errors.Is(err, graph.ErrPropertyNotFound) { + return nil + } else if err != nil { + return err + } else if !smbSigningEnabled && !restrictOutboundNtlm { + + // Fetch the admins with edges to the provided computer + if firstDegreeAdmins, err := fetchFirstDegreeNodes(tx, computer, ad.AdminTo); err != nil { + return err + } else if firstDegreeAdmins.ContainingNodeKinds(ad.Computer).Len() > 0 { + outC <- analysis.CreatePostRelationshipJob{ + FromID: authenticatedUserID, + ToID: computer.ID, + Kind: ad.CoerceAndRelayNTLMToSMB, + } + } else { + allAdminGroups := cardinality.NewBitmap64() + for group := range firstDegreeAdmins.ContainingNodeKinds(ad.Group) { + allAdminGroups.And(expandedGroups.Cardinality(group.Uint64())) + } + + // Fetch nodes where the node id is in our allAdminGroups bitmap and are of type Computer + if computerIds, err := ops.FetchNodeIDs(tx.Nodes().Filter( + query.And( + query.InIDs(query.Node(), graph.DuplexToGraphIDs(allAdminGroups)...), + query.Kind(query.Node(), ad.Computer), + ), + )); err != nil { + return err + } else if len(computerIds) > 0 { + outC <- analysis.CreatePostRelationshipJob{ + FromID: authenticatedUserID, + ToID: computer.ID, + Kind: ad.CoerceAndRelayNTLMToSMB, + } + } + } + } + + return nil +} + +// FetchAuthUsersMappedToDomains Fetch all nodes where the node is a Group and is an Authenticated User +func FetchAuthUsersMappedToDomains(tx graph.Transaction) (map[string]graph.ID, error) { + authenticatedUsers := make(map[string]graph.ID) + + err := tx.Nodes().Filter( + query.And( + query.Kind(query.Node(), ad.Group), + query.StringEndsWith(query.NodeProperty(common.ObjectID.String()), AuthenticatedUsersSuffix)), + ).Fetch(func(cursor graph.Cursor[*graph.Node]) error { + for authenticatedUser := range cursor.Chan() { + if domain, err := authenticatedUser.Properties.Get(ad.Domain.String()).String(); err != nil { + continue + } else { + authenticatedUsers[domain] = authenticatedUser.ID + } + } + + return cursor.Error() + }, + ) + + return authenticatedUsers, err +} diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go index 20e66a8b4f..c202199613 100644 --- a/packages/go/graphschema/ad/ad.go +++ b/packages/go/graphschema/ad/ad.go @@ -109,6 +109,7 @@ var ( ADCSESC10b = graph.StringKind("ADCSESC10b") ADCSESC13 = graph.StringKind("ADCSESC13") SyncedToEntraUser = graph.StringKind("SyncedToEntraUser") + CoerceAndRelayNTLMToSMB = graph.StringKind("CoerceAndRelayNTLMToSMB") ) type Property string @@ -215,10 +216,12 @@ const ( MaxPwdAge Property = "maxpwdage" LockoutDuration Property = "lockoutduration" LockoutObservationWindow Property = "lockoutobservationwindow" + SMBSigning Property = "smbsigning" + RestrictOutboundNTLM Property = "restrictoutboundntlm" ) func AllProperties() []Property { - return []Property{AdminCount, CASecurityCollected, CAName, CertChain, CertName, CertThumbprint, CertThumbprints, HasEnrollmentAgentRestrictions, EnrollmentAgentRestrictionsCollected, IsUserSpecifiesSanEnabled, IsUserSpecifiesSanEnabledCollected, RoleSeparationEnabled, RoleSeparationEnabledCollected, HasBasicConstraints, BasicConstraintPathLength, UnresolvedPublishedTemplates, DNSHostname, CrossCertificatePair, DistinguishedName, DomainFQDN, DomainSID, Sensitive, HighValue, BlocksInheritance, IsACL, IsACLProtected, IsDeleted, Enforced, Department, HasCrossCertificatePair, HasSPN, UnconstrainedDelegation, LastLogon, LastLogonTimestamp, IsPrimaryGroup, HasLAPS, DontRequirePreAuth, LogonType, HasURA, PasswordNeverExpires, PasswordNotRequired, FunctionalLevel, TrustType, SidFiltering, TrustedToAuth, SamAccountName, CertificateMappingMethodsRaw, CertificateMappingMethods, StrongCertificateBindingEnforcementRaw, StrongCertificateBindingEnforcement, EKUs, SubjectAltRequireUPN, SubjectAltRequireDNS, SubjectAltRequireDomainDNS, SubjectAltRequireEmail, SubjectAltRequireSPN, SubjectRequireEmail, AuthorizedSignatures, ApplicationPolicies, IssuancePolicies, SchemaVersion, RequiresManagerApproval, AuthenticationEnabled, SchannelAuthenticationEnabled, EnrolleeSuppliesSubject, CertificateApplicationPolicy, CertificateNameFlag, EffectiveEKUs, EnrollmentFlag, Flags, NoSecurityExtension, RenewalPeriod, ValidityPeriod, OID, HomeDirectory, CertificatePolicy, CertTemplateOID, GroupLinkID, ObjectGUID, ExpirePasswordsOnSmartCardOnlyAccounts, MachineAccountQuota, SupportedKerberosEncryptionTypes, TGTDelegationEnabled, PasswordStoredUsingReversibleEncryption, SmartcardRequired, UseDESKeyOnly, LogonScriptEnabled, LockedOut, UserCannotChangePassword, PasswordExpired, DSHeuristics, UserAccountControl, TrustAttributes, MinPwdLength, PwdProperties, PwdHistoryLength, LockoutThreshold, MinPwdAge, MaxPwdAge, LockoutDuration, LockoutObservationWindow} + return []Property{AdminCount, CASecurityCollected, CAName, CertChain, CertName, CertThumbprint, CertThumbprints, HasEnrollmentAgentRestrictions, EnrollmentAgentRestrictionsCollected, IsUserSpecifiesSanEnabled, IsUserSpecifiesSanEnabledCollected, RoleSeparationEnabled, RoleSeparationEnabledCollected, HasBasicConstraints, BasicConstraintPathLength, UnresolvedPublishedTemplates, DNSHostname, CrossCertificatePair, DistinguishedName, DomainFQDN, DomainSID, Sensitive, HighValue, BlocksInheritance, IsACL, IsACLProtected, IsDeleted, Enforced, Department, HasCrossCertificatePair, HasSPN, UnconstrainedDelegation, LastLogon, LastLogonTimestamp, IsPrimaryGroup, HasLAPS, DontRequirePreAuth, LogonType, HasURA, PasswordNeverExpires, PasswordNotRequired, FunctionalLevel, TrustType, SidFiltering, TrustedToAuth, SamAccountName, CertificateMappingMethodsRaw, CertificateMappingMethods, StrongCertificateBindingEnforcementRaw, StrongCertificateBindingEnforcement, EKUs, SubjectAltRequireUPN, SubjectAltRequireDNS, SubjectAltRequireDomainDNS, SubjectAltRequireEmail, SubjectAltRequireSPN, SubjectRequireEmail, AuthorizedSignatures, ApplicationPolicies, IssuancePolicies, SchemaVersion, RequiresManagerApproval, AuthenticationEnabled, SchannelAuthenticationEnabled, EnrolleeSuppliesSubject, CertificateApplicationPolicy, CertificateNameFlag, EffectiveEKUs, EnrollmentFlag, Flags, NoSecurityExtension, RenewalPeriod, ValidityPeriod, OID, HomeDirectory, CertificatePolicy, CertTemplateOID, GroupLinkID, ObjectGUID, ExpirePasswordsOnSmartCardOnlyAccounts, MachineAccountQuota, SupportedKerberosEncryptionTypes, TGTDelegationEnabled, PasswordStoredUsingReversibleEncryption, SmartcardRequired, UseDESKeyOnly, LogonScriptEnabled, LockedOut, UserCannotChangePassword, PasswordExpired, DSHeuristics, UserAccountControl, TrustAttributes, MinPwdLength, PwdProperties, PwdHistoryLength, LockoutThreshold, MinPwdAge, MaxPwdAge, LockoutDuration, LockoutObservationWindow, SMBSigning, RestrictOutboundNTLM} } func ParseProperty(source string) (Property, error) { switch source { @@ -424,6 +427,10 @@ func ParseProperty(source string) (Property, error) { return LockoutDuration, nil case "lockoutobservationwindow": return LockoutObservationWindow, nil + case "smbsigning": + return SMBSigning, nil + case "restrictoutboundntlm": + return RestrictOutboundNTLM, nil default: return "", errors.New("Invalid enumeration value: " + source) } @@ -632,6 +639,10 @@ func (s Property) String() string { return string(LockoutDuration) case LockoutObservationWindow: return string(LockoutObservationWindow) + case SMBSigning: + return string(SMBSigning) + case RestrictOutboundNTLM: + return string(RestrictOutboundNTLM) default: return "Invalid enumeration case: " + string(s) } @@ -840,6 +851,10 @@ func (s Property) Name() string { return "Lockout Duration" case LockoutObservationWindow: return "Lockout Observation Window" + case SMBSigning: + return "SMB Signing" + case RestrictOutboundNTLM: + return "Restrict Outbound NTLM" default: return "Invalid enumeration case: " + string(s) } @@ -856,13 +871,13 @@ func Nodes() []graph.Kind { return []graph.Kind{Entity, User, Computer, Group, GPO, OU, Container, Domain, LocalGroup, LocalUser, AIACA, RootCA, EnterpriseCA, NTAuthStore, CertTemplate, IssuancePolicy} } func Relationships() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, CoerceToTGT, GetChanges, GetChangesAll, GetChangesInFilteredSet, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, LocalToComputer, MemberOfLocalGroup, RemoteInteractiveLogonRight, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, RootCAFor, DCFor, PublishedTo, ManageCertificates, ManageCA, DelegatedEnrollmentAgent, Enroll, HostsCAService, WritePKIEnrollmentFlag, WritePKINameFlag, NTAuthStoreFor, TrustedForNTAuth, EnterpriseCAFor, IssuedSignedBy, GoldenCert, EnrollOnBehalfOf, OIDGroupLink, ExtendedByPolicy, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToEntraUser} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, CoerceToTGT, GetChanges, GetChangesAll, GetChangesInFilteredSet, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, LocalToComputer, MemberOfLocalGroup, RemoteInteractiveLogonRight, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, RootCAFor, DCFor, PublishedTo, ManageCertificates, ManageCA, DelegatedEnrollmentAgent, Enroll, HostsCAService, WritePKIEnrollmentFlag, WritePKINameFlag, NTAuthStoreFor, TrustedForNTAuth, EnterpriseCAFor, IssuedSignedBy, GoldenCert, EnrollOnBehalfOf, OIDGroupLink, ExtendedByPolicy, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToEntraUser, CoerceAndRelayNTLMToSMB} } 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, WriteGPLink, 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, CoerceToTGT, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, DCFor, SyncedToEntraUser} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, GPLink, AllowedToDelegate, CoerceToTGT, TrustedBy, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, DCFor, SyncedToEntraUser, CoerceAndRelayNTLMToSMB} } func IsACLKind(s graph.Kind) bool { for _, acl := range ACLRelationships() { diff --git a/packages/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts index 642cef13c0..a1637c42c4 100644 --- a/packages/javascript/bh-shared-ui/src/graphSchema.ts +++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts @@ -139,6 +139,7 @@ export enum ActiveDirectoryRelationshipKind { ADCSESC10b = 'ADCSESC10b', ADCSESC13 = 'ADCSESC13', SyncedToEntraUser = 'SyncedToEntraUser', + CoerceAndRelayNTLMToSMB = 'CoerceAndRelayNTLMToSMB', } export function ActiveDirectoryRelationshipKindToDisplay(value: ActiveDirectoryRelationshipKind): string | undefined { switch (value) { @@ -278,6 +279,8 @@ export function ActiveDirectoryRelationshipKindToDisplay(value: ActiveDirectoryR return 'ADCSESC13'; case ActiveDirectoryRelationshipKind.SyncedToEntraUser: return 'SyncedToEntraUser'; + case ActiveDirectoryRelationshipKind.CoerceAndRelayNTLMToSMB: + return 'CoerceAndRelayNTLMToSMB'; default: return undefined; } @@ -295,6 +298,7 @@ export const EdgeCompositionRelationships = [ 'ADCSESC10a', 'ADCSESC10b', 'ADCSESC13', + 'CoerceAndRelayNTLMToSMB', ]; export enum ActiveDirectoryKindProperties { AdminCount = 'admincount', @@ -398,6 +402,8 @@ export enum ActiveDirectoryKindProperties { MaxPwdAge = 'maxpwdage', LockoutDuration = 'lockoutduration', LockoutObservationWindow = 'lockoutobservationwindow', + SMBSigning = 'smbsigning', + RestrictOutboundNTLM = 'restrictoutboundntlm', } export function ActiveDirectoryKindPropertiesToDisplay(value: ActiveDirectoryKindProperties): string | undefined { switch (value) { @@ -603,6 +609,10 @@ export function ActiveDirectoryKindPropertiesToDisplay(value: ActiveDirectoryKin return 'Lockout Duration'; case ActiveDirectoryKindProperties.LockoutObservationWindow: return 'Lockout Observation Window'; + case ActiveDirectoryKindProperties.SMBSigning: + return 'SMB Signing'; + case ActiveDirectoryKindProperties.RestrictOutboundNTLM: + return 'Restrict Outbound NTLM'; default: return undefined; } @@ -655,6 +665,7 @@ export function ActiveDirectoryPathfindingEdges(): ActiveDirectoryRelationshipKi ActiveDirectoryRelationshipKind.ADCSESC13, ActiveDirectoryRelationshipKind.DCFor, ActiveDirectoryRelationshipKind.SyncedToEntraUser, + ActiveDirectoryRelationshipKind.CoerceAndRelayNTLMToSMB, ]; } export enum AzureNodeKind {