diff --git a/src/NtdsAudit/NtdsAudit.cs b/src/NtdsAudit/NtdsAudit.cs index aef96d0..502aab0 100644 --- a/src/NtdsAudit/NtdsAudit.cs +++ b/src/NtdsAudit/NtdsAudit.cs @@ -24,6 +24,8 @@ internal class NtdsAudit private readonly IReadOnlyDictionary _ldapDisplayNameToDatatableColumnNameDictionary; private readonly LinkTableRow[] _linkTable; private readonly MSysObjectsRow[] _mSysObjects; + private readonly bool _useOUFilter; + private readonly IEnumerable _ouFilter; /// /// Initializes a new instance of the class. @@ -33,7 +35,7 @@ internal class NtdsAudit /// A value indicating whether to include history hashes /// The path to the System hive. /// The path to a wordlist for simple hash cracking. - public NtdsAudit(string ntdsPath, bool dumphashes, bool includeHistoryHashes, string systemHivePath, string wordlistPath) + public NtdsAudit(string ntdsPath, bool dumphashes, bool includeHistoryHashes, string systemHivePath, string wordlistPath, string ouFilterFilePath) { ntdsPath = ntdsPath ?? throw new ArgumentNullException(nameof(ntdsPath)); @@ -43,6 +45,12 @@ public NtdsAudit(string ntdsPath, bool dumphashes, bool includeHistoryHashes, st progress = new ProgressBar("Performing audit..."); } + if (!string.IsNullOrWhiteSpace(ouFilterFilePath)) + { + _useOUFilter = true; + _ouFilter = File.ReadAllLines(ouFilterFilePath).Where(x => !string.IsNullOrWhiteSpace(x)); + } + try { using (var db = new JetDb(ntdsPath)) @@ -618,6 +626,12 @@ private ComputerInfo[] CalculateComputerInfo() Disabled = (row.UserAccountControlValue & (int)ADS_USER_FLAG.ADS_UF_ACCOUNTDISABLE) == (int)ADS_USER_FLAG.ADS_UF_ACCOUNTDISABLE, LastLogon = row.LastLogon ?? DateTime.Parse("01.01.1601 00:00:00", CultureInfo.InvariantCulture), }; + + if (_useOUFilter && !_ouFilter.Any(filterOU => computerInfo.Dn.EndsWith(filterOU))) + { + continue; + } + computers.Add(computerInfo); } } @@ -898,32 +912,35 @@ private UserInfo[] CalculateUserInfo() var users = new List(); foreach (var row in _datatable) { - if ((row.UserAccountControlValue & (int)ADS_USER_FLAG.ADS_UF_NORMAL_ACCOUNT) == (int)ADS_USER_FLAG.ADS_UF_NORMAL_ACCOUNT) + if ((row.UserAccountControlValue & (int)ADS_USER_FLAG.ADS_UF_NORMAL_ACCOUNT) == (int)ADS_USER_FLAG.ADS_UF_NORMAL_ACCOUNT && row.ObjectCategory.Equals("Person")) { - if (row.ObjectCategory.Equals("Person")) + var userInfo = new UserInfo { - var userInfo = new UserInfo - { - Dnt = row.Dnt.Value, - Name = row.Name, - Dn = row.Dn, - DomainSid = row.Sid.AccountDomainSid, - Disabled = (row.UserAccountControlValue & (int)ADS_USER_FLAG.ADS_UF_ACCOUNTDISABLE) == (int)ADS_USER_FLAG.ADS_UF_ACCOUNTDISABLE, - LastLogon = row.LastLogon ?? DateTime.Parse("01.01.1601 00:00:00", CultureInfo.InvariantCulture), - PasswordNotRequired = (row.UserAccountControlValue & (int)ADS_USER_FLAG.ADS_UF_PASSWD_NOTREQD) == (int)ADS_USER_FLAG.ADS_UF_PASSWD_NOTREQD, - PasswordNeverExpires = (row.UserAccountControlValue & (int)ADS_USER_FLAG.ADS_UF_DONT_EXPIRE_PASSWD) == (int)ADS_USER_FLAG.ADS_UF_DONT_EXPIRE_PASSWD, - Expires = GetAccountExpiresDateTimeFromByteArray(row.AccountExpires), - PasswordLastChanged = row.LastPasswordChange ?? DateTime.Parse("01.01.1601 00:00:00", CultureInfo.InvariantCulture), - SamAccountName = row.SamAccountName, - Rid = row.Rid, - LmHash = row.LmHash, - NtHash = row.NtHash, - LmHistory = row.LmHistory, - NtHistory = row.NtHistory, - ClearTextPassword = row.SupplementalCredentials?.ContainsKey("Primary:CLEARTEXT") ?? false ? Encoding.Unicode.GetString(row.SupplementalCredentials["Primary:CLEARTEXT"]) : null - }; - users.Add(userInfo); + Dnt = row.Dnt.Value, + Name = row.Name, + Dn = row.Dn, + DomainSid = row.Sid.AccountDomainSid, + Disabled = (row.UserAccountControlValue & (int)ADS_USER_FLAG.ADS_UF_ACCOUNTDISABLE) == (int)ADS_USER_FLAG.ADS_UF_ACCOUNTDISABLE, + LastLogon = row.LastLogon ?? DateTime.Parse("01.01.1601 00:00:00", CultureInfo.InvariantCulture), + PasswordNotRequired = (row.UserAccountControlValue & (int)ADS_USER_FLAG.ADS_UF_PASSWD_NOTREQD) == (int)ADS_USER_FLAG.ADS_UF_PASSWD_NOTREQD, + PasswordNeverExpires = (row.UserAccountControlValue & (int)ADS_USER_FLAG.ADS_UF_DONT_EXPIRE_PASSWD) == (int)ADS_USER_FLAG.ADS_UF_DONT_EXPIRE_PASSWD, + Expires = GetAccountExpiresDateTimeFromByteArray(row.AccountExpires), + PasswordLastChanged = row.LastPasswordChange ?? DateTime.Parse("01.01.1601 00:00:00", CultureInfo.InvariantCulture), + SamAccountName = row.SamAccountName, + Rid = row.Rid, + LmHash = row.LmHash, + NtHash = row.NtHash, + LmHistory = row.LmHistory, + NtHistory = row.NtHistory, + ClearTextPassword = row.SupplementalCredentials?.ContainsKey("Primary:CLEARTEXT") ?? false ? Encoding.Unicode.GetString(row.SupplementalCredentials["Primary:CLEARTEXT"]) : null + }; + + if (_useOUFilter && !_ouFilter.Any(filterOU => userInfo.Dn.EndsWith(filterOU))) + { + continue; } + + users.Add(userInfo); } } diff --git a/src/NtdsAudit/Program.cs b/src/NtdsAudit/Program.cs index 4254297..892e995 100644 --- a/src/NtdsAudit/Program.cs +++ b/src/NtdsAudit/Program.cs @@ -49,6 +49,7 @@ private static void Main(string[] args) var includeHistoryHashes = commandLineApplication.Option("--history-hashes", "Include history hashes in the pdwump output.", CommandOptionType.NoValue); var dumpReversiblePath = commandLineApplication.Option("--dump-reversible ", "The path to output clear text passwords, if reversible encryption is enabled.", CommandOptionType.SingleValue); var wordlistPath = commandLineApplication.Option("--wordlist", "The path to a wordlist of weak passwords for basic hash cracking. Warning, using this option is slow, the use of a dedicated password cracker, such as 'john', is recommended instead.", CommandOptionType.SingleValue); + var ouFilterFilePath = commandLineApplication.Option("--ou-filter-file ", "The path to file containing a line separated list of OUs to which to limit user and computer results.", CommandOptionType.SingleValue); var baseDate = commandLineApplication.Option("--base-date ", "Specifies a custom date to be used as the base date in statistics. The last modified date of the NTDS file is used by default.", CommandOptionType.SingleValue); var debug = commandLineApplication.Option("--debug", "Show debug output.", CommandOptionType.NoValue); @@ -116,6 +117,12 @@ private static void Main(string[] args) argumentsValid = false; } + if (ouFilterFilePath.HasValue() && !File.Exists(ouFilterFilePath.Value())) + { + ConsoleEx.WriteError($"OU filter file \"{ouFilterFilePath.Value()}\" does not exist."); + argumentsValid = false; + } + if (showHelp) { commandLineApplication.ShowHelp(); @@ -123,7 +130,7 @@ private static void Main(string[] args) if (!showHelp && argumentsValid) { - var ntdsAudit = new NtdsAudit(ntdsPath.Value, pwdumpPath.HasValue(), includeHistoryHashes.HasValue(), systemHivePath.Value(), wordlistPath.Value()); + var ntdsAudit = new NtdsAudit(ntdsPath.Value, pwdumpPath.HasValue(), includeHistoryHashes.HasValue(), systemHivePath.Value(), wordlistPath.Value(), ouFilterFilePath.Value()); var baseDateTime = baseDate.HasValue() ? DateTime.ParseExact(baseDate.Value(), "yyyyMMdd", null, DateTimeStyles.AssumeUniversal) : new FileInfo(ntdsPath.Value).LastWriteTimeUtc; @@ -219,11 +226,11 @@ private static void WriteComputersCsvFile(string computersCsvPath, NtdsAudit ntd { using (var file = new StreamWriter(computersCsvPath, false)) { - file.WriteLine("Domain,Computer,Disabled,Last Logon"); + file.WriteLine("Domain,Computer,Disabled,Last Logon,DN"); foreach (var computer in ntdsAudit.Computers) { var domain = ntdsAudit.Domains.Single(x => x.Sid == computer.DomainSid); - file.WriteLine($"{domain.Fqdn},{computer.Name},{computer.Disabled},{computer.LastLogon}"); + file.WriteLine($"{domain.Fqdn},{computer.Name},{computer.Disabled},{computer.LastLogon},\"{computer.Dn}\""); } } } @@ -349,11 +356,11 @@ private static void WriteUsersCsvFile(string usersCsvPath, NtdsAudit ntdsAudit, { using (var file = new StreamWriter(usersCsvPath, false)) { - file.WriteLine("Domain,Username,Administrator,Domain Admin,Enterprise Admin,Disabled,Expired,Password Never Expires,Password Not Required,Password Last Changed,Last Logon"); + file.WriteLine("Domain,Username,Administrator,Domain Admin,Enterprise Admin,Disabled,Expired,Password Never Expires,Password Not Required,Password Last Changed,Last Logon,DN"); foreach (var user in ntdsAudit.Users) { var domain = ntdsAudit.Domains.Single(x => x.Sid == user.DomainSid); - file.WriteLine($"{domain.Fqdn},{user.SamAccountName},{user.RecursiveGroupSids.Contains(domain.AdministratorsSid)},{user.RecursiveGroupSids.Contains(domain.DomainAdminsSid)},{user.RecursiveGroupSids.Intersect(ntdsAudit.Domains.Select(x => x.EnterpriseAdminsSid)).Any()},{user.Disabled},{!user.Disabled && user.Expires.HasValue && user.Expires.Value < baseDateTime},{user.PasswordNeverExpires},{user.PasswordNotRequired},{user.PasswordLastChanged},{user.LastLogon}"); + file.WriteLine($"{domain.Fqdn},{user.SamAccountName},{user.RecursiveGroupSids.Contains(domain.AdministratorsSid)},{user.RecursiveGroupSids.Contains(domain.DomainAdminsSid)},{user.RecursiveGroupSids.Intersect(ntdsAudit.Domains.Select(x => x.EnterpriseAdminsSid)).Any()},{user.Disabled},{!user.Disabled && user.Expires.HasValue && user.Expires.Value < baseDateTime},{user.PasswordNeverExpires},{user.PasswordNotRequired},{user.PasswordLastChanged},{user.LastLogon},\"{user.Dn}\""); } } } diff --git a/src/NtdsAudit/Properties/AssemblyInfo.cs b/src/NtdsAudit/Properties/AssemblyInfo.cs index ed4f16c..da5fb19 100644 --- a/src/NtdsAudit/Properties/AssemblyInfo.cs +++ b/src/NtdsAudit/Properties/AssemblyInfo.cs @@ -32,6 +32,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.6.0")] -[assembly: AssemblyFileVersion("2.0.6.0")] +[assembly: AssemblyVersion("2.0.7.0")] +[assembly: AssemblyFileVersion("2.0.7.0")] [assembly: CLSCompliant(true)]