From 01f43ae464398ffdae2ef87609a0ef33704a745d Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Wed, 12 Mar 2025 21:30:27 -0700 Subject: [PATCH 1/6] Add username and password authentication --- SnaffCore/ActiveDirectory/AdData.cs | 4 +- SnaffCore/ActiveDirectory/DirectorySearch.cs | 7 +- .../Concurrency/BlockingTaskScheduler.cs | 16 +++- SnaffCore/Config/Options.cs | 4 + SnaffCore/Impersonator.cs | 74 +++++++++++++++++++ SnaffCore/SnaffCon.cs | 4 + SnaffCore/SnaffCore.csproj | 1 + Snaffler/Config.cs | 15 ++++ Snaffler/SnaffleRunner.cs | 12 +++ 9 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 SnaffCore/Impersonator.cs 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..57ae4d94 100644 --- a/SnaffCore/Concurrency/BlockingTaskScheduler.cs +++ b/SnaffCore/Concurrency/BlockingTaskScheduler.cs @@ -62,7 +62,21 @@ public void New(Action action) // okay, let's add the thing proceed = true; - _taskFactory.StartNew(action, _cancellationSource.Token); + void actionWithImpersonation() + { + Impersonator.StartImpersonating(); + + 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..4d224fc1 --- /dev/null +++ b/SnaffCore/Impersonator.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using SnaffCore.Config; + +namespace SnaffCore +{ + public class Impersonator + { + private static IntPtr _userHandle = IntPtr.Zero; + + public static bool Login(string domain, string username, string password) + { + if (_userHandle != IntPtr.Zero) + { + return true; + } + + return LogonUser(username, domain, password, 2, 0, ref _userHandle); + } + + public static bool StartImpersonating() + { + if (_userHandle == IntPtr.Zero) + { + return false; + } + + 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); + } + + [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)] + 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..cb63f6e0 100644 --- a/SnaffCore/SnaffCon.cs +++ b/SnaffCore/SnaffCon.cs @@ -82,6 +82,8 @@ public static BlockingStaticTaskScheduler GetFileTaskScheduler() public void Execute() { + Impersonator.StartImpersonating(); + StartTime = DateTime.Now; // This is the main execution thread. Timer statusUpdateTimer = @@ -157,6 +159,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..5584e4b3 100644 --- a/Snaffler/SnaffleRunner.cs +++ b/Snaffler/SnaffleRunner.cs @@ -192,6 +192,13 @@ public void Run(string[] args) //------------------------------------------- + // Check if user credentials were specified + if ((Options.TargetDomain != null) && (Options.Username != null) && (Options.Password != null)) + { + Impersonator.Login(Options.TargetDomain, Options.Username, Options.Password); + Impersonator.StartImpersonating(); + } + if (Options.Snaffle && (Options.SnafflePath.Length > 4)) { Directory.CreateDirectory(Options.SnafflePath); @@ -218,6 +225,11 @@ public void Run(string[] args) Console.WriteLine(e.ToString()); DumpQueue(); } + finally + { + Impersonator.StopImpersonating(); + Impersonator.Free(); + } } private void DumpQueue() From b105f764847a35d6a28bc3369c0b02b9513d7cb9 Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Thu, 27 Mar 2025 14:17:44 -0700 Subject: [PATCH 2/6] Allow for null domain and password, add logging --- Snaffler/SnaffleRunner.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Snaffler/SnaffleRunner.cs b/Snaffler/SnaffleRunner.cs index 5584e4b3..0ff40aa6 100644 --- a/Snaffler/SnaffleRunner.cs +++ b/Snaffler/SnaffleRunner.cs @@ -193,9 +193,10 @@ public void Run(string[] args) //------------------------------------------- // Check if user credentials were specified - if ((Options.TargetDomain != null) && (Options.Username != null) && (Options.Password != null)) + if (Options.Username != null) { - Impersonator.Login(Options.TargetDomain, Options.Username, Options.Password); + Mq.Info($"Impersonating {Options.Username}."); + Impersonator.Login(Options.TargetDomain ?? Environment.UserDomainName, Options.Username, Options.Password ?? ""); Impersonator.StartImpersonating(); } From 57ddb484884dba744820678e85b7b6cdbca2a489 Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Thu, 27 Mar 2025 17:23:32 -0400 Subject: [PATCH 3/6] Update README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) 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: From 8c2bc5c092e456f99364e6c688142152f5181360 Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Thu, 27 Mar 2025 14:46:10 -0700 Subject: [PATCH 4/6] Add error handling --- SnaffCore/Concurrency/BlockingTaskScheduler.cs | 8 +++++++- SnaffCore/Impersonator.cs | 14 ++++++++------ SnaffCore/SnaffCon.cs | 8 +++++++- Snaffler/SnaffleRunner.cs | 17 +++++++++++++++-- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/SnaffCore/Concurrency/BlockingTaskScheduler.cs b/SnaffCore/Concurrency/BlockingTaskScheduler.cs index 57ae4d94..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; @@ -64,7 +65,12 @@ public void New(Action action) void actionWithImpersonation() { - Impersonator.StartImpersonating(); + bool impersonateResult = Impersonator.StartImpersonating(); + if (!impersonateResult) + { + int errorCode = Marshal.GetLastWin32Error(); + throw new Exception($"[Error Code {errorCode}] Failed to impersonate {Impersonator.GetUsername()}."); + } try { diff --git a/SnaffCore/Impersonator.cs b/SnaffCore/Impersonator.cs index 4d224fc1..40974f05 100644 --- a/SnaffCore/Impersonator.cs +++ b/SnaffCore/Impersonator.cs @@ -1,16 +1,12 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using SnaffCore.Config; 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) { @@ -19,6 +15,7 @@ public static bool Login(string domain, string username, string password) return true; } + _username = username; return LogonUser(username, domain, password, 2, 0, ref _userHandle); } @@ -52,6 +49,11 @@ public static bool Free() return CloseHandle(_userHandle); } + public static string GetUsername() + { + return _username; + } + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool LogonUser( string lpszUsername, @@ -62,7 +64,7 @@ private static extern bool LogonUser( ref IntPtr phToken ); - [DllImport("advapi32.dll", CharSet = CharSet.Unicode)] + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool ImpersonateLoggedOnUser(IntPtr hToken); [DllImport("advapi32.dll", CharSet = CharSet.Auto)] diff --git a/SnaffCore/SnaffCon.cs b/SnaffCore/SnaffCon.cs index cb63f6e0..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,7 +83,12 @@ public static BlockingStaticTaskScheduler GetFileTaskScheduler() public void Execute() { - Impersonator.StartImpersonating(); + 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. diff --git a/Snaffler/SnaffleRunner.cs b/Snaffler/SnaffleRunner.cs index 0ff40aa6..92dbe376 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 { @@ -196,8 +197,20 @@ public void Run(string[] args) if (Options.Username != null) { Mq.Info($"Impersonating {Options.Username}."); - Impersonator.Login(Options.TargetDomain ?? Environment.UserDomainName, Options.Username, Options.Password ?? ""); - Impersonator.StartImpersonating(); + + 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()}."); + } } if (Options.Snaffle && (Options.SnafflePath.Length > 4)) From 403adc52943608f8cdcd11fa5ee13c826221fe95 Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Thu, 27 Mar 2025 17:35:46 -0700 Subject: [PATCH 5/6] Fix impersonation bug --- SnaffCore/Impersonator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SnaffCore/Impersonator.cs b/SnaffCore/Impersonator.cs index 40974f05..bdef194c 100644 --- a/SnaffCore/Impersonator.cs +++ b/SnaffCore/Impersonator.cs @@ -23,7 +23,7 @@ public static bool StartImpersonating() { if (_userHandle == IntPtr.Zero) { - return false; + return true; } return ImpersonateLoggedOnUser(_userHandle); From c6c950f00bd0733b259fb5953323891fe57e8fdb Mon Sep 17 00:00:00 2001 From: p0rtL6 Date: Sun, 6 Apr 2025 11:15:50 -0700 Subject: [PATCH 6/6] Update CurrentUser to impersonated username --- Snaffler/SnaffleRunner.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Snaffler/SnaffleRunner.cs b/Snaffler/SnaffleRunner.cs index 92dbe376..493aec4d 100644 --- a/Snaffler/SnaffleRunner.cs +++ b/Snaffler/SnaffleRunner.cs @@ -211,6 +211,8 @@ public void Run(string[] args) 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))