feat(creds): DEBT-040 Phase 3 — RDP NLA / CredSSP NTLMv2 capture

When RDP_ENABLE_NLA=true (service_cfg.nla=true on the topology side),
confirm PROTOCOL_HYBRID on the X.224 Connection Confirm, upgrade the
socket to TLS using a self-signed cert generated at first start by
the entrypoint, then drive a tiny CredSSP loop:

- Read inbound TSRequest DER (bounded to MAX_TSREQUEST_LEN).
- Scan for the NTLMSSP signature, dispatch on message type:
  Type 1 -> respond with a hand-built TSRequest carrying our Type 2
  challenge. Type 3 -> parse_type3() and emit auth_attempt with the
  universal credential SD shape (secret_kind = ntlmssp_v2).
- Hand-built DER: no pyasn1 dependency.

Also folds in a small fix-up to commit 1: SMB SERVER_CHALLENGE was
hardcoded to 0x11..0x88 across the fleet, which would let a scanner
fingerprint every DECNET decky by its NTLM challenge. Both SMB and
RDP now derive the 8-byte challenge from
instance_seed.random_bytes(8, "ntlm_challenge"), giving each decky a
deterministic-but-distinct value. SMB Dockerfile gets the
instance_seed.py copy too (was synced into the build context but not
COPYed into the image).

- decnet/services/rdp.py: optional service_cfg.nla bool flips
  RDP_ENABLE_NLA in the compose env.
- decnet/templates/rdp/Dockerfile + entrypoint.sh: openssl install +
  per-decky cert generation gated on RDP_ENABLE_NLA.
- 9 NLA unit tests cover the DER reader/builder, _handle_nla round-
  trip with Type 1 / Type 3, oversized-DER rejection, and per-
  NODE_NAME challenge divergence.
- DEBT.md: DEBT-040 closed; full TS_INFO_PACKET capture documented as
  a follow-up if attacker telemetry justifies it.
This commit is contained in:
2026-04-25 07:42:52 -04:00
parent a8b9c82c97
commit b3d1301925
9 changed files with 506 additions and 35 deletions

View File

@@ -22,6 +22,7 @@ import asyncio
import os
import struct
import instance_seed
from ntlmssp import find_ntlmssp, parse_type3
from syslog_bridge import syslog_line, write_syslog_file, forward_syslog
@@ -44,10 +45,13 @@ SMB2_SESSION_SETUP = 0x0001
SMB2_MAGIC = b"\xfeSMB"
NBSS_SESSION_MESSAGE = 0x00
# Server's fixed 8-byte NTLM challenge (random-looking; honeypot, not crypto)
SERVER_CHALLENGE = b"\x11\x22\x33\x44\x55\x66\x77\x88"
# Stable server GUID — 16 zero bytes is fine for a honeypot.
SERVER_GUID = b"\x00" * 16
# Per-instance NTLM challenge: deterministic-per-decky-but-different-
# across-the-fleet. Derived from NODE_NAME so two captures from the
# same decky reuse the same challenge (lets offline attackers retry
# wordlists), while every decky in the fleet differs (looks like a
# real population of hosts to a scanner).
SERVER_CHALLENGE = instance_seed.random_bytes(8, "ntlm_challenge")
SERVER_GUID = instance_seed.random_bytes(16, "smb_server_guid")
# Read caps; an attacker shouldn't be able to make us allocate
# unbounded memory just by lying about NetBIOS frame length.