Description:
CoerceAndRelay edges (LDAP, LDAPS, ADCS, SMB) are never created when the RestrictSendingNTLMTraffic registry key does not exist on target computers, which is the default on every Windows machine where the "Network security: Restrict NTLM: Outgoing NTLM traffic to remote servers" policy has never been configured.
This is a regression introduced by #1787. Before that PR, the NTLMRegistryInfo fields were non-pointer uint types, so an absent registry key defaulted to 0, and 0 == 2 evaluated to false (not restricted), which is correct. After #1787 changed the fields to *uint pointers to support nullable values, an absent key becomes nil, which causes the ingestion to set RestrictOutboundNTLM to nil on the graph node. The analysis in PostNTLM (packages/go/analysis/ad/ntlm.go:96-101) then fails to read the property as a boolean and skips the computer entirely (fail-closed), excluding it from UnprotectedComputersCache and blocking all four edge types.
Per Microsoft's documentation:
"This state of being not defined is the same as Allow all, and the device will allow all NTLM authentication requests when the policy is deployed."
Since most environments never configure this policy, this effectively prevents CoerceAndRelay edges from being created in the majority of deployments.
Are you intending to fix this bug?
Yes. I can submit a PR, but the guidelines says to create an issue first. I'd like to get your take on this before I submit the PR.
Component(s) Affected:
Steps to Reproduce:
- Collect data with SharpHound (
--collectionmethods all) as Domain Admin against an environment where the RestrictSendingNTLMTraffic registry key has never been configured (the Windows default)
- Ingest data into BloodHound CE with
ntlm_post_processing feature flag enabled
- Verify prerequisites are met: SMB signing disabled on some computers, WebClient running, LDAP signing not enforced on DCs, vulnerable ADCS templates
- Run the built-in "All coerce and NTLM relay edges" query or:
MATCH p = (n:Base)-[:CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|CoerceAndRelayNTLMToADCS|CoerceAndRelayNTLMToSMB]->(:Base) RETURN p LIMIT 500
- Zero results returned despite all prerequisites being met
Expected Behavior:
When NTLMRegistryData.Collected is true and RestrictSendingNtlmTraffic is nil (key absent), RestrictOutboundNTLM should be set to false (not restricted), reflecting the Windows default of "Allow all." CoerceAndRelay edges should be created for computers meeting the other prerequisites.
Actual Behavior:
RestrictOutboundNTLM is set to nil in packages/go/ein/ad.go:110. The analysis in packages/go/analysis/ad/ntlm.go:96-101 fails to read the property as a boolean, treats the computer as protected (fail-closed), and excludes it from UnprotectedComputersCache. No CoerceAndRelay edges are created for any of the four types.
The registry key simply does not exist on a default Windows Server 2025 installation:
C:\>reg query HKLM\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0
Auth132 REG_SZ IISSUBA
NtlmMinClientSec REG_DWORD 0x20000000
NtlmMinServerSec REG_DWORD 0x20000000
Auth2 REG_SZ RASSFM
SharpHound correctly reports this as NTLMRegistryData.Collected: true with RestrictSendingNtlmTraffic: null.
Environment Information:
BloodHound: CE v8.7.0 (current main)
Collector: SharpHound 2.11.0
OS: Windows Server 2025 Standard (target DC/computers), Ubuntu 25.10 (BH host)
Docker: Docker Compose deployment
Potential Solution (optional):
In packages/go/ein/ad.go, change line 110 from:
itemProps[ad.RestrictOutboundNTLM.String()] = nil
to:
itemProps[ad.RestrictOutboundNTLM.String()] = false
This only affects the case where registry data was successfully collected (Collected=true) but the key is absent. Unreachable computers where collection failed remain unaffected (property stays unset, analysis still skips them with fail-closed behavior preserved).
I was also worried there may a scenario where the key is absent but NTLM is restricted. However, all I've found is that absence of the key is definitively "Allow All" per Microsoft. The only way to restrict is to explicitly create the key with value 1 or 2. The nil default in #1787 was arguably a bit conservative for this specific property. It makes sense for other registry values where absence of the key might be ambiguous, but RestrictSendingNTLMTraffic has a documented default.
Note that #1440 proposed a more comprehensive approach with a UI-configurable default value, but was closed in favor of #1787.
Related Issues:
Contributor Checklist:
Disclosure: I used LLM/AI to assist me in tracing down the issue in the code. I've reviewed the proposed fix, understand everything about it, done my own research on this issue, and feel confident about sharing this.
Description:
CoerceAndRelay edges (LDAP, LDAPS, ADCS, SMB) are never created when the
RestrictSendingNTLMTrafficregistry key does not exist on target computers, which is the default on every Windows machine where the "Network security: Restrict NTLM: Outgoing NTLM traffic to remote servers" policy has never been configured.This is a regression introduced by #1787. Before that PR, the
NTLMRegistryInfofields were non-pointeruinttypes, so an absent registry key defaulted to0, and0 == 2evaluated tofalse(not restricted), which is correct. After #1787 changed the fields to*uintpointers to support nullable values, an absent key becomesnil, which causes the ingestion to setRestrictOutboundNTLMtonilon the graph node. The analysis inPostNTLM(packages/go/analysis/ad/ntlm.go:96-101) then fails to read the property as a boolean and skips the computer entirely (fail-closed), excluding it fromUnprotectedComputersCacheand blocking all four edge types.Per Microsoft's documentation:
Since most environments never configure this policy, this effectively prevents CoerceAndRelay edges from being created in the majority of deployments.
Are you intending to fix this bug?
Yes. I can submit a PR, but the guidelines says to create an issue first. I'd like to get your take on this before I submit the PR.
Component(s) Affected:
Steps to Reproduce:
--collectionmethods all) as Domain Admin against an environment where theRestrictSendingNTLMTrafficregistry key has never been configured (the Windows default)ntlm_post_processingfeature flag enabledExpected Behavior:
When
NTLMRegistryData.CollectedistrueandRestrictSendingNtlmTrafficisnil(key absent),RestrictOutboundNTLMshould be set tofalse(not restricted), reflecting the Windows default of "Allow all." CoerceAndRelay edges should be created for computers meeting the other prerequisites.Actual Behavior:
RestrictOutboundNTLMis set tonilinpackages/go/ein/ad.go:110. The analysis inpackages/go/analysis/ad/ntlm.go:96-101fails to read the property as a boolean, treats the computer as protected (fail-closed), and excludes it fromUnprotectedComputersCache. No CoerceAndRelay edges are created for any of the four types.The registry key simply does not exist on a default Windows Server 2025 installation:
SharpHound correctly reports this as
NTLMRegistryData.Collected: truewithRestrictSendingNtlmTraffic: null.Environment Information:
BloodHound: CE v8.7.0 (current main)
Collector: SharpHound 2.11.0
OS: Windows Server 2025 Standard (target DC/computers), Ubuntu 25.10 (BH host)
Docker: Docker Compose deployment
Potential Solution (optional):
In
packages/go/ein/ad.go, change line 110 from:to:
This only affects the case where registry data was successfully collected (
Collected=true) but the key is absent. Unreachable computers where collection failed remain unaffected (property stays unset, analysis still skips them with fail-closed behavior preserved).I was also worried there may a scenario where the key is absent but NTLM is restricted. However, all I've found is that absence of the key is definitively "Allow All" per Microsoft. The only way to restrict is to explicitly create the key with value 1 or 2. The nil default in #1787 was arguably a bit conservative for this specific property. It makes sense for other registry values where absence of the key might be ambiguous, but
RestrictSendingNTLMTraffichas a documented default.Note that #1440 proposed a more comprehensive approach with a UI-configurable default value, but was closed in favor of #1787.
Related Issues:
NTLMRegistryInfofields to nullable pointersRestrictOutboundNTLM(closedwithout merge)
Contributor Checklist:
addressed.
Disclosure: I used LLM/AI to assist me in tracing down the issue in the code. I've reviewed the proposed fix, understand everything about it, done my own research on this issue, and feel confident about sharing this.