Hey everyone, apologies for the radio silence! It’s been a rather busy period for me, deep in the digital trenches. However I’m excited to be back with the third installment in the ‘Tales from a Threat Hunter’ (TfaTH) series. In this blog post we will focus on an often-overlooked artifact that can be a lifesaver during incident response, particularly when dealing with lateral movement via Windows Remote Management (WinRM).
Introduction
We all know WinRM is a double-edged sword. Essential for legitimate sysadmin tasks, but also a favorite tool for threat actors moving laterally across a network. Standard procedure dictates we dive into the Windows Event Logs – Security, System, and the dedicated Microsoft-Windows-WinRM/Operational logs – to trace this activity.
But what happens when you encounter sophisticated threat actors who know their anti-forensics? They know our playbook. After accomplishing their goals, they commonly clear those crucial event logs, leaving you seemingly blind to their lateral movement activities. When those primary sources vanish, it can feel like the trail has gone cold. This is where understanding less common artifacts as an incident responder becomes critical. Fortunately, Windows often keeps secondary records, and for WinRM client activity, there’s a gem hidden in the registry called WSManSafeClientList
.
Key Characteristics
Hidden deep within the Windows Registry lies a key specifically designed (though perhaps unintentionally from a pure security logging perspective) that maintains information about recent WinRM connections to the target system. Its location in the registry is:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Client\SafeClientList\WSManSafeClientList
This registry key stores a list of the source IP addresses (both IPv4 and IPv6) that have recently established WinRM connections to the system you are investigating. It maintains a rolling history of the last 10 connecting IPs. Essentially, it’s a client-side cache maintained by the Windows Remote Management service. Key characteristics include:
- Append Mechanism: New connection source IPs are added to the end of the binary data structure.
- Most Recent Last: The last valid entry decoded from the list represents the most recent WinRM connection source recorded.
- FIFO Cache: Once 10 entries are present, the oldest entry (at the beginning of the list) is overwritten by subsequent connections.
The registry value is not updated in real-time or immediately upon session termination. Evidence suggests the key is typically updated approximately 15 minutes after a WinRM session is successfully established. We can observe this behavior using monitoring tools. The screenshot below shows my Procmon capture filtered for registry operations on the SafeClientList
value. We can see svchost.exe
performing a RegSetValue
operation, writing to the HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Client\SafeClientList\WSManSafeClientList
key, approximately 15 minutes after the corresponding wsmprovhost.exe
process was initiated on the target system due to the incoming WinRM connection.
The LastWriteTime
of the WSManSafeClientList
registry key doesn’t reflect the exact time of the connection. Instead, it indicates the time when the last update occurred, which is approximately 15 minutes after the most recent connection (that triggered an update) was initiated. Why is this important for DFIR?
- Persistence against log clearing: Unlike event logs, this registry key isn’t typically cleared by attackers focusing solely on
wevtutil cl
or similar log-wiping methods. It persists unless the attacker specifically knows about it and targets it (which is less common based on my experience). - Lateral Movement trail: It directly provides the source IP addresses used by the attacker to connect to the target system via WinRM. Finding an internal IP address here could reveal the previous hop in the attacker’s lateral movement path. Finding an external IP might even point towards attacker infrastructure.
- Timestamp analysis: The
LastWriteTime
timestamp is valuable for correlating WinRM activity with other events on the source and target systems (e.g., network logs, file system timelines, other execution artifacts) around that timeframe.
Decoding WSManSafeClientList
The data is stored in a binary format (REG_BINARY
), meaning it’s not human-readable directly from the Registry Editor. To make sense of this REG_BINARY
value, we need to decode it. The structure within the decoded binary typically follows a pattern:
- The data consists of multiple 16-byte chunks.
- Each 16-byte chunk corresponds to one IP address.
- If a chunk represents an IPv4 address, it usually contains the 4 bytes of the IP address followed by 12 null bytes (or a pattern indicating IPv4-mapped IPv6, often ending in 8 nulls as seen in common implementations).
- If a chunk represents an IPv6 address, it uses all 16 bytes.
You can view the raw binary data directly from the registry using PowerShell and the Format-Hex
cmdlet, as shown in the screenshot below. The hex output highlights the 4-byte sequences corresponding to 10 IP addresses.
Manually decoding this is tedious and error-prone. Thankfully, we can script it. The PowerShell PoC script below demonstrates exactly how to parse this structure. You can find a copy of this script on my GitHub, here. It reads the Base64 representation of the binary value (which you can easily export or copy), decodes it back into bytes, and then parses the byte array, recognizing the patterns for IPv4 and IPv6 addresses stored within.
# Base64 encoded string containing IP address data
$WinRMIPList = (Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\SafeClientList" -Name "WSManSafeClientList").WSManSafeClientList
$base64String = [System.Convert]::ToBase64String($WinRMIPList)
# Decode the Base64 string into a byte array
try {
$value = [System.Convert]::FromBase64String($base64String)
} catch {
Write-Error "Failed to decode Base64 string: $_"
exit 1
}
# Define the size of each IP address entry (16 bytes for IPv6 or padded IPv4)
$chunkSize = 16
# Calculate the number of entries/chunks
$numChunks = [Math]::Floor($value.Length / $chunkSize)
# Loop through the byte array in chunks of $chunkSize
for ($i = 0; $i -lt $numChunks; $i++) {
$entryNumber = $i + 1
try {
# Calculate the start and end index for the current chunk in the byte array
$startIndex = $chunkSize * $i
$endIndex = $startIndex + $chunkSize - 1
# Extract the 16-byte chunk for the current entry
$addrBytes = $value[$startIndex..$endIndex]
# Assume the first 4 bytes is a IPv4 address
$isIPv4 = $true
# Check bytes at index 8 through 15 (the last 8 bytes of the 16-byte chunk)
for ($k = 8; $k -lt $chunkSize; $k++) {
# If any byte in the last 8 is not zero, it's not the IPv4 pattern we expect
if ($addrBytes[$k] -ne 0) {
$isIPv4 = $false
break
}
}
if ($isIPv4) {
# If it ends in 8 nulls, take the first 4 bytes
$ipBytesToParse = $addrBytes[0..3]
# The [System.Net.IPAddress] constructor correctly interprets a 4-byte array as IPv4
$ipAddress = [System.Net.IPAddress]::new($ipBytesToParse)
} else {
# Otherwise, use the full 16-byte chunk
$ipBytesToParse = $addrBytes
# The [System.Net.IPAddress] constructor interprets a 16-byte array as IPv6
$ipAddress = [System.Net.IPAddress]::new($ipBytesToParse)
}
Write-Host "Entry$($entryNumber): $($ipAddress.IPAddressToString)"
} catch {
Write-Error "Error processing Entry$($entryNumber): $_"
}
}
Save the script above (e.g., Decode-WSManSafeClientList.ps1
) and run it on the target machine (or against the registry hive mounted in your forensic workstation). You’ll need administrative permissions to read the HKLM hive. The script will produce output like this, revealing the source IP addresses of incoming WinRM connections to the system, with Entry10 being the most recent.
NOTE: ::1 (IPv6 loopback) might appear if WinRM connections were made to the local machine.
Putting it into Practice
When investigating a system you suspect was used as a pivot point for WinRM lateral movement, especially if event logs are missing or incomplete:
- Acquire: Collect the
SOFTWARE
registry hive, located in the%SystemRoot%\System32\Config\
directory. - Extract: Navigate to the
WSManSafeClientList
value and export/copy itsREG_BINARY
data. Often, forensic tools can export this directly or represent it in Base64. - Decode: Use a script (like the PowerShell example above) to parse the binary data and list the IP addresses.
- Analyze: Correlate these IP addresses with other findings. Does this list confirm suspected lateral movement from other hosts?
Conclusion
While cleared event logs present a significant challenge for us as incident responsders, they don’t always mean the end of the trail. Artifacts like WSManSafeClientList
provide valuable secondary evidence of WinRM client activity. It provides a resilient, albeit delayed, glimpse into recent WinRM activity directed at a host. By understanding where to look and how to decode the data, we can reconstruct parts of the attacker’s path even when they’ve tried to cover their tracks. Add this registry key to your DFIR checklist – it might just hold the missing link in your next investigation.