diff --git a/README.md b/README.md index 490b4c0e..9785d0e5 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,10 @@ The key incantations are: `-p` Path to a directory full of .toml formatted rules. Snaffler will load all of these in place of the default ruleset. +`--username` Username of an account that you want to impersonate + +`--password` Password of an account that you want to impersonate + ## What does any of this log output mean? Hopefully this annotated example will help: diff --git a/SnaffCore/ActiveDirectory/AdData.cs b/SnaffCore/ActiveDirectory/AdData.cs index 57feac13..a6df7268 100644 --- a/SnaffCore/ActiveDirectory/AdData.cs +++ b/SnaffCore/ActiveDirectory/AdData.cs @@ -72,7 +72,7 @@ private string GetNetBiosDomainName() { string ldapBase = $"CN=Partitions,CN=Configuration,DC={_targetDomain.Replace(".", ",DC=")}"; - DirectorySearch ds = new DirectorySearch(_targetDomain, _targetDc, ldapBase, null, null, 0, false); + DirectorySearch ds = new DirectorySearch(_targetDomain, _targetDc, ldapBase, MyOptions.Username, MyOptions.Password, 0, false); string[] ldapProperties = new string[] { "netbiosname"}; string ldapFilter = string.Format("(&(objectcategory=Crossref)(dnsRoot={0})(netBIOSName=*))",_targetDomain); @@ -107,7 +107,7 @@ private void SetDirectorySearch() } _targetDomainNetBIOSName = GetNetBiosDomainName(); - DirectorySearch directorySearch = new DirectorySearch(_targetDomain, _targetDc); + DirectorySearch directorySearch = new DirectorySearch(_targetDomain, _targetDc, null, MyOptions.Username, MyOptions.Password); _directorySearch = directorySearch; } diff --git a/SnaffCore/ActiveDirectory/DirectorySearch.cs b/SnaffCore/ActiveDirectory/DirectorySearch.cs index 0e0ce05a..0e580528 100644 --- a/SnaffCore/ActiveDirectory/DirectorySearch.cs +++ b/SnaffCore/ActiveDirectory/DirectorySearch.cs @@ -31,13 +31,10 @@ public class DirectorySearch //Thread-safe storage for our Ldap Connection Pool private readonly ConcurrentBag _connectionPool = new ConcurrentBag(); - public DirectorySearch(string domainName, string domainController, string ldapUserName = null, string ldapPassword = null, int ldapPort = 0, bool secureLdap = false) : - this(domainName, domainController, $"DC={domainName.Replace(".", ",DC=")}", ldapUserName, ldapPassword, ldapPort, secureLdap){ } - - public DirectorySearch(string domainName, string domainController, string baseLdapPath, string ldapUserName = null, string ldapPassword = null, int ldapPort = 0, bool secureLdap = false) + public DirectorySearch(string domainName, string domainController, string baseLdapPath = null, string ldapUserName = null, string ldapPassword = null, int ldapPort = 0, bool secureLdap = false) { _domainName = domainName; - _baseLdapPath = baseLdapPath; + _baseLdapPath = baseLdapPath ?? $"DC={domainName.Replace(".", ",DC=")}"; _domainController = domainController; _domainGuidMap = new Dictionary(); _ldapUsername = ldapUserName; diff --git a/SnaffCore/Concurrency/BlockingTaskScheduler.cs b/SnaffCore/Concurrency/BlockingTaskScheduler.cs index f748bd3a..3b2a508f 100644 --- a/SnaffCore/Concurrency/BlockingTaskScheduler.cs +++ b/SnaffCore/Concurrency/BlockingTaskScheduler.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Numerics; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -62,7 +63,26 @@ public void New(Action action) // okay, let's add the thing proceed = true; - _taskFactory.StartNew(action, _cancellationSource.Token); + void actionWithImpersonation() + { + bool impersonateResult = Impersonator.StartImpersonating(); + if (!impersonateResult) + { + int errorCode = Marshal.GetLastWin32Error(); + throw new Exception($"[Error Code {errorCode}] Failed to impersonate {Impersonator.GetUsername()}."); + } + + try + { + action(); + } + finally + { + Impersonator.StopImpersonating(); + } + } + + _taskFactory.StartNew(actionWithImpersonation, _cancellationSource.Token); } } } diff --git a/SnaffCore/Config/Options.cs b/SnaffCore/Config/Options.cs index 78c66282..72bde37d 100644 --- a/SnaffCore/Config/Options.cs +++ b/SnaffCore/Config/Options.cs @@ -58,6 +58,10 @@ public partial class Options public string TargetDc { get; set; } public bool LogDeniedShares { get; set; } = false; + // User Authentication Options + public string Username { get; set; } + public string Password { get; set; } + // FileScanner Options public bool DomainUserRules { get; set; } = false; public int DomainUserMinLen { get; set; } = 6; diff --git a/SnaffCore/Impersonator.cs b/SnaffCore/Impersonator.cs new file mode 100644 index 00000000..bdef194c --- /dev/null +++ b/SnaffCore/Impersonator.cs @@ -0,0 +1,76 @@ +using System; +using System.Runtime.InteropServices; + +namespace SnaffCore +{ + public class Impersonator + { + private static IntPtr _userHandle = IntPtr.Zero; + private static string _username = string.Empty; + + public static bool Login(string domain, string username, string password) + { + if (_userHandle != IntPtr.Zero) + { + return true; + } + + _username = username; + return LogonUser(username, domain, password, 2, 0, ref _userHandle); + } + + public static bool StartImpersonating() + { + if (_userHandle == IntPtr.Zero) + { + return true; + } + + return ImpersonateLoggedOnUser(_userHandle); + } + + public static bool StopImpersonating() + { + if (_userHandle == IntPtr.Zero) + { + return true; + } + + return RevertToSelf(); + } + + public static bool Free() + { + if (_userHandle == IntPtr.Zero) + { + return true; + } + + return CloseHandle(_userHandle); + } + + public static string GetUsername() + { + return _username; + } + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool LogonUser( + string lpszUsername, + string lpszDomain, + string lpszPassword, + int dwLogonType, + int dwLogonProvider, + ref IntPtr phToken + ); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool ImpersonateLoggedOnUser(IntPtr hToken); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto)] + private static extern bool RevertToSelf(); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern bool CloseHandle(IntPtr handle); + } +} diff --git a/SnaffCore/SnaffCon.cs b/SnaffCore/SnaffCon.cs index 6caf1ab4..22d69d5d 100644 --- a/SnaffCore/SnaffCon.cs +++ b/SnaffCore/SnaffCon.cs @@ -16,6 +16,7 @@ using static SnaffCore.Config.Options; using Timer = System.Timers.Timer; using System.Net; +using System.Runtime.InteropServices; namespace SnaffCore { @@ -82,6 +83,13 @@ public static BlockingStaticTaskScheduler GetFileTaskScheduler() public void Execute() { + bool impersonateResult = Impersonator.StartImpersonating(); + if (!impersonateResult) + { + int errorCode = Marshal.GetLastWin32Error(); + Mq.Error($"[Error Code {errorCode}] Failed to impersonate {Impersonator.GetUsername()}."); + } + StartTime = DateTime.Now; // This is the main execution thread. Timer statusUpdateTimer = @@ -157,6 +165,8 @@ public void Execute() Mq.Info("Finished at " + finished.ToLocalTime()); Mq.Info("Snafflin' took " + runSpan); Mq.Finish(); + + Impersonator.StopImpersonating(); } private void DomainDfsDiscovery() diff --git a/SnaffCore/SnaffCore.csproj b/SnaffCore/SnaffCore.csproj index fa0fc804..83a5164b 100644 --- a/SnaffCore/SnaffCore.csproj +++ b/SnaffCore/SnaffCore.csproj @@ -83,6 +83,7 @@ + diff --git a/Snaffler/Config.cs b/Snaffler/Config.cs index 0a7e9a57..90dffd26 100644 --- a/Snaffler/Config.cs +++ b/Snaffler/Config.cs @@ -108,6 +108,9 @@ private static Options ParseImpl(string[] args) "Interval between status updates (in minutes) also acts as a timeout for AD data to be gathered via LDAP. Turn this knob up if you aren't getting any computers from AD when you run Snaffler through a proxy or other slow link. Default = 5"); // list of letters i haven't used yet: gnqw + ValueArgument UsernameArg = new ValueArgument("username"); + ValueArgument PasswordArg = new ValueArgument("password"); + CommandLineParser.CommandLineParser parser = new CommandLineParser.CommandLineParser(); parser.Arguments.Add(timeOutArg); parser.Arguments.Add(configFileArg); @@ -132,6 +135,8 @@ private static Options ParseImpl(string[] args) parser.Arguments.Add(ruleDirArg); parser.Arguments.Add(logType); parser.Arguments.Add(compExclusionArg); + parser.Arguments.Add(UsernameArg); + parser.Arguments.Add(PasswordArg); // extra check to handle builtin behaviour from cmd line arg parser if ((args.Contains("--help") || args.Contains("/?") || args.Contains("help") || args.Contains("-h") || args.Length == 0)) @@ -380,6 +385,16 @@ private static Options ParseImpl(string[] args) } } + if (UsernameArg.Parsed) + { + parsedConfig.Username = UsernameArg.Value; + } + + if (PasswordArg.Parsed) + { + parsedConfig.Password = PasswordArg.Value; + } + if (!parsedConfig.LogToConsole && !parsedConfig.LogToFile) { Mq.Error( diff --git a/Snaffler/SnaffleRunner.cs b/Snaffler/SnaffleRunner.cs index 4d01882d..493aec4d 100644 --- a/Snaffler/SnaffleRunner.cs +++ b/Snaffler/SnaffleRunner.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using System.Text.RegularExpressions; using System.Threading; +using System.Runtime.InteropServices; namespace Snaffler { @@ -192,6 +193,28 @@ public void Run(string[] args) //------------------------------------------- + // Check if user credentials were specified + if (Options.Username != null) + { + Mq.Info($"Impersonating {Options.Username}."); + + bool loginResult = Impersonator.Login(Options.TargetDomain ?? Environment.UserDomainName, Options.Username, Options.Password ?? ""); + if (!loginResult) + { + int errorCode = Marshal.GetLastWin32Error(); + throw new Exception($"[Error Code {errorCode}] Failed to log in to {Impersonator.GetUsername()}."); + } + + bool impersonateResult = Impersonator.StartImpersonating(); + if (!impersonateResult) + { + int errorCode = Marshal.GetLastWin32Error(); + throw new Exception($"[Error Code {errorCode}] Failed to impersonate {Impersonator.GetUsername()}."); + } + + Options.CurrentUser = Options.Username; + } + if (Options.Snaffle && (Options.SnafflePath.Length > 4)) { Directory.CreateDirectory(Options.SnafflePath); @@ -218,6 +241,11 @@ public void Run(string[] args) Console.WriteLine(e.ToString()); DumpQueue(); } + finally + { + Impersonator.StopImpersonating(); + Impersonator.Free(); + } } private void DumpQueue()