Bug: CoerceAndRelay edges not created when RestrictSendingNTLMTraffic registry key is absent (default Windows config)

Open Beginner friendly
#2,534 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
84/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
go
Domain
api, backend, security

Research direction

Start in packages/go/ein/ad.go at the RestrictOutboundNTLM property assignment, then read packages/go/analysis/ad/ntlm.go:96-101 to understand how the value controls UnprotectedComputersCache. Reproduce the absent-key case with NTLMRegistryData.Collected true and verify that RestrictOutboundNTLM is treated as false and the four CoerceAndRelay edge types can be created.

Written by the indexing model from the issue text.

Description

bug triage

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:

  • API

Steps to Reproduce:

  1. Collect data with SharpHound (--collectionmethods all) as Domain Admin against an environment where the RestrictSendingNTLMTraffic registry key has never been configured (the Windows default)
  2. Ingest data into BloodHound CE with ntlm_post_processing feature flag enabled
  3. Verify prerequisites are met: SMB signing disabled on some computers, WebClient running, LDAP signing not enforced on DCs, vulnerable ADCS templates
  4. 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
    
  5. 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:

  • #1787 — The PR that introduced this regression by changing NTLMRegistryInfo fields to nullable pointers
  • #1440 — A previously proposed fix that included a UI-configurable default value for RestrictOutboundNTLM (closed
    without merge)

Contributor Checklist:

  • I have searched the issue tracker to ensure this bug hasn't been reported before or is not already being
    addressed.
  • I have provided clear steps to reproduce the issue.
  • I have included relevant environment information details.
  • I have attached necessary supporting documents.
  • I have checked that any JSON files I am attempting to upload to BloodHound are valid.

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.

Dominant language
Go
Stars
3.4k
Forks
376
Avg merge
2d 7h
Merged PRs (30d)
97

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from SpecterOps/BloodHound

All issues in SpecterOps/BloodHound

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.