mirror of
https://github.com/crypt0p3g/dpapi-toolkit
synced 2026-09-24 08:14:49 +00:00
Initial release
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
.DS_Store
|
||||
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
*_dec.*
|
||||
*_dec/
|
||||
*.pfx
|
||||
*.pvk
|
||||
*.key
|
||||
CacheData
|
||||
DPAPI_SYSTEM
|
||||
batch_report.json
|
||||
|
||||
SYSTEM
|
||||
SECURITY
|
||||
SAM
|
||||
NTUSER.DAT
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 dpapi-toolkit contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,195 @@
|
||||
# DPAPI-toolkit
|
||||
|
||||
An offline toolkit for Windows DPAPI evidence. You collect the files, point the
|
||||
tool at an artifact, and it recognizes the format and tells you exactly which
|
||||
master key it needs; you supply the key material and it decrypts.
|
||||
|
||||
Most DPAPI tooling is one script per format, or a feature buried inside a larger
|
||||
offensive framework. This is one tool for the whole range — master keys, DPAPI
|
||||
blobs, Credential Manager, Vault, CREDHIST, Wi-Fi, RDP/RDCMan, CAPI/CNG keys and
|
||||
certificates, PowerShell SecureStrings, KeePass, SCCM, Chromium `os_crypt` keys,
|
||||
and (with an optional plugin) DPAPI-NG — driven from collected files alone, with
|
||||
a command-line interface and a local drag-and-drop web UI over the same core.
|
||||
|
||||
It is built for red team and penetration testing, DFIR, and security research.
|
||||
No live host, domain controller, network, BKRP, or LSASS access is used, and it
|
||||
does not brute-force passwords or PINs: you supply the collected artifacts and
|
||||
key material explicitly.
|
||||
|
||||
> **Only use this against systems and evidence you own or are explicitly
|
||||
> authorized to examine.**
|
||||
|
||||

|
||||
|
||||
## Example
|
||||
|
||||
You've collected a user's `Protect` folder and a Credential Manager file offline
|
||||
and you know the account password. Inspect the credential first — no key needed:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py CREDENTIAL_FILE
|
||||
```
|
||||
|
||||
It prints the embedded DPAPI blob and the master-key GUID it requires. Point it
|
||||
at the `Protect` directory and let it match and unlock that master key, then
|
||||
decrypt:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py CREDENTIAL_FILE --type credential \
|
||||
--masterkey-dir Protect-S-1-5-21-... \
|
||||
--sid S-1-5-21-... \
|
||||
--password 'password'
|
||||
```
|
||||
|
||||
The result is JSON with the target, username, and recovered credential. The same
|
||||
inspect-then-decrypt flow applies to every supported format.
|
||||
|
||||
## Install
|
||||
|
||||
Python 3.10 or newer. The only hard dependency is
|
||||
[`cryptography`](https://pypi.org/project/cryptography/):
|
||||
|
||||
```bash
|
||||
python3 -m pip install cryptography
|
||||
```
|
||||
|
||||
Optional dependencies enable specific features:
|
||||
|
||||
```bash
|
||||
python3 -m pip install python-registry # read Outlook data from NTUSER.DAT
|
||||
python3 -m pip install dpapi-ng # offline DPAPI-NG plugin
|
||||
python3 -m pip install impacket # offline SYSTEM/SECURITY/SAM hive plugin
|
||||
```
|
||||
|
||||
No live Windows host, domain controller, network connection, or LSASS access is
|
||||
required. Registry secrets and master keys must already have been collected.
|
||||
|
||||
## What it covers
|
||||
|
||||
- **Master keys** — user and SYSTEM, unlocked by password, NT hash, local SHA1
|
||||
hash, recovered prekey/credential key, DPAPI_SYSTEM, or an AD domain backup key.
|
||||
- **Classic DPAPI blobs** and PowerShell `ConvertFrom-SecureString` /
|
||||
`Export-Clixml` SecureStrings.
|
||||
- **Credential Manager**, **Windows Vault** (`.vpol`/`.vcrd`), and **CREDHIST**
|
||||
password history.
|
||||
- **Wi-Fi** personal and enterprise/PEAP profiles.
|
||||
- **Remote Desktop** `.rdp` files and **RDCMan** `.rdg`/`.settings`.
|
||||
- **CAPI/CNG** private keys and certificates, with optional PKCS#12/PFX bundling.
|
||||
- **KeePass** `ProtectedUserKey.bin`, **SCCM** policy secrets, **Outlook** IMAP,
|
||||
software-backed **Windows Hello / NGC** keys, and **Chromium** `os_crypt` keys.
|
||||
- **Crackable-hash export** for offline password recovery — `$DPAPImk$` master
|
||||
keys (Hashcat 15300/15310/15900/15910), `$MSONLINEACCOUNT$` CacheData
|
||||
(mode 33700), and local SAM NT hashes. See [docs/hashes.md](docs/hashes.md).
|
||||
- **DPAPI-NG** (optional plugin) for offline SID-descriptor blobs, from a
|
||||
supplied KDS root key.
|
||||
- **Recursive batch mode** and a **local web UI** that share the same core.
|
||||
|
||||
## Identify your key material
|
||||
|
||||
The unlock method depends on what you already have. This maps it to the CLI
|
||||
option; the same choices appear in the web UI under **2. Unlock key**.
|
||||
|
||||
| What you have | Size / form | Option |
|
||||
|---|---:|---|
|
||||
| Encrypted Windows master key | GUID-named file under `Protect` | `--masterkey FILE` plus an unlocking method |
|
||||
| Decrypted master key | 64 bytes | `--real-masterkey FILE_OR_HEX_OR_BASE64` |
|
||||
| SHA1 mapping of a decrypted master key | 20 bytes | `--real-masterkey FILE_OR_HEX_OR_BASE64` |
|
||||
| DPAPI_SYSTEM secret | 40-byte `MachineKey \|\| UserKey`, one 20-byte key, or 44 bytes with version | `--dpapi-system FILE_OR_HEX_OR_TEXT` |
|
||||
| Final SID-bound user prekey | 20 bytes | `--prekey FILE_OR_HEX` |
|
||||
| NT hash | 16 bytes | `--nt-hash FILE_OR_HEX --sid SID` |
|
||||
| Local SHA1 password hash | 20-byte `SHA1(password as UTF-16LE)` | `--sha1-hash FILE_OR_HEX --sid SID` |
|
||||
| Unbound credential key | 16–128 bytes | `--credkey FILE_OR_HEX --sid SID` |
|
||||
| AD DPAPI backup material | PEM, DER, PVK, CAPI PRIVATEKEYBLOB, or 256-byte ServerWrap key | `--domain-backup-key VALUE` or `--pvk VALUE` |
|
||||
|
||||
The 40/44-byte DPAPI_SYSTEM value is not a decrypted master key. Use it to
|
||||
decrypt a GUID-named SYSTEM master key, then use the resulting 64-byte master key
|
||||
to decrypt the artifact.
|
||||
|
||||
## Local web UI
|
||||
|
||||
The web UI is a thin front end over the same offline core, so its results match
|
||||
the command line exactly:
|
||||
|
||||
```bash
|
||||
python3 dpapi_web.py # http://127.0.0.1:8765/
|
||||
```
|
||||
|
||||
It binds to `127.0.0.1` only, has no LAN/public bind option, holds decrypted
|
||||
results in bounded memory for a single one-time download, and never writes
|
||||
persistent decrypted output. See [docs/web-ui.md](docs/web-ui.md) for the full
|
||||
workflow, the per-format flows, and the security posture.
|
||||
|
||||
## Documentation
|
||||
|
||||
| Guide | Contents |
|
||||
|---|---|
|
||||
| [docs/cli-reference.md](docs/cli-reference.md) | Inspection, master-key unlocking, output options, entropy, Hashcat export, batch mode, default collection locations, troubleshooting. |
|
||||
| [docs/artifacts.md](docs/artifacts.md) | Per-format command examples for every supported artifact. |
|
||||
| [docs/web-ui.md](docs/web-ui.md) | The local web UI: complete workflow, core artifact flows, chained recovery, and its security model. |
|
||||
| [docs/plugins.md](docs/plugins.md) | Removable offline plugins (Certificate/PFX, CacheData, Windows hives, DPAPI-NG) and the DPAPI-NG walkthrough. |
|
||||
| [docs/hashes.md](docs/hashes.md) | Getting crackable hashes for offline password recovery — `$DPAPImk$`, `$MSONLINEACCOUNT$`, and SAM NT hashes, with the Hashcat commands. |
|
||||
|
||||
Quick help is also built in: `python3 dpapi_toolkit.py -h` for short help, `-hh`
|
||||
for detailed key/artifact/location/limitation help, and appending `--structure`
|
||||
to any artifact prints its parsed fields without decrypting.
|
||||
|
||||
## Layout
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `dpapi_toolkit.py` | Core parsing/decryption plus the CLI. Also importable as a module. |
|
||||
| `dpapi_web.py` | Local, dependency-free web UI (standard-library HTTP server). |
|
||||
| `dpapi_plugins.py` | Manifest discovery and lazy loading for the offline plugins. |
|
||||
| `plugins/certificate_pfx/` | Certificate/private-key matching and offline PKCS#12/PFX creation. |
|
||||
| `plugins/cachedata/` | Entra ID/CloudAP `CacheData` known-password decoder and Hashcat mode-33700 exporter. |
|
||||
| `plugins/dpapi_ng/` | DPAPI-NG SID-descriptor decryption from a locally supplied KDS root key. |
|
||||
| `plugins/windows_hives/` | Offline DPAPI_SYSTEM extraction and optional local SAM hash export. |
|
||||
|
||||
## Scope and limitations
|
||||
|
||||
The toolkit is intentionally offline: no outbound HTTP, RPC, domain-controller,
|
||||
BKRP, LSASS, or live-registry access, and no password/PIN brute-forcing. Beyond
|
||||
that:
|
||||
|
||||
- Classic DPAPI is fully supported. The optional DPAPI-NG plugin handles offline
|
||||
SID-descriptor blobs when the matching KDS root key is supplied; other
|
||||
protection descriptors are not supported, and it never falls back to RPC.
|
||||
- Collected `SYSTEM`/`SECURITY` hives can be processed by the `windows_hives`
|
||||
plugin, or an already-extracted DPAPI_SYSTEM value supplied directly.
|
||||
- Entra `CacheData` can be decrypted with one supplied known password, or
|
||||
exported as a mode-33700 verifier for external recovery; the toolkit tests no
|
||||
candidates. Other CloudAP/LSASS sources still require a supplied prekey or
|
||||
credential key.
|
||||
- Only the primary encrypted section of a master-key file is tried; its local
|
||||
secondary BackupKey section is not yet used as a fallback.
|
||||
- Encrypted PVK and PEM backup keys require an explicitly supplied password.
|
||||
- TPM-bound Windows Hello material cannot be recovered offline from copied files.
|
||||
The NGC/PIN node chain is not yet implemented; only software CNG PIN keys are.
|
||||
- CNG DSA V2 (>1024-bit) and some uncommon legacy provider blobs are not yet
|
||||
converted to PEM; their decrypted bytes are preserved.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This is an independent implementation, but it stands on a large body of public
|
||||
research and tooling on Windows DPAPI internals. Thanks to the projects and
|
||||
authors whose published work made the offline algorithms here possible:
|
||||
|
||||
- [mimikatz](https://github.com/gentilkiwi/mimikatz) by Benjamin Delpy
|
||||
(`@gentilkiwi`) — the reference for the DPAPI master-key, blob, CREDHIST,
|
||||
Vault, and credential formats and their key derivations.
|
||||
- [impacket](https://github.com/fortra/impacket) by Fortra (originally Alberto
|
||||
Solino, `@agsolino`) — its `dpapi.py`/`secretsdump` logic and registry-hive
|
||||
classes; also the optional dependency used by the SYSTEM/SECURITY/SAM hive
|
||||
plugin.
|
||||
- [SharpDPAPI](https://github.com/GhostPack/SharpDPAPI) by Will Schroeder
|
||||
(`@harmj0y`) and GhostPack — cross-checks for many artifact formats and the
|
||||
Credential/Vault/CAPI/CNG paths.
|
||||
- [DPAPImk2john](https://github.com/openwall/john) and the
|
||||
[hashcat](https://hashcat.net/) team — the `$DPAPImk$` hash format and modes
|
||||
15300/15310/15900/15910 that the Hashcat export targets.
|
||||
- [dpapi-ng](https://github.com/jborean93/dpapi-ng) by Jordan Borean
|
||||
(`@jborean93`) — the optional library behind the offline DPAPI-NG plugin.
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,308 @@
|
||||
# Artifact formats
|
||||
|
||||
Per-format CLI examples. All of these also work through the local web UI (see
|
||||
[web-ui.md](web-ui.md)); the masterkey-unlocking options, output selection, and
|
||||
batch behavior common to every format are documented in
|
||||
[cli-reference.md](cli-reference.md).
|
||||
|
||||
## Classic DPAPI blob and PowerShell SecureString
|
||||
|
||||
A classic blob normally starts with:
|
||||
|
||||
```text
|
||||
01000000d08c9ddf0115d1118c7a00c04fc297eb
|
||||
```
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py blob.bin --type blob --real-masterkey KEY
|
||||
python3 dpapi_toolkit.py blob.hex --type blob --real-masterkey KEY
|
||||
python3 dpapi_toolkit.py securestring.txt --type powershell --real-masterkey KEY
|
||||
```
|
||||
|
||||
Binary and hexadecimal input are both accepted, and in `auto`/`blob` mode a
|
||||
Base64-wrapped blob is unwrapped automatically before the structure is parsed,
|
||||
including the Chromium `DPAPI`-prefixed form used by browser key storage.
|
||||
|
||||
PowerShell support applies to `ConvertFrom-SecureString` output created without
|
||||
an explicit `-Key` or `-SecureKey`.
|
||||
|
||||
`Export-Clixml` credential/SecureString documents are also supported. Every
|
||||
DPAPI-backed `<SS>` value is decrypted and returned in one JSON document rather
|
||||
than silently using only the first value:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py credential.clixml --type clixml \
|
||||
--masterkey-dir Protect-SID --sid SID --password PASSWORD
|
||||
```
|
||||
|
||||
## KeePass ProtectedUserKey.bin
|
||||
|
||||
KeePass Windows-user-account key material stored in `ProtectedUserKey.bin` is
|
||||
a classic DPAPI blob. The filename is auto-detected and the clear key is saved
|
||||
as `.key`:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py ProtectedUserKey.bin --type keepass \
|
||||
--masterkey-dir Protect-SID --sid SID --password PASSWORD
|
||||
```
|
||||
|
||||
## SCCM policy secrets
|
||||
|
||||
`--type sccm` scans a collected `OBJECTS.DATA`, SQL export, or an individual
|
||||
`PolicySecret Version="1"` value for the wrapped SYSTEM-DPAPI blob. The full
|
||||
file is bounded by the normal input/upload limits and the number and size of
|
||||
candidate values are capped. Every PolicySecret is decrypted independently: a
|
||||
missing masterkey produces an error object for that value while secrets whose
|
||||
masterkeys are available remain in the JSON result.
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py OBJECTS.DATA --type sccm \
|
||||
--masterkey-dir SYSTEM-PROTECT --dpapi-system DPAPI_SYSTEM_HEX
|
||||
```
|
||||
|
||||
## Credential Manager
|
||||
|
||||
Locations:
|
||||
|
||||
```text
|
||||
%LOCALAPPDATA%\Microsoft\Credentials\*
|
||||
%APPDATA%\Microsoft\Credentials\*
|
||||
```
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py CREDENTIAL_FILE \
|
||||
--type credential \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--sid SID \
|
||||
--password PASSWORD
|
||||
```
|
||||
|
||||
Output JSON includes target, username, credential, persistence, timestamp, and
|
||||
attributes when the plaintext schema is recognized.
|
||||
|
||||
## Windows Vault
|
||||
|
||||
Locations:
|
||||
|
||||
```text
|
||||
%LOCALAPPDATA%\Microsoft\Vault\<VAULT-GUID>\Policy.vpol
|
||||
%LOCALAPPDATA%\Microsoft\Vault\<VAULT-GUID>\*.vcrd
|
||||
%SYSTEMROOT%\System32\config\systemprofile\AppData\Local\Microsoft\Vault
|
||||
```
|
||||
|
||||
Decrypt `Policy.vpol` and extract its AES keys:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py Policy.vpol \
|
||||
--type vpol \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--sid SID \
|
||||
--password PASSWORD
|
||||
```
|
||||
|
||||
Decrypt a record using its policy directly:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py RECORD.vcrd \
|
||||
--type vcrd \
|
||||
--vault-policy Policy.vpol \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--sid SID \
|
||||
--password PASSWORD
|
||||
```
|
||||
|
||||
Or provide an extracted Vault AES key/JSON:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py RECORD.vcrd --type vcrd --vault-key AES_KEY_HEX
|
||||
python3 dpapi_toolkit.py RECORD.vcrd --type vcrd --vault-key POLICY_OUTPUT.json
|
||||
```
|
||||
|
||||
Batch mode decrypts `Policy.vpol` first and applies its keys to `.vcrd` files in
|
||||
the same Vault directory.
|
||||
|
||||
## CAPI, CNG, and public certificates
|
||||
|
||||
Locations:
|
||||
|
||||
```text
|
||||
CAPI: %APPDATA%\Microsoft\Crypto\RSA\<SID>\*
|
||||
CNG: %APPDATA%\Microsoft\Crypto\Keys\*
|
||||
Cert: %APPDATA%\Microsoft\SystemCertificates\My\Certificates\<THUMBPRINT>
|
||||
```
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py CAPI_FILE --type capi --real-masterkey KEY
|
||||
python3 dpapi_toolkit.py CNG_FILE --type cng --real-masterkey KEY
|
||||
python3 dpapi_toolkit.py CERT_FILE --type cert
|
||||
```
|
||||
|
||||
Recognized private keys are converted to PKCS#8 PEM: CAPI/CNG RSA, CAPI DSS2,
|
||||
CNG DSA (legacy 512–1024-bit blob), and CNG ECDH/ECDSA P-256, P-384, and P-521.
|
||||
Unknown or newer Windows key structures remain available as raw decrypted
|
||||
bytes. Public certificate files are converted to PEM-encoded `.crt` files.
|
||||
|
||||
To correlate a certificate with a decrypted key and build a PKCS#12/PFX, add
|
||||
the certificate and an explicit PFX password. Public keys are compared before
|
||||
the bundle is created, so a mismatched certificate is rejected:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py CNG_FILE --type cng --real-masterkey KEY \
|
||||
--certificate CERT_FILE --pfx-password 'new PFX password'
|
||||
```
|
||||
|
||||
Use `--pfx-password ''` only when an intentionally unencrypted PFX is required.
|
||||
In the web UI this workflow is under **3. Plugins → Certificate / PFX bundle**.
|
||||
Its main input accepts a ready PEM/DER private key, an encrypted CAPI/CNG key,
|
||||
or a folder of keys; encrypted keys also use the masterkey material in
|
||||
**2. Unlock key**. The certificate input accepts one certificate or a folder of
|
||||
them (for example a copied `SystemCertificates\My\Certificates` directory). Each
|
||||
recovered key is matched to a certificate by SHA-256 SPKI, and each match is
|
||||
reported by its SHA-1 thumbprint (the store filename) and bundled into its own
|
||||
PFX.
|
||||
|
||||
## Personal Wi-Fi profiles
|
||||
|
||||
Location:
|
||||
|
||||
```text
|
||||
%ProgramData%\Microsoft\Wlansvc\Profiles\Interfaces\<INTERFACE-GUID>\*.xml
|
||||
```
|
||||
|
||||
Wi-Fi `keyMaterial` is normally SYSTEM DPAPI:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py profile.xml \
|
||||
--type wifi \
|
||||
--masterkey SYSTEM-MASTERKEY-GUID \
|
||||
--dpapi-system DPAPI_SYSTEM_HEX
|
||||
```
|
||||
|
||||
Or use an already-decrypted SYSTEM masterkey:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py profile.xml --type wifi --real-masterkey SYSTEM_KEY
|
||||
```
|
||||
|
||||
## Enterprise Wi-Fi / PEAP
|
||||
|
||||
Export `MSMUserData` from:
|
||||
|
||||
```text
|
||||
HKCU\Software\Microsoft\Wlansvc\UserData\Profiles\<PROFILE-GUID>\MSMUserData
|
||||
```
|
||||
|
||||
The outer layer is SYSTEM DPAPI and the nested password is user DPAPI:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py MSMUserData.bin \
|
||||
--type wifi-peap \
|
||||
--system-masterkey SYSTEM-MASTERKEY-GUID \
|
||||
--dpapi-system DPAPI_SYSTEM_HEX \
|
||||
--masterkey USER-MASTERKEY-GUID \
|
||||
--sid USER_SID \
|
||||
--password USER_PASSWORD
|
||||
```
|
||||
|
||||
`--system-masterkey` accepts an encrypted or already-decrypted SYSTEM masterkey
|
||||
as a raw file or hex. Use `--real-masterkey` for the nested user layer when its
|
||||
masterkey is already decrypted.
|
||||
|
||||
## Outlook IMAP
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py NTUSER.DAT \
|
||||
--type outlook \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--sid SID \
|
||||
--password PASSWORD
|
||||
```
|
||||
|
||||
Direct hive parsing requires `python-registry`. Alternatively export the binary
|
||||
`IMAP Password` registry value and supply it instead of `NTUSER.DAT`.
|
||||
|
||||
## Saved Remote Desktop `.rdp` files
|
||||
|
||||
Standard `.rdp` files may contain a DPAPI-protected line:
|
||||
|
||||
```text
|
||||
password 51:b:<hexadecimal DPAPI blob>
|
||||
```
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py connection.rdp \
|
||||
--type rdp \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--sid SID \
|
||||
--password PASSWORD
|
||||
```
|
||||
|
||||
UTF-8, UTF-16LE, and BOM-marked files are supported. Output JSON includes the
|
||||
address, username, domain, gateway, password field name, and decrypted value.
|
||||
|
||||
## Remote Desktop Connection Manager
|
||||
|
||||
RDCMan files normally use `.rdg` and store Base64 DPAPI credential profiles.
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py sessions.rdg \
|
||||
--type rdcman \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--sid SID \
|
||||
--password PASSWORD
|
||||
```
|
||||
|
||||
## Windows Hello / NGC software keys
|
||||
|
||||
Collect these offline:
|
||||
|
||||
```text
|
||||
%WINDIR%\ServiceProfiles\LocalService\AppData\Local\Microsoft\Ngc
|
||||
%WINDIR%\ServiceProfiles\LocalService\AppData\Roaming\Microsoft\Crypto\Keys
|
||||
%WINDIR%\ServiceProfiles\LocalService\AppData\Local\Microsoft\Vault
|
||||
SYSTEM, SECURITY, and SOFTWARE hives
|
||||
```
|
||||
|
||||
Inspect/decrypt a software-backed NGC CNG key:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py NGC_CNG_KEY \
|
||||
--type ngc-cng \
|
||||
--masterkey SYSTEM-MASTERKEY-GUID \
|
||||
--dpapi-system DPAPI_SYSTEM_HEX \
|
||||
--pin PIN
|
||||
```
|
||||
|
||||
Only the software CNG private-key/PIN stage is implemented. Microsoft Platform
|
||||
Crypto Provider keys are TPM-bound and cannot normally be decrypted from copied
|
||||
files. The full `15.dat` + secondary key + NgcPin Vault/registry password chain
|
||||
is not implemented. Supply one known PIN with `--pin`; PIN Hashcat export and
|
||||
brute-force functionality are intentionally disabled.
|
||||
|
||||
## Chromium Local State (browser os_crypt key)
|
||||
|
||||
Chromium browsers (Chrome, Edge, Brave) store the AES-256-GCM key that protects
|
||||
`v10`/`v11` cookies and saved logins in the `Local State` file, under
|
||||
`os_crypt.encrypted_key`. That value is Base64 of the ASCII prefix `DPAPI`
|
||||
followed by a classic user-DPAPI blob:
|
||||
|
||||
```text
|
||||
%LOCALAPPDATA%\Google\Chrome\User Data\Local State
|
||||
%LOCALAPPDATA%\Microsoft\Edge\User Data\Local State
|
||||
```
|
||||
|
||||
Drop the whole `Local State` file, the raw `os_crypt.encrypted_key` string, or
|
||||
the already-decoded `DPAPI`-prefixed value, and add the owning user's masterkey:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py "Local State" --type localstate \
|
||||
--masterkey MASTERKEY-GUID --sid S-1-5-21-... --password PASSWORD
|
||||
|
||||
python3 dpapi_toolkit.py "Local State" --type localstate --real-masterkey KEY
|
||||
```
|
||||
|
||||
`auto` also recognizes the file by name and by content. The result is JSON with
|
||||
the recovered `os_crypt_key_hex` (normally the 32-byte AES-256-GCM key), which
|
||||
then decrypts the browser's cookie and login databases. This recovers the DPAPI
|
||||
key only; it does not read the SQLite databases, and app-bound encryption
|
||||
(newer Chrome, not a plain DPAPI key) is out of scope.
|
||||
@@ -0,0 +1,532 @@
|
||||
# CLI reference
|
||||
|
||||
The toolkit logic lives in the importable module `dpapi_toolkit.py`, which is
|
||||
also the CLI. For per-format command examples (Vault, Wi-Fi, RDP, Chromium, and
|
||||
the rest) see [artifacts.md](artifacts.md); for the removable plugins and the
|
||||
DPAPI-NG walkthrough see [plugins.md](plugins.md).
|
||||
|
||||
## Help and inspection
|
||||
|
||||
Short command help:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py -h
|
||||
```
|
||||
|
||||
Detailed key, artifact, location, and limitation help:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py -hh
|
||||
```
|
||||
|
||||
Inspect a supported artifact without providing a key:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py artifact.bin
|
||||
```
|
||||
|
||||
The tool prints every embedded classic DPAPI blob and its required masterkey
|
||||
GUID. No output file is created during inspection.
|
||||
|
||||
Use `-` as the input to read one artifact from standard input. Binary and
|
||||
textual hexadecimal input are both accepted:
|
||||
|
||||
```bash
|
||||
cat artifact.bin | python3 dpapi_toolkit.py - --type blob --real-masterkey KEY
|
||||
printf '%s' '01000000...' | python3 dpapi_toolkit.py - --structure
|
||||
```
|
||||
|
||||
Standard input is a single-artifact CLI flow; folder batch mode and web drag and
|
||||
drop use local files instead.
|
||||
|
||||
Print the full parsed structure of a blob or an encrypted master-key file
|
||||
without decrypting anything:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py artifact.bin --structure
|
||||
```
|
||||
|
||||
`--structure` lists each blob's identity and scope (version, provider/master-key
|
||||
GUIDs, flags, description), cryptography (cipher and hash algorithms), and binary
|
||||
fields (salt, HMAC, ciphertext, signature). For an encrypted master-key file it
|
||||
also reports the PBKDF2 iteration count and the Hashcat mode it maps to. This is
|
||||
the same breakdown the GUIs show in their Structure view.
|
||||
|
||||
## Automatic masterkey-directory lookup
|
||||
|
||||
`--masterkey-dir` works for both one artifact and recursive batch mode. The
|
||||
tool reads the GUID embedded in each DPAPI blob, searches the directory
|
||||
recursively, and decrypts only the matching GUID-named masterkey.
|
||||
|
||||
With a password:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB \
|
||||
--masterkey-dir PROTECT-SID \
|
||||
--sid S-1-5-21-... \
|
||||
--password PASSWORD
|
||||
```
|
||||
|
||||
With DPAPI_SYSTEM:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py SYSTEM_BLOB \
|
||||
--masterkey-dir SYSTEM_PROTECT_DIRECTORY \
|
||||
--dpapi-system DPAPI_SYSTEM_HEX
|
||||
```
|
||||
|
||||
With recovered user material:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB --masterkey-dir PROTECT-SID --prekey PREKEY
|
||||
python3 dpapi_toolkit.py BLOB --masterkey-dir PROTECT-SID --sid SID --nt-hash HASH
|
||||
python3 dpapi_toolkit.py BLOB --masterkey-dir PROTECT-SID --sid SID --sha1-hash HASH
|
||||
python3 dpapi_toolkit.py BLOB --masterkey-dir PROTECT-SID --sid SID --credkey KEY
|
||||
```
|
||||
|
||||
With the AD domain backup key:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB \
|
||||
--masterkey-dir PROTECT-SID \
|
||||
--domain-backup-key BACKUPKEY.pvk
|
||||
```
|
||||
|
||||
The directory may also contain already-decrypted 20-byte mappings or 64-byte
|
||||
masterkeys. For automatic matching, those raw files must be named exactly with
|
||||
their masterkey GUID. `--real-masterkey KEY` remains the direct fallback when
|
||||
you already know that one clear key applies, so it does not need directory
|
||||
lookup.
|
||||
|
||||
Files containing multiple DPAPI blobs may reference different GUIDs. Each blob
|
||||
is resolved and decrypted separately. If only some matching masterkeys are
|
||||
available, those values are still returned and each unresolved value is listed
|
||||
in the activity log and, for structured JSON formats, in an `errors` entry. The
|
||||
run fails only when none of its independent values can be decrypted. CREDHIST is
|
||||
the exception: it returns the recovered prefix, but cannot continue past a
|
||||
missing link because every older entry depends on the preceding one. Optional
|
||||
entropy works normally with directory lookup:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB \
|
||||
--masterkey-dir PROTECT-SID \
|
||||
--sid SID --password PASSWORD \
|
||||
--entropy-file entropy.bin
|
||||
```
|
||||
|
||||
## Output behavior
|
||||
|
||||
Outputs are never overwritten. A run timestamp is prepended to every output:
|
||||
|
||||
```text
|
||||
20260831_142501_secret_dec.bin
|
||||
```
|
||||
|
||||
If that name already exists, `_2`, `_3`, and so on are appended.
|
||||
`--out-file NAME` selects the base output name but keeps the timestamp prefix.
|
||||
|
||||
Batch runs create a separate directory:
|
||||
|
||||
```text
|
||||
20260831_142501_ARTIFACTS_dec/
|
||||
```
|
||||
|
||||
With `--out-dir RESULTS`, the timestamped run directory is created below
|
||||
`RESULTS`. For a single artifact, `--out-dir RESULTS` places the timestamped
|
||||
output file directly in `RESULTS`. Every batch run also writes
|
||||
`batch_report.json`.
|
||||
|
||||
Choose the output representation with `-o` / `--out`:
|
||||
|
||||
```bash
|
||||
-o auto # automatic text/binary handling; default
|
||||
-o hex # one continuous hexadecimal text line, saved as .hex
|
||||
-o raw # exact decrypted bytes, saved as .bin
|
||||
-o unhex # ASCII/UTF-16 hex text -> binary .bin
|
||||
-o utf16-utf8 # UTF-16LE -> UTF-8 .txt
|
||||
--show # also display the result in the terminal
|
||||
```
|
||||
|
||||
`--output-format` is a long alias for `-o/--out`. `--hex`, `--raw`, `--unhex`,
|
||||
and `--utf16-utf8` remain available as convenience aliases. `--console` is an
|
||||
alias for `--show`. Use `--out-file FILE` when a custom base filename is needed.
|
||||
When `--out-dir` and `--out-file` are combined, the directory comes from
|
||||
`--out-dir` and the base filename comes from `--out-file`.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py blob.bin --real-masterkey KEY -o hex --show
|
||||
python3 dpapi_toolkit.py blob.bin --real-masterkey KEY -o raw --console
|
||||
python3 dpapi_toolkit.py blob.bin --real-masterkey KEY -o unhex
|
||||
python3 dpapi_toolkit.py blob.bin --real-masterkey KEY -o utf16-utf8 --show
|
||||
```
|
||||
|
||||
Structured formats such as Credentials, Vault, Wi-Fi, RDP, Outlook, and RDCMan
|
||||
are normally saved as JSON. RSA private keys are normally converted to PEM.
|
||||
|
||||
## Unlocking user masterkeys
|
||||
|
||||
Default location:
|
||||
|
||||
```text
|
||||
%APPDATA%\Microsoft\Protect\<USER-SID>\<MASTERKEY-GUID>
|
||||
```
|
||||
|
||||
### Current account password
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--sid S-1-5-21-... \
|
||||
--password 'password'
|
||||
```
|
||||
|
||||
Omit `--password` to receive a non-echoing prompt. For an empty password, pass
|
||||
`--password ''` explicitly. Password mode automatically tries local, classic
|
||||
domain, and newer domain derivation and accepts only a verified masterkey.
|
||||
|
||||
### NT hash
|
||||
|
||||
A 16-byte NT hash is a valid input for classic domain-style and Protected Users
|
||||
masterkey derivation when the owning SID is also known. It is not interchangeable
|
||||
with the 20-byte local SHA1 password hash; the toolkit tries only the derivations
|
||||
appropriate to the supplied value and accepts a result only after masterkey HMAC
|
||||
verification.
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--sid S-1-5-21-... \
|
||||
--nt-hash NTHASH
|
||||
```
|
||||
|
||||
### Local SHA1 password hash
|
||||
|
||||
This input is exactly the 20-byte digest
|
||||
`SHA1(password.encode("utf-16le"))`. It is a classic local-DPAPI password
|
||||
credential key, not an Entra `dpapi_prekey`. The toolkit combines it with the
|
||||
owning SID to derive and verify the masterkey prekey.
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--sid S-1-5-21-... \
|
||||
--sha1-hash SHA1
|
||||
```
|
||||
|
||||
### Final prekey recovered from another offline source
|
||||
|
||||
This is already the final 20-byte SID-bound key used to unlock an encrypted user
|
||||
masterkey. It is not a PIN and is not applied directly to a DPAPI blob.
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--prekey PREKEY_HEX
|
||||
```
|
||||
|
||||
### Unbound credential key
|
||||
|
||||
This is commonly recovered from CloudAP/CacheData. It still needs the owning
|
||||
account SID so the toolkit can derive the masterkey prekey; it is not itself a
|
||||
Windows Hello PIN input.
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--sid S-1-5-21-... \
|
||||
--credkey CREDENTIAL_KEY_HEX
|
||||
```
|
||||
|
||||
### Already-decrypted masterkey
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB --real-masterkey MASTERKEY_HEX
|
||||
python3 dpapi_toolkit.py BLOB --real-masterkey MASTERKEY_BASE64
|
||||
python3 dpapi_toolkit.py BLOB --real-masterkey masterkey.bin
|
||||
```
|
||||
|
||||
`--real-masterkey` accepts the full 64-byte masterkey or its 20-byte SHA1
|
||||
mapping. It bypasses password, SID, DPAPI_SYSTEM, and domain backup processing.
|
||||
|
||||
## SID interpretation: local, on-prem AD, and Entra ID
|
||||
|
||||
The tool validates and classifies a supplied SID before attempting decryption:
|
||||
|
||||
- `S-1-12-1-...` is treated as an Entra ID/cloud-account SID. The classic
|
||||
on-prem AD DPAPI domain backup key is rejected for this SID. Use recovered
|
||||
CloudAP `--prekey`/`--credkey`, an already-decrypted masterkey, or the software
|
||||
NGC path where applicable.
|
||||
- `S-1-5-21-...` may be either a local account or an on-prem AD account. The SID
|
||||
alone cannot distinguish them; successful masterkey HMAC verification selects
|
||||
the usable password derivation.
|
||||
- Built-in and service SIDs are identified separately when verbose output is
|
||||
enabled.
|
||||
|
||||
An Entra SID does not by itself prove that the account uses a PIN, nor whether
|
||||
the Windows Hello key is software-backed or TPM-backed. That requires the NGC
|
||||
protector/provider and CNG key metadata. `--pin` is accepted only with
|
||||
`--type ngc-cng`. If the profile is Hello/TPM-only, the classic DPAPI masterkey
|
||||
cannot be cracked from its SID and masterkey file: neither the PIN verifier nor
|
||||
the TPM private key is stored in the `$DPAPImk$` record. Such a record can only
|
||||
test password/credential-derived DPAPI candidates, when those exist. Software
|
||||
NGC PIN testing is a separate `--type ngc-cng` workflow.
|
||||
|
||||
## SYSTEM and service-account DPAPI
|
||||
|
||||
Common masterkey locations:
|
||||
|
||||
```text
|
||||
%WINDIR%\System32\Microsoft\Protect\S-1-5-18\User\<MASTERKEY-GUID>
|
||||
%WINDIR%\System32\Microsoft\Protect\S-1-5-18\<MASTERKEY-GUID>
|
||||
```
|
||||
|
||||
The DPAPI_SYSTEM LSA secret must be extracted offline from both the `SYSTEM` and
|
||||
`SECURITY` hives. Accepted forms are:
|
||||
|
||||
- Full 44 bytes: version + 20-byte MachineKey + 20-byte UserKey.
|
||||
- 40 bytes: MachineKey + UserKey.
|
||||
- One 16/20-byte component.
|
||||
- Text containing `MachineKey:` and `UserKey:` values.
|
||||
|
||||
Decrypt an artifact in one command:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py SYSTEM_BLOB \
|
||||
--masterkey SYSTEM-MASTERKEY-GUID \
|
||||
--dpapi-system DPAPI_SYSTEM_HEX
|
||||
```
|
||||
|
||||
Decrypt only the SYSTEM masterkey first:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py SYSTEM-MASTERKEY-GUID \
|
||||
--type masterkey \
|
||||
--dpapi-system DPAPI_SYSTEM_HEX
|
||||
```
|
||||
|
||||
The resulting timestamped `.bin` can then be supplied with `--real-masterkey`.
|
||||
|
||||
## AD domain backup key
|
||||
|
||||
Domain user masterkey files may contain a DomainKey section protected with the
|
||||
domain DPAPI backup RSA key.
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py MASTERKEY-GUID \
|
||||
--type masterkey \
|
||||
--domain-backup-key BACKUPKEY.pvk
|
||||
```
|
||||
|
||||
The alias `--pvk` is accepted. PEM, DER, Windows PVK, raw CAPI
|
||||
PRIVATEKEYBLOB, hex, and Base64 inputs are supported. For encrypted PVK or PEM
|
||||
files, use `--pvk-password TEXT` or `--pvk-password-file FILE`. Strong and
|
||||
legacy weak-key Microsoft PVK encryption are both recognized; no password
|
||||
guessing is performed.
|
||||
|
||||
Legacy version-1 masterkey DomainKey sections can instead be recovered with the
|
||||
collected 256-byte `G$BCKUPKEY_<GUID>` ServerWrap key (the `.key` exported by
|
||||
common AD backup-key tooling). The MS-BKRP HMAC and SID are validated locally;
|
||||
no BKRP RPC request is made. A full `P_BACKUP_KEY` value containing the leading
|
||||
version dword is accepted as well.
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py MASTERKEY-GUID --type masterkey \
|
||||
--pvk BACKUPKEY.pvk --pvk-password-file pvk-password.txt
|
||||
```
|
||||
|
||||
## Optional entropy
|
||||
|
||||
If the application supplied optional entropy to `CryptProtectData`, the same
|
||||
bytes are required during decryption.
|
||||
|
||||
Literal UTF-8 text:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB --real-masterkey KEY --entropy 'application text'
|
||||
```
|
||||
|
||||
Hex, Base64, or UTF-16LE text:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB --real-masterkey KEY --entropy hex:01020304
|
||||
python3 dpapi_toolkit.py BLOB --real-masterkey KEY --entropy base64:AQIDBA==
|
||||
python3 dpapi_toolkit.py BLOB --real-masterkey KEY --entropy utf16:Secret
|
||||
```
|
||||
|
||||
Exact bytes from a file:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py BLOB --real-masterkey KEY --entropy-file entropy.bin
|
||||
```
|
||||
|
||||
Explicit entropy overrides built-in entropy for that run. Without an explicit
|
||||
value, known CAPI/CNG entropy is selected automatically.
|
||||
|
||||
## CREDHIST password history
|
||||
|
||||
Default location:
|
||||
|
||||
```text
|
||||
%APPDATA%\Microsoft\Protect\<USER-SID>\CREDHIST
|
||||
```
|
||||
|
||||
CREDHIST is a chain of older SHA1 and NT password hashes encrypted using the
|
||||
newer credential. Start with the current password or current key material:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py CREDHIST --type credhist --password CURRENT_PASSWORD
|
||||
python3 dpapi_toolkit.py CREDHIST --type credhist --nt-hash CURRENT_NT_HASH
|
||||
python3 dpapi_toolkit.py CREDHIST --type credhist --sha1-hash CURRENT_SHA1
|
||||
python3 dpapi_toolkit.py CREDHIST --type credhist --prekey CURRENT_PREKEY
|
||||
python3 dpapi_toolkit.py CREDHIST --type credhist --credkey CURRENT_CREDKEY
|
||||
```
|
||||
|
||||
The SID is stored in each CREDHIST entry, so `--sid` is not required. Output is
|
||||
JSON containing each history GUID, SID, SHA1 hash, and NT hash. Those recovered
|
||||
hashes can then decrypt masterkeys protected before the password changed.
|
||||
|
||||
## Hashcat masterkey export
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py MASTERKEY-GUID --hashcat --sid S-1-5-21-...
|
||||
```
|
||||
|
||||
`--hashcat` always treats the input as an encrypted master-key file, so
|
||||
`--type` is not required. Add `--type masterkey` when you want to be explicit:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py MASTERKEY-GUID --type masterkey --hashcat --sid S-1-5-21-...
|
||||
```
|
||||
|
||||
The Hashcat mode comes from the masterkey's cipher/hash, and the derivation
|
||||
context comes from the account type:
|
||||
|
||||
| Masterkey algorithms | Local account | Domain account (legacy) | Domain account (2016+) |
|
||||
|---|---:|---:|---:|
|
||||
| 3DES/SHA1 | 15300 | 15300 | 15310 |
|
||||
| AES-256/SHA-512 | 15900 | 15900 | 15910 |
|
||||
|
||||
In short: a **local** account masterkey cracks as **15900** (or 15300), and a
|
||||
**domain** account masterkey is usually **15910** (or 15310) on 2016-and-later
|
||||
domain controllers, or 15900/15300 on older ones. The mode alone is not enough:
|
||||
the `$DPAPImk$` record also embeds a context number (1 local, 2 domain, 3
|
||||
domain-new), and Hashcat derives the key differently for each, so a local
|
||||
masterkey only cracks with the local (context 1) record even though it is also
|
||||
mode 15900.
|
||||
|
||||
The masterkey file does not reliably reveal which account type it belongs to, so
|
||||
the default `--hashcat-context all` exports the local, domain, and domain-new
|
||||
records; crack whichever matches. Use `local`, `domain`, `domain-new`, or
|
||||
`domain-auto` (both domain forms) to narrow it.
|
||||
|
||||
To export every master key under a directory at once, add `--batch`. Each master
|
||||
key becomes its own record (they may have different passwords and are cracked
|
||||
independently), the owning SID is read from a `Protect\<SID>` parent directory
|
||||
when present or from `--sid` otherwise, and the records are grouped into one file
|
||||
per mode:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py PROTECT-DIR --batch --hashcat --sid S-1-5-21-...
|
||||
```
|
||||
|
||||
## Recursive batch mode
|
||||
|
||||
One encrypted masterkey for a directory:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py ARTIFACTS \
|
||||
--batch \
|
||||
--masterkey MASTERKEY-GUID \
|
||||
--sid SID \
|
||||
--password PASSWORD
|
||||
```
|
||||
|
||||
One already-decrypted masterkey:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py ARTIFACTS --batch --real-masterkey KEY
|
||||
```
|
||||
|
||||
A Protect folder of masterkeys matched to blobs by GUID:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py ARTIFACTS \
|
||||
--batch \
|
||||
--masterkey-dir PROTECT-SID \
|
||||
--sid SID \
|
||||
--password PASSWORD
|
||||
```
|
||||
|
||||
Domain masterkeys with an AD backup key:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py ARTIFACTS \
|
||||
--batch \
|
||||
--masterkey-dir PROTECT-SID \
|
||||
--domain-backup-key BACKUPKEY.pvk
|
||||
```
|
||||
|
||||
SYSTEM masterkeys:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py ARTIFACTS \
|
||||
--batch \
|
||||
--masterkey-dir SYSTEM_PROTECT_DIR \
|
||||
--dpapi-system DPAPI_SYSTEM_HEX
|
||||
```
|
||||
|
||||
Batch mode recursively recognizes certificates, CREDHIST, Wi-Fi XML, `.rdp`,
|
||||
RDCMan, Vault policies/records, Credentials, CAPI/CNG, and generic embedded
|
||||
classic DPAPI blobs. First-class CLIXML, KeePass, SCCM, and removable-plugin
|
||||
dispatch currently use single-artifact mode. Masterkeys are cached by GUID. A
|
||||
`--real-masterkey` has no GUID and is tried as a fallback. Within one recognized
|
||||
multi-value file, successful values are kept when other GUIDs are unavailable;
|
||||
the batch report marks that artifact as `partial` and records per-item errors.
|
||||
|
||||
## Default collection locations
|
||||
|
||||
| Artifact | Default location |
|
||||
|---|---|
|
||||
| User masterkeys | `%APPDATA%\Microsoft\Protect\<SID>\*` |
|
||||
| SYSTEM masterkeys | `%WINDIR%\System32\Microsoft\Protect\S-1-5-18\*` |
|
||||
| CREDHIST | `%APPDATA%\Microsoft\Protect\<SID>\CREDHIST` |
|
||||
| Local Credentials | `%LOCALAPPDATA%\Microsoft\Credentials\*` |
|
||||
| Roaming Credentials | `%APPDATA%\Microsoft\Credentials\*` |
|
||||
| User Vault | `%LOCALAPPDATA%\Microsoft\Vault\*` |
|
||||
| SYSTEM Vault | `%WINDIR%\System32\config\systemprofile\AppData\Local\Microsoft\Vault` |
|
||||
| CAPI keys | `%APPDATA%\Microsoft\Crypto\RSA\<SID>\*` |
|
||||
| CNG keys | `%APPDATA%\Microsoft\Crypto\Keys\*` |
|
||||
| Public certificates | `%APPDATA%\Microsoft\SystemCertificates\My\Certificates\*` |
|
||||
| Wi-Fi profiles | `%ProgramData%\Microsoft\Wlansvc\Profiles\Interfaces\*\*.xml` |
|
||||
| Outlook profiles | `NTUSER.DAT`, under `Software\Microsoft\Office\...\Outlook\Profiles` |
|
||||
| NGC metadata | `%WINDIR%\ServiceProfiles\LocalService\AppData\Local\Microsoft\Ngc` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
`required master key <GUID>`
|
||||
|
||||
: Find that exact GUID-named file under the appropriate user or SYSTEM Protect
|
||||
directory, or supply its already-decrypted key with `--real-masterkey`.
|
||||
|
||||
`invalid padding` or `signature verification failed`
|
||||
|
||||
: The masterkey or optional entropy is wrong. DPAPI plaintext is accepted only
|
||||
after cryptographic verification.
|
||||
|
||||
`could not decrypt master key`
|
||||
|
||||
: Check the owning SID and unlocking material. A password changed by an
|
||||
administrator may require CREDHIST or the AD domain backup key.
|
||||
|
||||
`no classic DPAPI blob found`
|
||||
|
||||
: Force the correct `--type`, verify that the file was exported as binary, or
|
||||
confirm that the application actually uses classic DPAPI rather than DPAPI-NG
|
||||
or custom encryption.
|
||||
|
||||
`output path exists`
|
||||
|
||||
: Normal output selection avoids collisions automatically. This error would
|
||||
indicate a race with another process; rerun to receive a new timestamp/suffix.
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
# Getting crackable hashes (offline password recovery)
|
||||
|
||||
The toolkit never cracks passwords itself — it runs no wordlists, generates no
|
||||
candidates, and launches no Hashcat. Instead it exports hashes and verifiers in
|
||||
standard [Hashcat](https://hashcat.net/) formats so you can run an authorized
|
||||
offline recovery in a separate process. Three sources produce crackable material,
|
||||
and CREDHIST yields historical hashes as a byproduct.
|
||||
|
||||
| Source | Flag | Hashcat mode | Token | Recovers |
|
||||
|---|---|---:|---|---|
|
||||
| Windows master key | `--hashcat` | 15300 / 15310 / 15900 / 15910 | `$DPAPImk$` | user logon password |
|
||||
| Entra ID / Microsoft-account CacheData | `--cachedata-hashcat` | 33700 | `$MSONLINEACCOUNT$` | cloud-account password |
|
||||
| Local SAM | `--plugin windows_hives --sam-hive SAM` | 1000 | `username:RID:LM:NT:::` | local-account password (or pass-the-hash) |
|
||||
|
||||
Recovered passwords/hashes then unlock the master key that protects the actual
|
||||
artifact — close the loop with the normal decrypt flow in
|
||||
[cli-reference.md](cli-reference.md) and [artifacts.md](artifacts.md).
|
||||
|
||||
## 1. Master keys → `$DPAPImk$`
|
||||
|
||||
Export a `$DPAPImk$` record from one encrypted master key (the owning SID is
|
||||
required):
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py MASTERKEY-GUID --hashcat --sid S-1-5-21-...
|
||||
```
|
||||
|
||||
Every master key under a directory at once (each becomes its own record, grouped
|
||||
one file per mode):
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py PROTECT-DIR --batch --hashcat --sid S-1-5-21-...
|
||||
```
|
||||
|
||||
The mode comes from the master key's cipher/hash, and the record embeds a
|
||||
context number (1 local, 2 domain, 3 domain-new) that Hashcat derives
|
||||
differently — so the file does not reveal the account type and the default
|
||||
`--hashcat-context all` exports every form; crack whichever matches:
|
||||
|
||||
```bash
|
||||
hashcat -m 15900 FILE.hc15900 WORDLIST # AES-256/SHA-512 master key
|
||||
hashcat -m 15300 FILE.hc15300 WORDLIST # legacy 3DES/SHA1 master key
|
||||
```
|
||||
|
||||
Full mode/context table and the `local` / `domain` / `domain-new` / `domain-auto`
|
||||
selectors are in [cli-reference.md → Hashcat masterkey export](cli-reference.md#hashcat-masterkey-export).
|
||||
A cracked password feeds straight back in as `--password` to unlock the key.
|
||||
|
||||
## 2. Entra ID / Microsoft-account CacheData → `$MSONLINEACCOUNT$`
|
||||
|
||||
Export a mode-33700 verifier from a collected `CacheData` file. The toolkit
|
||||
validates the file and emits the verifier only; it tests no candidates:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py CacheData --plugin cachedata --cachedata-hashcat
|
||||
```
|
||||
|
||||
Then recover the password in a separate local Hashcat process:
|
||||
|
||||
```bash
|
||||
hashcat -m 33700 FILE.hc33700 WORDLIST
|
||||
```
|
||||
|
||||
Feed the recovered password back into the same plugin to derive the DPAPI
|
||||
prekey and credential key, which unlock the Entra user's master key:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py CacheData --plugin cachedata --password 'recovered password'
|
||||
```
|
||||
|
||||
See [plugins.md → Entra ID CacheData](plugins.md#entra-id-cachedata) and the
|
||||
chained walkthrough in [web-ui.md](web-ui.md#chained-recovery-flows).
|
||||
|
||||
## 3. Local accounts → NT hashes from SAM
|
||||
|
||||
Extract local-account hashes offline from collected `SYSTEM` + `SECURITY` +
|
||||
`SAM` hives:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py SYSTEM --plugin windows_hives \
|
||||
--security-hive SECURITY --sam-hive SAM
|
||||
```
|
||||
|
||||
The `.sam` result uses the standard `username:RID:LM:NT:::` format. Crack the NT
|
||||
hash, or use it directly for pass-the-hash / as a master-key unlock via
|
||||
`--nt-hash`:
|
||||
|
||||
```bash
|
||||
hashcat -m 1000 nt_hashes.txt WORDLIST
|
||||
```
|
||||
|
||||
Details in [plugins.md → Windows SYSTEM/SECURITY/SAM hives](plugins.md#windows-systemsecuritysam-hives).
|
||||
|
||||
## Byproduct: historical hashes from CREDHIST
|
||||
|
||||
Decrypting `CREDHIST` with the current password/key recovers every older SHA1
|
||||
and NT hash in the chain. Those unlock master keys created before a password
|
||||
change, and each NT hash is also crackable with `hashcat -m 1000`:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py CREDHIST --type credhist --password CURRENT_PASSWORD
|
||||
```
|
||||
|
||||
See [cli-reference.md → CREDHIST password history](cli-reference.md#credhist-password-history).
|
||||
|
||||
## In the web UI
|
||||
|
||||
Drop an encrypted master key (or the `Protect` folder) in **1. Artifact**, enter
|
||||
the owning SID in **2. Unlock key**, choose the Hashcat context, and click
|
||||
**Masterkey → Hashcat**; each mode comes back as its own download. The CacheData
|
||||
verifier is exported from **3. Plugins → Windows Entra ID CacheData** with
|
||||
**Export Hashcat mode 33700**. All exports are hashes/verifiers only — the
|
||||
toolkit performs no guessing anywhere.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
+140
@@ -0,0 +1,140 @@
|
||||
# Removable offline application plugins
|
||||
|
||||
Plugins are loaded only when selected (or when their narrow filename rule
|
||||
matches) from the toolkit-local `plugins` directory. They are ordinary local
|
||||
Python code and are therefore trusted at the same level as the toolkit itself;
|
||||
remove a plugin directory to remove the feature. Plugin manifests cannot point
|
||||
outside their own directory, and the toolkit never downloads plugins. Each
|
||||
manifest may declare its own text, password, and file controls for the web
|
||||
interface. The web interface always exposes **3. Plugins**; selecting a plugin
|
||||
there reveals only that plugin's controls, names its required main artifact,
|
||||
and states whether **2. Unlock key** is also needed. **1. Artifact** is not used
|
||||
for plugin runs. Removing a plugin removes its choice and panel.
|
||||
|
||||
All application plugins are entirely offline. They do not contact a vendor
|
||||
service or a Windows host.
|
||||
|
||||
## Certificate / PFX bundle
|
||||
|
||||
The Certificate / PFX plugin bundles a PEM/DER private key, or decrypts a
|
||||
CAPI/CNG key first, then matches it to a certificate by public key. Both the key
|
||||
input and the certificate input may be a single file or a folder, so a directory
|
||||
of keys can be matched against a copied `SystemCertificates\My\Certificates`
|
||||
folder in one run; each match is reported by the certificate SHA1 thumbprint:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py private-key.pem --plugin certificate_pfx \
|
||||
--certificate certificate.cer --pfx-password 'new PFX password'
|
||||
|
||||
python3 dpapi_toolkit.py CNG_FILE --plugin certificate_pfx \
|
||||
--real-masterkey KEY --certificate CERT_FILE \
|
||||
--pfx-password 'new PFX password'
|
||||
|
||||
python3 dpapi_toolkit.py CRYPTO_KEYS_DIR --plugin certificate_pfx \
|
||||
--masterkey-dir PROTECT-SID --sid SID --password PASSWORD \
|
||||
--certificate MY_CERTIFICATES_DIR --pfx-password 'new PFX password'
|
||||
```
|
||||
|
||||
## Entra ID CacheData
|
||||
|
||||
The CacheData plugin supports a password node when the exact password is known.
|
||||
It validates the file checksum, applies the fixed PBKDF2/AES flow, and returns
|
||||
the PRT JSON, raw DPAPI credential key, and derived SID-bound prekey. It can also
|
||||
export a bounded Hashcat mode-33700 verifier for a separate authorized recovery
|
||||
process. When a file contains several password nodes, each is attempted and
|
||||
every node matching the supplied password is returned even if sibling nodes do
|
||||
not decrypt. The toolkit itself accepts no wordlist, generates no candidates,
|
||||
and does not launch Hashcat:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py CacheData --plugin cachedata --password 'known password'
|
||||
python3 dpapi_toolkit.py CacheData --plugin cachedata --cachedata-hashcat
|
||||
```
|
||||
|
||||
PIN/NGC CacheData nodes remain separate because they require the collected NGC
|
||||
and CNG key chain; TPM-backed keys cannot be reconstructed from copied files.
|
||||
|
||||
## Windows SYSTEM/SECURITY/SAM hives
|
||||
|
||||
The Windows-hives plugin derives the boot key from a collected `SYSTEM` hive.
|
||||
Add a collected `SECURITY` hive to recover the 20-byte DPAPI_SYSTEM MachineKey
|
||||
and UserKey, and optionally add `SAM` to export local account hashes. It does
|
||||
not use Remote Registry, RPC, SMB, cached-domain-logon extraction, password
|
||||
history, or general LSA-secret output. Pass the hives explicitly, or point the
|
||||
main input at a folder that holds them (each hive is matched by name):
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py SYSTEM --plugin windows_hives \
|
||||
--security-hive SECURITY --sam-hive SAM
|
||||
|
||||
python3 dpapi_toolkit.py HIVES_DIR --plugin windows_hives
|
||||
```
|
||||
|
||||
The raw `.dpapi_system` result can be supplied directly to a later command with
|
||||
`--dpapi-system`. The `.sam` result uses the standard
|
||||
`username:RID:LM:NT:::` representation and is written with owner-only
|
||||
permissions like every other decrypted secret.
|
||||
|
||||
## DPAPI-NG (offline root key)
|
||||
|
||||
The DPAPI-NG plugin accepts an exported KDS root key as JSON and deliberately
|
||||
uses only its preloaded in-process key cache. It supports the SID protection
|
||||
descriptor implemented by the optional `dpapi-ng` package and refuses a cache
|
||||
miss instead of allowing DNS/RPC discovery:
|
||||
|
||||
```bash
|
||||
python3 dpapi_toolkit.py secret.dpapi-ng --plugin dpapi_ng \
|
||||
--dpapi-ng-root-key root-key.json
|
||||
```
|
||||
|
||||
### Walkthrough
|
||||
|
||||
DPAPI-NG (the `NCryptProtectSecret`/`NCryptUnprotectSecret` API) protects data to
|
||||
a protection descriptor, usually a SID or group, rather than to one account
|
||||
password. Its keys derive from a domain-wide **KDS root key** stored on the
|
||||
domain controllers. This plugin needs that root key supplied explicitly; it never
|
||||
contacts a domain controller.
|
||||
|
||||
1. **Export the KDS root key (needs Domain Admin or SYSTEM).** The root keys live
|
||||
in the configuration partition at
|
||||
`CN=Master Root Keys,CN=Group Key Distribution Service,CN=Services,CN=Configuration,DC=...`.
|
||||
The `msKds-RootKeyData` attribute is confidential and only Domain Admins/SYSTEM
|
||||
can read it. Read the objects with a privileged LDAP query (for example
|
||||
`Get-ADObject -SearchBase 'CN=Master Root Keys,...' -Filter * -Properties *`),
|
||||
or recover them offline from a collected `NTDS.dit`.
|
||||
|
||||
2. **Write `root-key.json`.** The file is one object or a list of objects. Each
|
||||
object needs `RootKeyId` (the object `cn` GUID) and Base64 `RootKeyData` (the
|
||||
64-byte `msKds-RootKeyData`). The remaining fields are optional and fall back to
|
||||
the Windows defaults shown here when omitted:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"RootKeyId": "d9a2b0a1-1111-2222-3333-444455556666",
|
||||
"RootKeyData": "<base64 of msKds-RootKeyData>",
|
||||
"Version": 1,
|
||||
"KdfAlgorithm": "SP800_108_CTR_HMAC",
|
||||
"KdfParameters": "<base64 of msKds-KDFParam, optional>",
|
||||
"SecretAgreementAlgorithm": "DH",
|
||||
"SecretAgreementParameters": "<base64 of msKds-SecretAgreementParam, optional>",
|
||||
"PrivateKeyLength": 512,
|
||||
"PublicKeyLength": 2048
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The attribute-to-field mapping is `cn` -> `RootKeyId`, `msKds-RootKeyData` ->
|
||||
`RootKeyData`, `msKds-Version` -> `Version`, `msKds-KDFAlgorithmID` ->
|
||||
`KdfAlgorithm`, `msKds-KDFParam` -> `KdfParameters`,
|
||||
`msKds-SecretAgreementAlgorithmID` -> `SecretAgreementAlgorithm`,
|
||||
`msKds-SecretAgreementParam` -> `SecretAgreementParameters`,
|
||||
`msKds-PrivateKeyLength` -> `PrivateKeyLength`, and `msKds-PublicKeyLength` ->
|
||||
`PublicKeyLength`. Only KDF `SP800_108_CTR_HMAC` and secret-agreement `DH`,
|
||||
`ECDH_P256`, `ECDH_P384`, or `ECDH_P521` are supported.
|
||||
|
||||
3. **Decrypt.** On the CLI, run the command above. In the web UI, open
|
||||
**3. Plugins**, choose **DPAPI-NG (offline root key)**, drop the DPAPI-NG
|
||||
artifact as the main input and `root-key.json` in its field, then **Run
|
||||
plugin**. `1. Artifact` and `2. Unlock key` are not used. If no supplied root
|
||||
key matches the blob, the plugin stops rather than falling back to the network.
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
# Local web UI
|
||||
|
||||
The web UI is a thin front end over the same offline core as the CLI, so its
|
||||
results match the command line exactly. Start it with:
|
||||
|
||||
```bash
|
||||
python3 dpapi_web.py # http://127.0.0.1:8765/
|
||||
python3 dpapi_web.py --port 9000 --no-open
|
||||
```
|
||||
|
||||
It uses an inspect-first flow: drop an artifact and it shows the parsed
|
||||
**Structure** (identity, cryptography, and binary fields) plus the required
|
||||
master-key GUID, then drop or type the key material and decrypt. Decrypted
|
||||
results appear under the **Decrypted data** tab with per-value copy buttons.
|
||||
Drop an encrypted master-key file and it is auto-detected: enter the SID and
|
||||
click **Masterkey → Hashcat** to export a `$DPAPImk$` hash, or add unlock
|
||||
material and decrypt it. A dropped folder (or several files) switches to batch
|
||||
mode and the results come back as a single zip download. Specialized decoders
|
||||
live under **3. Plugins**, where both the plugin and its artifact are selected;
|
||||
the main Artifact tab is kept for core masterkeys, DPAPI blobs, and Windows
|
||||
artifact formats. Optional entropy and Vault controls are under **2. Unlock
|
||||
key** in a collapsed section, so the normal workflow stays compact.
|
||||
|
||||
## Security posture of the local server
|
||||
|
||||
No web-framework dependency: it runs a standard-library HTTP server bound to
|
||||
`127.0.0.1` (loopback only) at the site root. Mutating requests require a
|
||||
per-run token embedded in the served page, cross-origin/host checks are
|
||||
enforced, uploads are bounded, and downloads expire after ten minutes and are
|
||||
single-use. The web API does not accept hosting-server input/output paths and
|
||||
never writes persistent decrypted output: uploaded material is placed in a
|
||||
private per-request temporary directory (memory-backed `/dev/shm` on Linux when
|
||||
available), removed after the request on both success and failure, and results
|
||||
are held only in bounded memory until their one-time browser download or expiry.
|
||||
This is browser-request hardening, not an isolation boundary against a hostile
|
||||
process running as the same operating-system user. Decrypted secrets pass
|
||||
through this local process; use **Reset & wipe** or stop it when finished.
|
||||
|
||||
The server binds to `127.0.0.1` only; the only options are `--port` and
|
||||
`--no-open`, and there is no way to bind a LAN or public interface. Each analyst
|
||||
runs a separate copy and opens it from the same computer. Its request token
|
||||
prevents cross-origin mutations; it is not user authentication and the server is
|
||||
not a multi-user service.
|
||||
|
||||
Unlike the CLI, the local web interface deliberately has no `--out-dir`,
|
||||
`--out-file`, server-side autosave, or typed server-path behavior. A folder run
|
||||
is assembled below the request's temporary directory, zipped into memory, and
|
||||
returned to the browser. Persistent copies exist only when the browser user
|
||||
downloads or copies a result.
|
||||
|
||||
## Complete workflow
|
||||
|
||||
The web interface follows the same sequence for every core artifact:
|
||||
|
||||
1. In **1. Artifact**, drop the artifact, paste its hexadecimal form, or drop a
|
||||
folder for batch processing. Leave **Type** on `auto` when the format is
|
||||
recognized, or select the exact type from the tables below.
|
||||
2. Click **Inspect** first. **Structure** shows the detected format, embedded
|
||||
DPAPI blobs, cryptographic fields, and required masterkey GUIDs. Inspection
|
||||
does not need a key and does not create decrypted output.
|
||||
3. In **2. Unlock key**, add either an already-decrypted masterkey or the
|
||||
encrypted masterkey/Protect directory plus one applicable unlocking method.
|
||||
4. Open **Optional entropy / Vault** only when the source application used
|
||||
extra entropy or the input is a Vault record.
|
||||
5. Choose the output representation and click **Decrypt**. Read or copy values
|
||||
under **Decrypted data**, or use the one-time download button for a file.
|
||||
6. Click **Reset & wipe** when finished. It clears the browser fields and
|
||||
expires pending in-memory downloads; stopping the local process clears the
|
||||
remaining download store.
|
||||
|
||||
Plugins use **3. Plugins** instead of **1. Artifact**. Select the plugin, drop
|
||||
its named main input in that tab, fill the controls it reveals, and click
|
||||
**Run plugin**. Use **2. Unlock key** only when the selected plugin says that a
|
||||
classic-DPAPI masterkey is also required.
|
||||
|
||||
## Choose one masterkey path
|
||||
|
||||
These paths unlock the masterkey required by the artifact. Key material unlocks
|
||||
an encrypted masterkey; it is not applied directly to the artifact.
|
||||
|
||||
| What you have | **2. Unlock key** flow |
|
||||
|---|---|
|
||||
| Decrypted masterkey | Drop the 64-byte masterkey or its 20-byte SHA1 mapping under **Decrypted masterkey**, or paste its hexadecimal/Base64 value. No SID or password is needed. |
|
||||
| One encrypted user masterkey and password | Drop **Encrypted masterkey**, enter its owning SID and password, then decrypt the artifact. Select **Empty password** only when the password really is empty. |
|
||||
| Protect directory and password | Drop the **Protect dir of masterkeys**, enter the owning SID and password, and decrypt. Every artifact blob is matched to its GUID-named masterkey. |
|
||||
| Domain/Protected Users NT hash | Drop the encrypted masterkey or Protect directory, enter the owning `S-1-5-21-...` SID and the 16-byte NT hash. |
|
||||
| Local SHA1 password hash | Drop the encrypted masterkey or Protect directory, enter the owning SID and the 20-byte `SHA1(password encoded as UTF-16LE)`. |
|
||||
| Recovered Entra prekey | Drop the encrypted user masterkey or Protect directory, enter its `S-1-12-1-...` Entra SID, and paste the 20-byte recovered prekey. The field appears only for an Entra SID in a masterkey workflow. |
|
||||
| Recovered Entra credential key | Drop the encrypted user masterkey or Protect directory, enter its `S-1-12-1-...` Entra SID, and paste the recovered credential key. The toolkit derives the SID-bound prekey. |
|
||||
| DPAPI_SYSTEM | Drop the encrypted SYSTEM masterkey or SYSTEM Protect directory, then drop or paste DPAPI_SYSTEM. Do not put DPAPI_SYSTEM in the decrypted-masterkey field. |
|
||||
| AD domain backup key | Drop the encrypted domain masterkey or Protect directory and add the AD backup key. If the PVK/PEM file is encrypted, also enter or load its file password. |
|
||||
|
||||
If an application used optional entropy, expand **Optional entropy / Vault** and
|
||||
enter it as text, `hex:`, `base64:`, or `utf16:`, or drop the exact entropy
|
||||
file. Explicit entropy replaces any built-in application entropy for that run.
|
||||
|
||||
## Core artifact flows
|
||||
|
||||
Every value offered by the web **Type** selector is covered here.
|
||||
|
||||
| Type | **1. Artifact** input | Additional flow | Result |
|
||||
|---|---|---|---|
|
||||
| `auto` | Any supported core artifact | Click **Inspect**, add the requested masterkey path when one is reported, then **Decrypt**. Use an explicit type if automatic recognition is ambiguous. | Detected format's normal result. |
|
||||
| `masterkey` | GUID-named encrypted masterkey | Add one applicable unlock method from the table above. | Decrypted 64-byte masterkey. It can be downloaded and reused through **Decrypted masterkey**. |
|
||||
| `credhist` | `CREDHIST` | Supply the current password, NT hash, or SHA1 hash. The SID is stored in the entries. The CLI additionally supports a current prekey or credential key. | JSON containing historical SIDs, SHA1 hashes, and NT hashes. Use the matching recovered hash to unlock an older masterkey. |
|
||||
| `blob` | Raw or hex classic DPAPI blob | Add its required masterkey and optional entropy. | Raw, text, or selected output format. |
|
||||
| `credential` | Credential Manager file | Add the owning user masterkey. | JSON with target, username, credential, persistence, timestamp, and attributes. |
|
||||
| `capi` | Encrypted CAPI private-key file | Add the owning masterkey; normal CAPI entropy is selected automatically. | PKCS#8 PEM when the private-key structure is recognized, otherwise raw decrypted bytes. |
|
||||
| `cng` | Encrypted CNG private-key file | Add the owning masterkey; normal CNG entropy is selected automatically. | PKCS#8 PEM when recognized, otherwise raw decrypted bytes. |
|
||||
| `cert` | Serialized Windows, DER, or PEM public certificate | No masterkey is needed. | PEM-encoded `.crt`. Use the Certificate/PFX plugin when a matching private key should be bundled. |
|
||||
| `vpol` | Vault `Policy.vpol` | Add the user or SYSTEM masterkey required by the policy blob. | JSON containing the Vault AES keys. |
|
||||
| `vcrd` | Vault `.vcrd` record | Expand **Optional entropy / Vault** and either drop its `Policy.vpol` plus that policy's masterkey, paste a Vault AES key, or drop the JSON produced from `Policy.vpol`. | Decrypted Vault record JSON. |
|
||||
| `powershell` | `ConvertFrom-SecureString` DPAPI hex | Paste or drop it, then add the owning user masterkey. This does not apply to values created with PowerShell `-Key` or `-SecureKey`. | UTF-8 plaintext. |
|
||||
| `clixml` | PowerShell `Export-Clixml` document | Add the masterkey or Protect directory for every embedded SecureString. | One JSON document containing the username and every decrypted SecureString. |
|
||||
| `keepass` | `ProtectedUserKey.bin` | Add its owning user masterkey. | Clear KeePass user-account key as `.key`. |
|
||||
| `sccm` | `OBJECTS.DATA`, SQL export, or one `PolicySecret Version="1"` value | Add the matching SYSTEM masterkey and DPAPI_SYSTEM, or an already-decrypted SYSTEM masterkey. | JSON containing all recovered policy secrets. |
|
||||
| `wifi` | Personal Wi-Fi profile XML | Add the required SYSTEM masterkey plus DPAPI_SYSTEM, or a decrypted SYSTEM masterkey. | JSON containing profile details and key material. An enterprise profile points to the separate `wifi-peap` flow. |
|
||||
| `wifi-peap` | Exported `MSMUserData` binary | Add the outer SYSTEM masterkey in **SYSTEM masterkey (PEAP only)** plus DPAPI_SYSTEM. Also add the nested user masterkey/password path using the normal masterkey controls. | JSON containing PEAP identity and password. |
|
||||
| `outlook` | `NTUSER.DAT` or exported binary `IMAP Password` value | `NTUSER.DAT` needs optional `python-registry`. Add the owning user masterkey. | JSON containing supported Outlook IMAP accounts and passwords. |
|
||||
| `rdp` | Saved `.rdp` file containing `password 51:b:` | Add the owning user masterkey. | JSON containing connection metadata and decrypted password fields. |
|
||||
| `rdcman` | RDCMan `.rdg` or `.settings` file | Add the masterkey or Protect directory for its credential profiles. | JSON containing every recovered credential profile. |
|
||||
| `ngc-cng` | Software-backed Windows Hello CNG key | Add the encrypted SYSTEM masterkey and DPAPI_SYSTEM, then enter the one known PIN shown for this type. TPM-backed keys and PIN guessing are not supported. | Decrypted Windows Hello private key, or NGC metadata when the file is inspectable but not a supported software-PIN key. |
|
||||
| `localstate` | Chromium `Local State` file, its `os_crypt.encrypted_key` value, or the `DPAPI`-prefixed key | Add the owning user masterkey. Base64 and the `DPAPI` prefix are handled automatically. | JSON with the recovered `os_crypt_key_hex` (the AES-256-GCM key for browser cookies/logins). |
|
||||
|
||||
## Chained recovery flows
|
||||
|
||||
Some jobs produce key material for a later job. Keep every stage local and use
|
||||
the downloaded or copied result only in the next stage that names it.
|
||||
|
||||
### Offline hives to a SYSTEM-protected artifact
|
||||
|
||||
1. In **3. Plugins**, choose **Windows SYSTEM/SECURITY/SAM hives**.
|
||||
2. Drop `SYSTEM` and `SECURITY` together into the one plugin input (or the folder
|
||||
that holds them); add `SAM` too only when local account hash export is also
|
||||
required, then **Run plugin**. Each hive is matched by name.
|
||||
3. Download the `.dpapi_system` result, or click **Use as DPAPI_SYSTEM** on it to
|
||||
load it straight into **2. Unlock key**.
|
||||
4. In **1. Artifact**, drop the SYSTEM-protected artifact. In **2. Unlock key**,
|
||||
add its encrypted SYSTEM masterkey or SYSTEM Protect directory and the
|
||||
downloaded DPAPI_SYSTEM value, then **Decrypt**.
|
||||
|
||||
### CacheData to an Entra user artifact
|
||||
|
||||
The usual Entra location is
|
||||
`%WINDIR%\System32\config\systemprofile\AppData\Local\Microsoft\Windows\CloudAPCache\AzureAD\<unique_hash>\Cache\CacheData`.
|
||||
Microsoft-account entries use `MicrosoftAccount` instead of `AzureAD`.
|
||||
Reading this system-profile location normally requires a process started with
|
||||
**Run as administrator**: membership in the local Administrators group is not
|
||||
enough when the process still has a filtered, medium-integrity UAC token. A
|
||||
full/high-integrity administrator token is normally sufficient; SYSTEM is an
|
||||
alternative if the actual host ACL or collection method requires it. Do not
|
||||
change ownership or ACLs on evidence merely to make collection easier.
|
||||
|
||||
1. Collect that `CacheData` file with an authorized elevated or offline
|
||||
evidence-collection method. In **3. Plugins**, choose **Windows Entra ID
|
||||
CacheData**, drop the file, then choose one route:
|
||||
- enter the one known password and **Run plugin**; or
|
||||
- select **Export Hashcat mode 33700**, click **Run plugin**, download the
|
||||
`.hc33700` verifier, and test authorized candidates in a separate local
|
||||
Hashcat process with `hashcat -m 33700 FILE.hc33700 WORDLIST`.
|
||||
2. If external recovery finds the password, clear the export checkbox, enter
|
||||
that password in the CacheData plugin, and run it again. The JSON result
|
||||
contains the Entra SID and derived `dpapi_prekey_hex`; the
|
||||
separate `.credkey` result contains the raw credential key.
|
||||
3. In **1. Artifact**, drop the target artifact. In **2. Unlock key**, drop its
|
||||
encrypted Entra masterkey or Protect directory and enter the recovered
|
||||
`S-1-12-1-...` SID.
|
||||
4. Paste either the derived prekey or the credential key, not both, and click
|
||||
**Decrypt**. These recovered keys unlock the masterkey, not the artifact and
|
||||
not a Windows Hello PIN.
|
||||
|
||||
### CREDHIST to an artifact protected before a password change
|
||||
|
||||
1. Decrypt `CREDHIST` with the current password/hash using the `credhist` flow.
|
||||
2. Locate the entry whose GUID/SID corresponds to the older masterkey and copy
|
||||
its recovered SHA1 or NT hash.
|
||||
3. Drop the older artifact in **1. Artifact** and its older encrypted masterkey
|
||||
in **2. Unlock key**. Enter the entry's SID and matching recovered hash, then
|
||||
**Decrypt**.
|
||||
|
||||
### Vault policy to Vault records
|
||||
|
||||
1. Decrypt `Policy.vpol` with type `vpol` and its DPAPI masterkey.
|
||||
2. For one record, drop the `.vcrd`, select `vcrd`, and load the policy output
|
||||
JSON or paste one extracted AES key under **Optional entropy / Vault**.
|
||||
3. Alternatively, drop the `.vcrd`, add the original `Policy.vpol` there, and
|
||||
leave its DPAPI masterkey material in **2. Unlock key** so both stages run
|
||||
together.
|
||||
4. For a whole Vault folder, use the batch flow; the policy is processed before
|
||||
records in the same directory.
|
||||
|
||||
### CAPI/CNG private key and certificate to PFX
|
||||
|
||||
1. In **3. Plugins**, choose **Certificate / PFX bundle**.
|
||||
2. Drop a ready PEM/DER private key, an encrypted CAPI/CNG key, or a folder of
|
||||
keys as the main plugin input, then add the matching certificate or a folder
|
||||
of certificates.
|
||||
3. For encrypted CAPI/CNG keys, use **2. Unlock key** to add their DPAPI
|
||||
masterkey material.
|
||||
4. Enter a new PFX password, or explicitly select an unencrypted PFX, then
|
||||
**Run plugin**. Each key is matched to a certificate by public key; the
|
||||
activity log reports every match by its SHA1 thumbprint (the store filename),
|
||||
and one PFX is created per match.
|
||||
|
||||
### Enterprise Wi-Fi / PEAP two-layer recovery
|
||||
|
||||
1. Export `MSMUserData`, drop it in **1. Artifact**, and select `wifi-peap`.
|
||||
2. In **2. Unlock key**, add the outer SYSTEM masterkey using **SYSTEM masterkey
|
||||
(PEAP only)** and add DPAPI_SYSTEM when that masterkey is encrypted.
|
||||
3. In the normal masterkey controls, add the nested user's encrypted masterkey
|
||||
plus SID/password material, a matching Protect directory, or an already
|
||||
decrypted user masterkey.
|
||||
4. Click **Decrypt** to recover the PEAP identity and nested password.
|
||||
|
||||
## Hashcat and batch flows
|
||||
|
||||
To export a masterkey hash, drop the encrypted masterkey in **1. Artifact**,
|
||||
select `masterkey` if needed, enter its owning SID in **2. Unlock key**, choose
|
||||
the Hashcat context, and click **Masterkey → Hashcat**. This exports only a
|
||||
`$DPAPImk$` record; it does not crack passwords or PINs. `domain-auto` exports
|
||||
both domain derivation forms because the file does not reliably identify which
|
||||
one applies.
|
||||
|
||||
For batch processing, drop a folder in **1. Artifact**, add one encrypted or
|
||||
decrypted masterkey, or a Protect directory plus its unlock material, in
|
||||
**2. Unlock key**, and click **Decrypt**. The toolkit recursively matches blobs
|
||||
to GUID-named masterkeys and returns one in-memory ZIP containing the results and
|
||||
`batch_report.json`. Batch mode recognizes certificates, CREDHIST, Wi-Fi XML,
|
||||
RDP, RDCMan, Vault policies/records, Credentials, CAPI/CNG, and generic classic
|
||||
DPAPI blobs. CLIXML, KeePass, SCCM, PEAP, Outlook, NGC, and plugins remain
|
||||
single-artifact flows.
|
||||
|
||||
## Plugin flows
|
||||
|
||||
| Plugin | **3. Plugins** flow | Uses **2. Unlock key** | Result |
|
||||
|---|---|---|---|
|
||||
| Certificate / PFX bundle | Drop a PEM/DER private key, encrypted CAPI/CNG key, or a folder of keys; add one certificate or a certificate folder and a new PFX password, then **Run plugin**. | Only for encrypted CAPI/CNG input. | Each matching decrypted/normalized private key plus its PFX; the log shows certificate SHA1 and public-key SPKI SHA256 fingerprints. |
|
||||
| Windows Entra ID CacheData | Drop `CacheData`. Enter one known password to decrypt, or select **Export Hashcat mode 33700** to create a verifier for a separate local Hashcat run. The toolkit itself performs no guessing. | No. | Decrypted PRT JSON, Entra SID, derived DPAPI prekey, and raw `.credkey`; or a `.hc33700` verifier. |
|
||||
| DPAPI-NG (offline root key) | Drop the DPAPI-NG artifact and the exported KDS root-key JSON, then **Run plugin**. Only the supplied in-process key cache is used. | No. | Decrypted SID-descriptor payload when the matching root key is present. |
|
||||
| Windows SYSTEM/SECURITY/SAM hives | Drop `SYSTEM` and `SECURITY` together (and `SAM` for local hashes), or the folder holding them, into the one plugin input. Each hive is matched by name. | No. | Raw/JSON DPAPI_SYSTEM material and optional SAM hash records. |
|
||||
@@ -0,0 +1,183 @@
|
||||
"""Small, local-only plugin loader for application-specific DPAPI layers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from fnmatch import fnmatch
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from types import ModuleType
|
||||
|
||||
|
||||
PLUGIN_ROOT = Path(__file__).with_name("plugins")
|
||||
PLUGIN_ID = re.compile(r"^[a-z][a-z0-9_-]{0,31}$")
|
||||
MAX_MANIFEST_BYTES = 64 * 1024
|
||||
WEB_FIELD = re.compile(r"^[a-z][a-z0-9_]{0,63}$")
|
||||
WEB_FIELD_KINDS = frozenset(("text", "password", "file", "checkbox"))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PluginWebField:
|
||||
name: str
|
||||
kind: str
|
||||
label: str
|
||||
placeholder: str
|
||||
help: str
|
||||
optional: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PluginManifest:
|
||||
plugin_id: str
|
||||
name: str
|
||||
description: str
|
||||
directory: Path
|
||||
file_patterns: tuple[str, ...]
|
||||
web_help: str
|
||||
web_input_label: str
|
||||
web_workflow_help: str
|
||||
web_fields: tuple[PluginWebField, ...]
|
||||
|
||||
|
||||
def discover_plugins() -> dict[str, PluginManifest]:
|
||||
root = PLUGIN_ROOT.resolve()
|
||||
manifests: dict[str, PluginManifest] = {}
|
||||
if not root.is_dir():
|
||||
return manifests
|
||||
for directory in sorted(path for path in root.iterdir() if path.is_dir()):
|
||||
manifest_path = directory / "plugin.json"
|
||||
module_path = directory / "plugin.py"
|
||||
if not manifest_path.is_file() or not module_path.is_file():
|
||||
continue
|
||||
if manifest_path.stat().st_size > MAX_MANIFEST_BYTES:
|
||||
raise ValueError(f"plugin manifest is too large: {manifest_path}")
|
||||
try:
|
||||
raw = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise ValueError(f"invalid plugin manifest {manifest_path}: {exc}") from None
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError(f"plugin manifest must be an object: {manifest_path}")
|
||||
plugin_id = raw.get("id")
|
||||
name = raw.get("name")
|
||||
description = raw.get("description", "")
|
||||
patterns = raw.get("file_patterns", [])
|
||||
web = raw.get("web", {})
|
||||
if (
|
||||
not isinstance(plugin_id, str)
|
||||
or not PLUGIN_ID.fullmatch(plugin_id)
|
||||
or plugin_id != directory.name
|
||||
):
|
||||
raise ValueError(f"plugin id must match its safe directory name: {directory}")
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
raise ValueError(f"plugin {plugin_id} has no display name")
|
||||
if not isinstance(description, str):
|
||||
raise ValueError(f"plugin {plugin_id} description must be text")
|
||||
if not isinstance(patterns, list) or not all(
|
||||
isinstance(pattern, str) and pattern and "/" not in pattern and "\\" not in pattern
|
||||
for pattern in patterns
|
||||
):
|
||||
raise ValueError(f"plugin {plugin_id} has invalid file patterns")
|
||||
if not isinstance(web, dict):
|
||||
raise ValueError(f"plugin {plugin_id} web configuration must be an object")
|
||||
web_help = web.get("help", description)
|
||||
web_input_label = web.get("input_label", "plugin artifact")
|
||||
web_workflow_help = web.get(
|
||||
"workflow_help",
|
||||
"Tab 1 is not used for plugins. Add the main input here.",
|
||||
)
|
||||
raw_web_fields = web.get("fields", [])
|
||||
if not isinstance(web_help, str):
|
||||
raise ValueError(f"plugin {plugin_id} web help must be text")
|
||||
if not isinstance(web_input_label, str) or not web_input_label.strip():
|
||||
raise ValueError(f"plugin {plugin_id} web input label must be text")
|
||||
if not isinstance(web_workflow_help, str) or not web_workflow_help.strip():
|
||||
raise ValueError(f"plugin {plugin_id} web workflow help must be text")
|
||||
if not isinstance(raw_web_fields, list) or len(raw_web_fields) > 16:
|
||||
raise ValueError(f"plugin {plugin_id} has invalid web fields")
|
||||
web_fields = []
|
||||
seen_web_fields = set()
|
||||
for field in raw_web_fields:
|
||||
if not isinstance(field, dict):
|
||||
raise ValueError(f"plugin {plugin_id} web field must be an object")
|
||||
field_name = field.get("name")
|
||||
kind = field.get("kind")
|
||||
label = field.get("label")
|
||||
placeholder = field.get("placeholder", "")
|
||||
field_help = field.get("help", "")
|
||||
optional = field.get("optional", False)
|
||||
if (
|
||||
not isinstance(field_name, str)
|
||||
or not WEB_FIELD.fullmatch(field_name)
|
||||
or field_name in seen_web_fields
|
||||
or kind not in WEB_FIELD_KINDS
|
||||
or not isinstance(label, str)
|
||||
or not label.strip()
|
||||
or not isinstance(placeholder, str)
|
||||
or not isinstance(field_help, str)
|
||||
or not isinstance(optional, bool)
|
||||
):
|
||||
raise ValueError(f"plugin {plugin_id} has an invalid web field")
|
||||
seen_web_fields.add(field_name)
|
||||
web_fields.append(PluginWebField(
|
||||
field_name,
|
||||
kind,
|
||||
label.strip(),
|
||||
placeholder,
|
||||
field_help,
|
||||
optional,
|
||||
))
|
||||
if plugin_id in manifests:
|
||||
raise ValueError(f"duplicate plugin id: {plugin_id}")
|
||||
manifests[plugin_id] = PluginManifest(
|
||||
plugin_id,
|
||||
name.strip(),
|
||||
description.strip(),
|
||||
directory.resolve(),
|
||||
tuple(patterns),
|
||||
web_help.strip(),
|
||||
web_input_label.strip(),
|
||||
web_workflow_help.strip(),
|
||||
tuple(web_fields),
|
||||
)
|
||||
return manifests
|
||||
|
||||
|
||||
def detect_plugin(filename: str) -> str | None:
|
||||
matches = [
|
||||
manifest.plugin_id
|
||||
for manifest in discover_plugins().values()
|
||||
if any(fnmatch(filename.casefold(), pattern.casefold()) for pattern in manifest.file_patterns)
|
||||
]
|
||||
return matches[0] if len(matches) == 1 else None
|
||||
|
||||
|
||||
def load_plugin(plugin_id: str) -> tuple[PluginManifest, ModuleType]:
|
||||
manifest = discover_plugins().get(plugin_id)
|
||||
if manifest is None:
|
||||
raise ValueError(f"unknown or unavailable plugin {plugin_id!r}")
|
||||
root = PLUGIN_ROOT.resolve()
|
||||
module_path = (manifest.directory / "plugin.py").resolve()
|
||||
try:
|
||||
module_path.relative_to(root)
|
||||
except ValueError:
|
||||
raise ValueError(f"plugin {plugin_id} escapes the plugin directory") from None
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
f"dpapi_toolkit_plugin_{plugin_id}", module_path
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ValueError(f"cannot load plugin {plugin_id}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
if getattr(module, "PLUGIN_ID", None) != plugin_id or not callable(
|
||||
getattr(module, "run", None)
|
||||
):
|
||||
raise ValueError(f"plugin {plugin_id} does not implement the required contract")
|
||||
return manifest, module
|
||||
|
||||
|
||||
def run_plugin(plugin_id: str, data, source_path, was_hex, args, core, emit):
|
||||
manifest, module = load_plugin(plugin_id)
|
||||
emit(f"[+] plugin: {manifest.name} ({manifest.plugin_id})")
|
||||
return module.run(data, source_path, was_hex, args, core, emit)
|
||||
+5212
File diff suppressed because it is too large
Load Diff
+1962
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "certificate_pfx",
|
||||
"name": "Certificate / PFX bundle",
|
||||
"description": "Match a certificate to a private key and create a PKCS#12/PFX bundle.",
|
||||
"file_patterns": [],
|
||||
"web": {
|
||||
"input_label": "private key, CAPI/CNG key, or a folder of them",
|
||||
"workflow_help": "Drop one PEM/DER private key, one encrypted CAPI/CNG key, or a whole folder of keys. For encrypted CAPI/CNG keys, also add their DPAPI masterkey material in 2. Unlock key. Tab 1 is not used.",
|
||||
"help": "Recovers each key (a ready PEM/DER key, including password-protected PEM/DER, or a CAPI/CNG key decrypted with 2. Unlock key material) and matches it to a certificate by public key. Drop one certificate or a folder (for example a copied SystemCertificates\\My\\Certificates directory). Each match is reported by the certificate SHA1 thumbprint and bundled into its own PFX.",
|
||||
"fields": [
|
||||
{"name": "certificate", "kind": "file", "label": "matching certificate or certs folder", "help": "PEM, DER, or serialized Windows certificate; a single file or a dropped folder of certificates."},
|
||||
{"name": "key_password", "kind": "password", "label": "Private-key password", "help": "For password-protected PEM/DER key input; one value is applied to the supplied key or key folder.", "optional": true},
|
||||
{"name": "pfx_password", "kind": "password", "label": "New PFX password", "help": "Password that will protect the generated PFX."},
|
||||
{"name": "empty_pfx_password", "kind": "checkbox", "label": "Create unencrypted PFX", "help": "Use only when an intentionally unencrypted PFX is required."},
|
||||
{"name": "pfx_password_file", "kind": "file", "label": "PFX password file", "help": "Alternative to entering the new PFX password above.", "optional": true}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Certificate/private-key correlation and offline PKCS#12 creation.
|
||||
|
||||
Both the key input and the certificate input may be a single file or a folder:
|
||||
|
||||
- Key input: one decrypted PEM/DER private key, one encrypted CAPI/CNG key, or a
|
||||
folder of them (for example a copied ``Crypto\\Keys`` directory). Encrypted
|
||||
CAPI/CNG keys are decrypted through the normal core engine using the master-key
|
||||
material in 2. Unlock key.
|
||||
- Certificate input: one certificate or a folder of them (for example a copied
|
||||
``SystemCertificates\\My\\Certificates`` directory, whose files are named by the
|
||||
certificate SHA1 thumbprint).
|
||||
|
||||
Every recovered key is matched to a certificate by SHA256(SPKI) of the public
|
||||
key. Each match is reported by the certificate SHA1 thumbprint (the store
|
||||
filename) and bundled into its own PFX.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
|
||||
|
||||
PLUGIN_ID = "certificate_pfx"
|
||||
|
||||
|
||||
def _spki_sha256(key, serialization) -> str:
|
||||
public = key.public_key() if hasattr(key, "public_key") else key
|
||||
encoded = public.public_bytes(
|
||||
serialization.Encoding.DER,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
return hashlib.sha256(encoded).hexdigest().upper()
|
||||
|
||||
|
||||
def _files_under(path: Path) -> list:
|
||||
if path.is_dir():
|
||||
found = sorted(item for item in path.rglob("*") if item.is_file())
|
||||
if not found:
|
||||
raise ValueError(f"no files found under {path.name}")
|
||||
return found
|
||||
return [path]
|
||||
|
||||
|
||||
def _load_certificates(cert_arg, core, serialization):
|
||||
"""Return {spki_sha256: (name, der_bytes, certificate)} from a file or folder."""
|
||||
path = Path(cert_arg)
|
||||
if path.is_dir():
|
||||
sources = [(item.name, item.read_bytes()) for item in _files_under(path)]
|
||||
else:
|
||||
data, cert_path, _ = core.read_data(cert_arg, "certificate")
|
||||
sources = [((cert_path.name if cert_path else "certificate"), data)]
|
||||
by_spki, errors = {}, []
|
||||
for name, data in sources:
|
||||
try:
|
||||
certificate = core.load_x509_certificate(data)
|
||||
except ValueError as exc:
|
||||
errors.append(f"{name}: {exc}")
|
||||
continue
|
||||
by_spki.setdefault(
|
||||
_spki_sha256(certificate.public_key(), serialization),
|
||||
(name, data, certificate),
|
||||
)
|
||||
if not by_spki:
|
||||
detail = f" ({'; '.join(errors)})" if errors else ""
|
||||
raise ValueError(f"no readable certificate found{detail}")
|
||||
return by_spki
|
||||
|
||||
|
||||
def _recover_key_pem(raw, path, args, core):
|
||||
"""Return a PKCS#8 PEM for one key: a ready PEM/DER key, or a CAPI/CNG decrypt."""
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
key_password = (
|
||||
args.key_password.encode("utf-8")
|
||||
if args.key_password is not None
|
||||
else None
|
||||
)
|
||||
try:
|
||||
key = core.load_private_key(raw, key_password)
|
||||
return key.private_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PrivateFormat.PKCS8,
|
||||
serialization.NoEncryption(),
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
if path is None:
|
||||
return None
|
||||
inner = copy(args)
|
||||
inner.plugin = ""
|
||||
inner.type = "auto"
|
||||
inner.input = str(path)
|
||||
inner.certificate = None
|
||||
inner.pfx_password = None
|
||||
inner.pfx_password_file = None
|
||||
inner.key_password = None
|
||||
inner.output_format = "auto"
|
||||
try:
|
||||
outputs = core.run_single(inner, emit=lambda *_: None)
|
||||
except ValueError:
|
||||
return None
|
||||
for item in outputs:
|
||||
if item.extension == ".pem":
|
||||
return item.data
|
||||
return None
|
||||
|
||||
|
||||
def _recover_keys(data, source_path, args, core, emit):
|
||||
"""Return [(name, pem_bytes)] from one key input or a folder of key inputs."""
|
||||
if source_path is not None and Path(source_path).is_dir():
|
||||
paths = _files_under(Path(source_path))
|
||||
elif source_path is not None:
|
||||
paths = [Path(source_path)]
|
||||
else:
|
||||
pem = _recover_key_pem(data, None, args, core)
|
||||
return [("input", pem)] if pem else []
|
||||
keys = []
|
||||
for path in paths:
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
except OSError as exc:
|
||||
emit(f"[!] {path.name}: cannot read ({exc})")
|
||||
continue
|
||||
pem = _recover_key_pem(raw, path, args, core)
|
||||
if pem:
|
||||
keys.append((path.name, pem))
|
||||
else:
|
||||
emit(f"[!] {path.name}: not a private key and not a decryptable CAPI/CNG key")
|
||||
return keys
|
||||
|
||||
|
||||
def run(data, source_path, _was_hex, args, core, emit):
|
||||
if not args.certificate:
|
||||
raise ValueError("certificate/PFX bundling requires a matching certificate")
|
||||
try:
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
except ImportError:
|
||||
raise ValueError("install cryptography to construct a PFX/PKCS#12 bundle") from None
|
||||
|
||||
certs_by_spki = _load_certificates(args.certificate, core, serialization)
|
||||
emit(f"[+] {len(certs_by_spki)} candidate certificate(s)")
|
||||
|
||||
keys = _recover_keys(data, source_path, args, core, emit)
|
||||
if not keys:
|
||||
raise ValueError("no private key recovered from the key input")
|
||||
emit(f"[+] {len(keys)} recovered private key(s)")
|
||||
|
||||
pfx_secret = core.pfx_password(args)
|
||||
outputs, errors = [], []
|
||||
for index, (key_name, pem) in enumerate(keys):
|
||||
try:
|
||||
key = core.load_private_key(pem)
|
||||
except ValueError as exc:
|
||||
errors.append(f"{key_name}: {exc}")
|
||||
continue
|
||||
match = certs_by_spki.get(_spki_sha256(key, serialization))
|
||||
if match is None:
|
||||
emit(f"[!] {key_name}: no certificate public key matches this key")
|
||||
errors.append(f"{key_name}: no matching certificate")
|
||||
continue
|
||||
cert_name, cert_data, certificate = match
|
||||
thumbprint = certificate.fingerprint(hashes.SHA1()).hex().upper()
|
||||
emit(f"[+] {key_name} -> {thumbprint} ({certificate.subject.rfc4514_string()})")
|
||||
emit(f"[+] certificate SHA1 thumbprint: {thumbprint}")
|
||||
emit(f"[+] certificate public-key SHA256 (SPKI): {_spki_sha256(certificate.public_key(), serialization)}")
|
||||
emit(f"[+] private-key public-key SHA256 (SPKI): {_spki_sha256(key, serialization)}")
|
||||
try:
|
||||
bundle = core.build_pkcs12_bundle(
|
||||
pem, cert_data, pfx_secret, thumbprint.encode("ascii")
|
||||
)
|
||||
except ValueError as exc:
|
||||
errors.append(f"{key_name}: {exc}")
|
||||
continue
|
||||
outputs.extend((
|
||||
core.OutputItem(
|
||||
pem, ".pem", f"private key {thumbprint}", source_path,
|
||||
index * 2, len(keys) * 2,
|
||||
),
|
||||
core.OutputItem(
|
||||
bundle, ".pfx", f"PFX {thumbprint}", source_path,
|
||||
index * 2 + 1, len(keys) * 2,
|
||||
),
|
||||
))
|
||||
|
||||
if not outputs:
|
||||
detail = errors[-1] if errors else "no key matched any certificate"
|
||||
raise ValueError(f"could not correlate any key and certificate: {detail}")
|
||||
emit(
|
||||
f"[+] matched and created {len(outputs) // 2} PFX bundle(s)"
|
||||
+ (f"; {len(errors)} key(s) unmatched" if errors else "")
|
||||
)
|
||||
return outputs
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "dpapi_ng",
|
||||
"name": "DPAPI-NG (offline root key)",
|
||||
"description": "Offline-only DPAPI-NG SID-descriptor decryption from an explicitly supplied KDS root-key JSON file.",
|
||||
"file_patterns": ["*.dpapi-ng", "*.ncrypt"],
|
||||
"web": {
|
||||
"input_label": "DPAPI-NG artifact",
|
||||
"workflow_help": "Use this tab only. Tab 1 and Unlock key are not needed; add the KDS root-key JSON below.",
|
||||
"help": "Only locally supplied KDS root keys are tried; DNS and RPC fallback are disabled.",
|
||||
"fields": [
|
||||
{"name": "dpapi_ng_root_key", "kind": "file", "label": "KDS root-key JSON", "help": "Exported root-key record or list of records."}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Strictly offline adapter for jborean93/dpapi-ng.
|
||||
|
||||
The public dpapi-ng convenience API can fall back to DNS/RPC on a cache miss.
|
||||
This adapter intentionally uses the parsed blob and preloaded cache directly,
|
||||
checks for a local key first, and never calls that network-capable API.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
|
||||
|
||||
PLUGIN_ID = "dpapi_ng"
|
||||
MAX_ROOT_KEY_JSON = 1024 * 1024
|
||||
MAX_ROOT_KEYS = 64
|
||||
MAX_PARAMETER_BYTES = 64 * 1024
|
||||
SUPPORTED_KDF_ALGORITHMS = frozenset(("SP800_108_CTR_HMAC",))
|
||||
SUPPORTED_SECRET_ALGORITHMS = frozenset(("DH", "ECDH_P256", "ECDH_P384", "ECDH_P521"))
|
||||
|
||||
|
||||
def _field(record: dict, *names, default=None):
|
||||
folded = {str(key).casefold(): value for key, value in record.items()}
|
||||
for name in names:
|
||||
if name.casefold() in folded:
|
||||
return folded[name.casefold()]
|
||||
return default
|
||||
|
||||
|
||||
def _b64(record: dict, name: str, *, optional: bool = False) -> bytes | None:
|
||||
value = _field(record, name)
|
||||
if value in (None, "") and optional:
|
||||
return None
|
||||
if not isinstance(value, str):
|
||||
raise ValueError(f"DPAPI-NG root-key field {name} must be Base64 text")
|
||||
try:
|
||||
decoded = base64.b64decode(value, validate=True)
|
||||
except (ValueError, binascii.Error):
|
||||
raise ValueError(f"DPAPI-NG root-key field {name} is invalid Base64") from None
|
||||
if len(decoded) > MAX_PARAMETER_BYTES:
|
||||
raise ValueError(f"DPAPI-NG root-key field {name} exceeds 64 KiB")
|
||||
return decoded
|
||||
|
||||
|
||||
def load_root_key_records(path_value: str) -> list[dict]:
|
||||
path = Path(path_value)
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
except OSError as exc:
|
||||
raise ValueError(f"cannot read --dpapi-ng-root-key: {exc}") from None
|
||||
if len(raw) > MAX_ROOT_KEY_JSON:
|
||||
raise ValueError("DPAPI-NG root-key JSON exceeds the 1 MiB limit")
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise ValueError(f"invalid DPAPI-NG root-key JSON: {exc}") from None
|
||||
records = parsed if isinstance(parsed, list) else [parsed]
|
||||
if not records or len(records) > MAX_ROOT_KEYS or not all(
|
||||
isinstance(record, dict) for record in records
|
||||
):
|
||||
raise ValueError("DPAPI-NG root-key JSON must contain 1 to 64 objects")
|
||||
return records
|
||||
|
||||
|
||||
def populate_cache(cache, records: list[dict]) -> None:
|
||||
for record in records:
|
||||
root_id = _field(record, "RootKeyId", "cn")
|
||||
if not isinstance(root_id, str):
|
||||
raise ValueError("DPAPI-NG root key is missing RootKeyId/cn")
|
||||
try:
|
||||
root_id = uuid.UUID(root_id)
|
||||
except ValueError:
|
||||
raise ValueError(f"invalid DPAPI-NG RootKeyId {root_id!r}") from None
|
||||
key = _b64(record, "RootKeyData")
|
||||
if not 32 <= len(key) <= 1024:
|
||||
raise ValueError("DPAPI-NG RootKeyData has an unsafe or invalid length")
|
||||
version = _field(record, "Version", default=1)
|
||||
private_length = _field(record, "PrivateKeyLength", default=512)
|
||||
public_length = _field(record, "PublicKeyLength", default=2048)
|
||||
if not all(type(value) is int for value in (version, private_length, public_length)):
|
||||
raise ValueError("DPAPI-NG key version/length fields must be integers")
|
||||
if version != 1:
|
||||
raise ValueError(f"unsupported DPAPI-NG root-key version {version}")
|
||||
if not 1 <= private_length <= 8192 or not 1 <= public_length <= 8192:
|
||||
raise ValueError("DPAPI-NG key lengths must be between 1 and 8192 bits")
|
||||
kdf_algorithm = _field(record, "KdfAlgorithm", default="SP800_108_CTR_HMAC")
|
||||
secret_algorithm = _field(record, "SecretAgreementAlgorithm", default="DH")
|
||||
if not isinstance(kdf_algorithm, str) or not isinstance(secret_algorithm, str):
|
||||
raise ValueError("DPAPI-NG algorithm fields must be text")
|
||||
if kdf_algorithm not in SUPPORTED_KDF_ALGORITHMS:
|
||||
raise ValueError(f"unsupported DPAPI-NG KDF algorithm {kdf_algorithm!r}")
|
||||
if secret_algorithm not in SUPPORTED_SECRET_ALGORITHMS:
|
||||
raise ValueError(
|
||||
f"unsupported DPAPI-NG secret-agreement algorithm {secret_algorithm!r}"
|
||||
)
|
||||
cache.load_key(
|
||||
key,
|
||||
root_id,
|
||||
version=version,
|
||||
kdf_algorithm=kdf_algorithm,
|
||||
kdf_parameters=_b64(record, "KdfParameters", optional=True),
|
||||
secret_algorithm=secret_algorithm,
|
||||
secret_parameters=_b64(record, "SecretAgreementParameters", optional=True),
|
||||
private_key_length=private_length,
|
||||
public_key_length=public_length,
|
||||
)
|
||||
|
||||
|
||||
def run(data, source_path, was_hex, args, core, emit):
|
||||
if not args.dpapi_ng_root_key:
|
||||
raise ValueError("offline DPAPI-NG requires --dpapi-ng-root-key ROOT_KEY.json")
|
||||
try:
|
||||
from dpapi_ng import KeyCache
|
||||
from dpapi_ng._blob import DPAPINGBlob
|
||||
from dpapi_ng._client import _decrypt_blob
|
||||
except ImportError:
|
||||
raise ValueError(
|
||||
"install the optional dpapi-ng package to use the offline DPAPI-NG plugin"
|
||||
) from None
|
||||
cache = KeyCache()
|
||||
populate_cache(cache, load_root_key_records(args.dpapi_ng_root_key))
|
||||
try:
|
||||
blob = DPAPINGBlob.unpack(data)
|
||||
target_sd = blob.protection_descriptor.get_target_sd()
|
||||
key = cache._get_key(
|
||||
target_sd,
|
||||
blob.key_identifier.root_key_identifier,
|
||||
blob.key_identifier.l0,
|
||||
blob.key_identifier.l1,
|
||||
blob.key_identifier.l2,
|
||||
)
|
||||
except (ValueError, NotImplementedError) as exc:
|
||||
raise ValueError(f"invalid or unsupported DPAPI-NG blob: {exc}") from None
|
||||
if key is None:
|
||||
raise ValueError(
|
||||
"no supplied offline KDS root key matches this DPAPI-NG blob; "
|
||||
"network fallback is disabled"
|
||||
)
|
||||
try:
|
||||
cleartext = _decrypt_blob(blob, key)
|
||||
except (ValueError, NotImplementedError) as exc:
|
||||
raise ValueError(f"DPAPI-NG decryption failed: {exc}") from None
|
||||
emit("[+] DPAPI-NG decrypted entirely from the supplied offline KDS root key")
|
||||
prepared, extension = core.prepare_output(
|
||||
cleartext, "blob", was_hex, args.output_format
|
||||
)
|
||||
return [core.OutputItem(prepared, extension, "DPAPI-NG", source_path)]
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "windows_hives",
|
||||
"name": "Windows SYSTEM/SECURITY/SAM hives",
|
||||
"description": "Offline DPAPI_SYSTEM extraction and optional local SAM hash export from collected registry hives.",
|
||||
"file_patterns": ["SYSTEM"],
|
||||
"web": {
|
||||
"input_label": "SYSTEM + SECURITY (and optional SAM) hives",
|
||||
"workflow_help": "Drop the collected hives together here, SYSTEM and SECURITY (add SAM to also export local hashes), or the folder that holds them. Tab 1 and Unlock key are not used.",
|
||||
"help": "Drop SYSTEM and SECURITY together (add SAM to also export local account hashes), or drop the folder that contains them. Each hive is found by name; SYSTEM is required, SECURITY yields DPAPI_SYSTEM, SAM is optional. Processing is local only.",
|
||||
"fields": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
"""Offline SYSTEM/SECURITY/SAM registry-hive processing.
|
||||
|
||||
Only local filenames are passed to Impacket's offline registry classes. This
|
||||
module never constructs RemoteOperations and never enables cached-logon,
|
||||
password-history, or general LSA-secret output.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
PLUGIN_ID = "windows_hives"
|
||||
DPAPI_KEY = re.compile(r"(?im)^dpapi_(machine|user)key:0x([0-9a-f]{40})$")
|
||||
SAM_LINE = re.compile(
|
||||
r"^[^:\r\n]+:[0-9]+:[0-9a-fA-F]{32}:[0-9a-fA-F]{32}:::$"
|
||||
)
|
||||
|
||||
|
||||
def parse_dpapi_system_callbacks(lines: list[str]) -> tuple[bytes, bytes]:
|
||||
found: dict[str, bytes] = {}
|
||||
for line in lines:
|
||||
for kind, value in DPAPI_KEY.findall(line):
|
||||
found[kind.casefold()] = bytes.fromhex(value)
|
||||
if set(found) != {"machine", "user"}:
|
||||
raise ValueError("SECURITY hive did not yield both DPAPI_SYSTEM keys")
|
||||
return found["machine"], found["user"]
|
||||
|
||||
|
||||
def validate_sam_lines(lines: list[str]) -> list[str]:
|
||||
clean = []
|
||||
for value in lines:
|
||||
line = value.strip()
|
||||
if not SAM_LINE.fullmatch(line):
|
||||
raise ValueError("Impacket returned an unexpected SAM record")
|
||||
clean.append(line)
|
||||
if not clean:
|
||||
raise ValueError("SAM hive contained no local account hash records")
|
||||
return clean
|
||||
|
||||
|
||||
def _hive_path(value: str | None, label: str) -> str | None:
|
||||
if not value:
|
||||
return None
|
||||
path = Path(value)
|
||||
try:
|
||||
with path.open("rb") as stream:
|
||||
magic = stream.read(4)
|
||||
except OSError as exc:
|
||||
raise ValueError(f"cannot read {label}: {exc}") from None
|
||||
if magic != b"regf":
|
||||
raise ValueError(f"{label} is not a Windows registry hive")
|
||||
return str(path)
|
||||
|
||||
|
||||
def _offline_classes():
|
||||
try:
|
||||
from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes
|
||||
except ImportError:
|
||||
raise ValueError(
|
||||
"install the optional impacket package to use the windows_hives plugin"
|
||||
) from None
|
||||
return LocalOperations, LSASecrets, SAMHashes
|
||||
|
||||
|
||||
def _is_hive(path: Path) -> bool:
|
||||
try:
|
||||
with path.open("rb") as stream:
|
||||
return stream.read(4) == b"regf"
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _locate_hive(files: list[Path], name: str) -> str | None:
|
||||
"""Find a hive by name among files: exact basename, then stem (handles
|
||||
SYSTEM.hive / system.dat), then a real regf hive whose name contains it."""
|
||||
for item in files:
|
||||
if item.name.casefold() == name:
|
||||
return str(item)
|
||||
for item in files:
|
||||
if item.stem.casefold() == name:
|
||||
return str(item)
|
||||
for item in files:
|
||||
if name in item.name.casefold() and _is_hive(item):
|
||||
return str(item)
|
||||
return None
|
||||
|
||||
|
||||
def run(_data, source_path, _was_hex, args, core, emit):
|
||||
if source_path is None:
|
||||
raise ValueError(
|
||||
"windows_hives requires the SYSTEM hive (drop SYSTEM, SECURITY and "
|
||||
"optional SAM together, or the folder holding them)"
|
||||
)
|
||||
source = Path(source_path)
|
||||
if source.is_dir():
|
||||
# One drop-all folder: locate each hive by name. Explicit flags win.
|
||||
files = [item for item in sorted(source.rglob("*")) if item.is_file()]
|
||||
system_src = _locate_hive(files, "system")
|
||||
if not system_src:
|
||||
seen = ", ".join(item.name for item in files[:12]) or "nothing"
|
||||
raise ValueError(
|
||||
f"no SYSTEM hive found among the dropped hives (saw: {seen}); "
|
||||
"include the SYSTEM hive and name it SYSTEM"
|
||||
)
|
||||
security_src = args.security_hive or _locate_hive(files, "security")
|
||||
sam_src = args.sam_hive or _locate_hive(files, "sam")
|
||||
else:
|
||||
system_src = str(source)
|
||||
security_src = args.security_hive
|
||||
sam_src = args.sam_hive
|
||||
|
||||
system_path = _hive_path(system_src, "SYSTEM hive")
|
||||
security_path = _hive_path(security_src, "SECURITY hive")
|
||||
sam_path = _hive_path(sam_src, "SAM hive")
|
||||
if not security_path and not sam_path:
|
||||
raise ValueError(
|
||||
"drop a SECURITY hive (for DPAPI_SYSTEM) and/or a SAM hive together "
|
||||
"with SYSTEM"
|
||||
)
|
||||
|
||||
LocalOperations, LSASecrets, SAMHashes = _offline_classes()
|
||||
try:
|
||||
boot_key = LocalOperations(system_path).getBootKey()
|
||||
except Exception as exc:
|
||||
raise ValueError(f"could not derive the SYSTEM boot key: {exc}") from None
|
||||
if not isinstance(boot_key, bytes) or len(boot_key) != 16:
|
||||
raise ValueError("SYSTEM hive produced an invalid boot key")
|
||||
emit("[+] derived the 16-byte boot key from the offline SYSTEM hive")
|
||||
|
||||
payloads: list[tuple[bytes, str, str]] = []
|
||||
errors: list[str] = []
|
||||
if security_path:
|
||||
callbacks: list[str] = []
|
||||
lsa = None
|
||||
try:
|
||||
lsa = LSASecrets(
|
||||
security_path,
|
||||
boot_key,
|
||||
None,
|
||||
isRemote=False,
|
||||
history=False,
|
||||
perSecretCallback=lambda _kind, secret: callbacks.append(str(secret)),
|
||||
)
|
||||
# Impacket processes the hive locally. The callback deliberately
|
||||
# retains only the DPAPI_SYSTEM record parsed below.
|
||||
lsa.dumpSecrets()
|
||||
machine_key, user_key = parse_dpapi_system_callbacks(callbacks)
|
||||
raw = machine_key + user_key
|
||||
metadata = {
|
||||
"format": "DPAPI_SYSTEM",
|
||||
"machine_key_hex": machine_key.hex(),
|
||||
"user_key_hex": user_key.hex(),
|
||||
"combined_hex": raw.hex(),
|
||||
}
|
||||
payloads.extend((
|
||||
(raw, ".dpapi_system", "DPAPI_SYSTEM raw key material"),
|
||||
(json.dumps(metadata, indent=2).encode() + b"\n", ".json", "DPAPI_SYSTEM metadata"),
|
||||
))
|
||||
emit("[+] recovered DPAPI_SYSTEM MachineKey and UserKey")
|
||||
except Exception as exc:
|
||||
message = f"could not decrypt DPAPI_SYSTEM from SECURITY: {exc}"
|
||||
errors.append(message)
|
||||
emit(f"[!] {message}")
|
||||
finally:
|
||||
if lsa is not None:
|
||||
try:
|
||||
lsa.finish()
|
||||
except Exception as exc:
|
||||
emit(f"[!] SECURITY hive cleanup warning: {exc}")
|
||||
|
||||
if sam_path:
|
||||
sam_lines: list[str] = []
|
||||
sam = None
|
||||
try:
|
||||
sam = SAMHashes(
|
||||
sam_path,
|
||||
boot_key,
|
||||
isRemote=False,
|
||||
history=False,
|
||||
perSecretCallback=lambda secret: sam_lines.append(str(secret)),
|
||||
)
|
||||
sam.dump()
|
||||
clean = validate_sam_lines(sam_lines)
|
||||
payloads.append((("\n".join(clean) + "\n").encode(), ".sam", "local SAM hashes"))
|
||||
emit(f"[+] recovered {len(clean)} local SAM account hash record(s)")
|
||||
except Exception as exc:
|
||||
message = f"could not decrypt the offline SAM hive: {exc}"
|
||||
errors.append(message)
|
||||
emit(f"[!] {message}")
|
||||
finally:
|
||||
if sam is not None:
|
||||
try:
|
||||
sam.finish()
|
||||
except Exception as exc:
|
||||
emit(f"[!] SAM hive cleanup warning: {exc}")
|
||||
|
||||
if not payloads:
|
||||
detail = errors[-1] if errors else "no requested hive output was recovered"
|
||||
raise ValueError(detail)
|
||||
if errors:
|
||||
emit(
|
||||
f"[!] partial hive result: recovered {len(payloads)} output(s); "
|
||||
f"{len(errors)} requested component(s) failed"
|
||||
)
|
||||
|
||||
count = len(payloads)
|
||||
return [
|
||||
core.OutputItem(data, extension, label, source_path, index, count)
|
||||
for index, (data, extension, label) in enumerate(payloads)
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
-r requirements.txt
|
||||
|
||||
# Direct Outlook parsing from a raw NTUSER.DAT hive (--type outlook on a hive).
|
||||
# Not needed if you export the binary "IMAP Password" value instead.
|
||||
python-registry>=1.3
|
||||
|
||||
# Offline DPAPI-NG plugin: SID-descriptor decryption from an exported KDS root
|
||||
# key. No network fallback is ever used.
|
||||
dpapi-ng>=0.2
|
||||
|
||||
# Offline SYSTEM/SECURITY/SAM hive plugin: DPAPI_SYSTEM recovery and optional
|
||||
# local SAM hash export. Uses only Impacket's local-file registry classes.
|
||||
impacket>=0.11
|
||||
@@ -0,0 +1 @@
|
||||
cryptography>=41.0
|
||||
Reference in New Issue
Block a user