Initial release

This commit is contained in:
ruslan
2026-09-23 22:27:51 +09:00
commit 69b891a96a
20 changed files with 9543 additions and 0 deletions
+22
View File
@@ -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
+21
View File
@@ -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.
+195
View File
@@ -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.**
![The local web UI: drop an artifact, inspect its structure, add key material, and decrypt — all on 127.0.0.1.](docs/images/web-ui.webp)
## 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).
+308
View File
@@ -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.
+532
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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. |
+183
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1962
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -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}
]
}
}
+194
View File
@@ -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
+14
View File
@@ -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."}
]
}
}
+150
View File
@@ -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)]
+12
View File
@@ -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": []
}
}
+213
View File
@@ -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)
]
+13
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
cryptography>=41.0