Пример #1
0
        private void CalculateDnsForDatatableRows()
        {
            Stopwatch stopwatch = null;

            if (ShowDebugOutput)
            {
                ConsoleEx.WriteDebug($"Called: {nameof(NtdsAudit)}::{nameof(CalculateDnsForDatatableRows)}");
                stopwatch = new Stopwatch();
                stopwatch.Start();
            }

            var commonNameAttrbiuteId         = int.Parse(Regex.Replace(_ldapDisplayNameToDatatableColumnNameDictionary["cn"], "[A-Za-z-]", string.Empty, RegexOptions.None), CultureInfo.InvariantCulture);
            var organizationalUnitAttrbiuteId = int.Parse(Regex.Replace(_ldapDisplayNameToDatatableColumnNameDictionary["ou"], "[A-Za-z-]", string.Empty, RegexOptions.None), CultureInfo.InvariantCulture);
            var domainComponentAttrbiuteId    = int.Parse(Regex.Replace(_ldapDisplayNameToDatatableColumnNameDictionary["dc"], "[A-Za-z-]", string.Empty, RegexOptions.None), CultureInfo.InvariantCulture);

            var attributeIdToDistinguishedNamePrefexDictionary = new Dictionary <int, string>
            {
                [commonNameAttrbiuteId]         = "CN=",
                [organizationalUnitAttrbiuteId] = "OU=",
                [domainComponentAttrbiuteId]    = "DC=",
            };

            var dntToPartialDnDictionary = new Dictionary <int, string>();
            var dntToPdntDictionary      = new Dictionary <int, int>();

            foreach (var row in _datatable)
            {
                if (row.RdnType == commonNameAttrbiuteId ||
                    row.RdnType == organizationalUnitAttrbiuteId ||
                    row.RdnType == domainComponentAttrbiuteId)
                {
                    dntToPartialDnDictionary[row.Dnt.Value] = attributeIdToDistinguishedNamePrefexDictionary[row.RdnType.Value] + row.Name;
                    if (row.ParentDnt.Value != 0)
                    {
                        dntToPdntDictionary[row.Dnt.Value] = row.ParentDnt.Value;
                    }
                }
            }

            var dntToDnDictionary = new Dictionary <int, string>();

            foreach (var kvp in dntToPartialDnDictionary)
            {
                dntToDnDictionary[kvp.Key] = dntToPartialDnDictionary[kvp.Key];
                var parentDnt = dntToPdntDictionary[kvp.Key];
                while (dntToPartialDnDictionary.ContainsKey(parentDnt))
                {
                    dntToDnDictionary[kvp.Key] += "," + dntToPartialDnDictionary[parentDnt];
                    parentDnt = dntToPdntDictionary[parentDnt];
                }
            }

            foreach (var row in _datatable)
            {
                if (row.RdnType == commonNameAttrbiuteId ||
                    row.RdnType == organizationalUnitAttrbiuteId ||
                    row.RdnType == domainComponentAttrbiuteId)
                {
                    row.Dn = dntToDnDictionary[row.Dnt.Value];
                }
            }

            if (ShowDebugOutput)
            {
                stopwatch.Stop();
                ConsoleEx.WriteDebug($"  Completed in {stopwatch.Elapsed}");
            }
        }
Пример #2
0
        private static DatatableRow[] EnumerateDatatableTable(JetDb db, IReadOnlyDictionary <string, string> ldapDisplayNameToDatatableColumnNameDictionary, bool dumpHashes, bool includeHistoryHashes)
        {
            Stopwatch stopwatch = null;

            if (ShowDebugOutput)
            {
                ConsoleEx.WriteDebug($"Called: {nameof(NtdsAudit)}::{nameof(EnumerateDatatableTable)}");
                stopwatch = new Stopwatch();
                stopwatch.Start();
            }

            var datatable    = new List <DatatableRow>();
            var deletedCount = 0;

            using (var table = db.OpenJetDbTable(DATATABLE))
            {
                // Get a dictionary mapping column names to column ids
                var columnDictionary = table.GetColumnDictionary();

                // Loop over the table
                table.MoveBeforeFirst();
                while (table.TryMoveNext())
                {
                    var accountExpiresColumn = new BytesColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["accountExpires"]]
                    };
                    var displayNameColumn = new StringColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["displayName"]]
                    };
                    var distinguishedNameTagColumn = new Int32ColumnValue {
                        Columnid = columnDictionary["DNT_col"]
                    };
                    var groupTypeColumn = new Int32ColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["groupType"]]
                    };
                    var isDeletedColumn = new Int32ColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["isDeleted"]]
                    };
                    var lastLogonColumn = new LdapDateTimeColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["lastLogonTimestamp"]]
                    };
                    var lmColumn = new BytesColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["dBCSPwd"]]
                    };
                    var lmHistoryColumn = new BytesColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["lmPwdHistory"]]
                    };
                    var nameColumn = new StringColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["name"]]
                    };
                    var ntColumn = new BytesColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["unicodePwd"]]
                    };
                    var ntHistoryColumn = new BytesColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["ntPwdHistory"]]
                    };
                    var objColumn = new BoolColumnValue {
                        Columnid = columnDictionary["OBJ_col"]
                    };
                    var objectCategoryColumn = new Int32ColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["objectCategory"]]
                    };
                    var objectSidColumn = new BytesColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["objectSid"]]
                    };
                    var parentDistinguishedNameTagColumn = new Int32ColumnValue {
                        Columnid = columnDictionary["PDNT_col"]
                    };
                    var passwordLastSetColumn = new LdapDateTimeColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["pwdLastSet"]]
                    };
                    var pekListColumn = new BytesColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["pekList"]]
                    };
                    var primaryGroupIdColumn = new Int32ColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["primaryGroupID"]]
                    };
                    var rdnTypeColumn = new Int32ColumnValue {
                        Columnid = columnDictionary["RDNtyp_col"]
                    };                                                                                      // The RDNTyp_col holds the Attribute-ID for the attribute being used as the RDN, such as CN, OU, DC
                    var samAccountNameColumn = new StringColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["sAMAccountName"]]
                    };
                    var timeColumn = new LdapDateTimeColumnValue {
                        Columnid = columnDictionary["time_col"]
                    };
                    var userAccountControlColumn = new Int32ColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["userAccountControl"]]
                    };
                    var supplementalCredentialsColumn = new BytesColumnValue {
                        Columnid = columnDictionary[ldapDisplayNameToDatatableColumnNameDictionary["supplementalCredentials"]]
                    };

                    var columns = new List <ColumnValue>
                    {
                        accountExpiresColumn,
                        displayNameColumn,
                        distinguishedNameTagColumn,
                        groupTypeColumn,
                        isDeletedColumn,
                        lastLogonColumn,
                        nameColumn,
                        objColumn,
                        objectCategoryColumn,
                        objectSidColumn,
                        parentDistinguishedNameTagColumn,
                        passwordLastSetColumn,
                        primaryGroupIdColumn,
                        rdnTypeColumn,
                        samAccountNameColumn,
                        timeColumn,
                        userAccountControlColumn,
                    };

                    if (dumpHashes)
                    {
                        columns.Add(pekListColumn);
                        columns.Add(lmColumn);
                        columns.Add(ntColumn);
                        columns.Add(supplementalCredentialsColumn);

                        if (includeHistoryHashes)
                        {
                            columns.Add(lmHistoryColumn);
                            columns.Add(ntHistoryColumn);
                        }
                    }

                    table.RetrieveColumns(columns.ToArray());

                    // Skip deleted objects
                    if (isDeletedColumn.Value.HasValue && isDeletedColumn.Value != 0)
                    {
                        deletedCount++;
                        continue;
                    }

                    // Some deleted objects do not have the isDeleted flag but do have a string appended to the DN (https://support.microsoft.com/en-us/help/248047/phantoms--tombstones-and-the-infrastructure-master)
                    if (nameColumn.Value?.Contains("\nDEL:") ?? false)
                    {
                        deletedCount++;
                        continue;
                    }

                    SecurityIdentifier sid = null;
                    uint rid = 0;
                    if (objectSidColumn.Error == JET_wrn.Success)
                    {
                        var sidBytes = objectSidColumn.Value;
                        var ridBytes = sidBytes.Skip(sidBytes.Length - sizeof(int)).Take(sizeof(int)).Reverse().ToArray();
                        sidBytes = sidBytes.Take(sidBytes.Length - sizeof(int)).Concat(ridBytes).ToArray();
                        rid      = BitConverter.ToUInt32(ridBytes, 0);
                        sid      = new SecurityIdentifier(sidBytes, 0);
                    }

                    var row = new DatatableRow
                    {
                        AccountExpires          = accountExpiresColumn.Value,
                        DisplayName             = displayNameColumn.Value,
                        Dnt                     = distinguishedNameTagColumn.Value,
                        GroupType               = groupTypeColumn.Value,
                        LastLogon               = lastLogonColumn.Value,
                        Name                    = nameColumn.Value,
                        ObjectCategoryDnt       = objectCategoryColumn.Value,
                        Rid                     = rid,
                        Sid                     = sid,
                        ParentDnt               = parentDistinguishedNameTagColumn.Value,
                        Phantom                 = objColumn.Value == false,
                        LastPasswordChange      = passwordLastSetColumn.Value,
                        PrimaryGroupDnt         = primaryGroupIdColumn.Value,
                        RdnType                 = rdnTypeColumn.Value,
                        SamAccountName          = samAccountNameColumn.Value,
                        UserAccountControlValue = userAccountControlColumn.Value,
                    };

                    if (dumpHashes)
                    {
                        if (pekListColumn.Value != null)
                        {
                            row.PekList = pekListColumn.Value;
                        }

                        if (lmColumn.Value != null)
                        {
                            row.EncryptedLmHash = lmColumn.Value;
                        }

                        if (ntColumn.Value != null)
                        {
                            row.EncryptedNtHash = ntColumn.Value;
                        }

                        if (includeHistoryHashes)
                        {
                            if (lmHistoryColumn.Value != null)
                            {
                                row.EncryptedLmHistory = lmHistoryColumn.Value;
                            }

                            if (ntHistoryColumn.Value != null)
                            {
                                row.EncryptedNtHistory = ntHistoryColumn.Value;
                            }
                        }

                        if (supplementalCredentialsColumn.Value != null)
                        {
                            row.SupplementalCredentialsBlob = supplementalCredentialsColumn.Value;
                        }
                    }

                    datatable.Add(row);
                }
            }

            if (ShowDebugOutput)
            {
                ConsoleEx.WriteDebug($"  Skipped {deletedCount} deleted objects");
                ConsoleEx.WriteDebug($"  Enumerated {datatable.Count} objects");

                stopwatch.Stop();
                ConsoleEx.WriteDebug($"  Completed in {stopwatch.Elapsed}");
            }

            return(datatable.ToArray());
        }
Пример #3
0
        private static void Main(string[] args)
        {
            LaunchDebugger();

            var commandLineApplication = new CommandLineApplication
            {
                FullName         = "NtdsAudit",
                Description      = "A utility for auditing Active Directory",
                ExtendedHelpText = @"
WARNING: Use of the --pwdump option will result in decryption of password hashes using the System Key.
Sensitive information will be stored in memory and on disk. Ensure the pwdump file is handled appropriately.",
            };

            commandLineApplication.VersionOption("-v | --version", Assembly.GetEntryAssembly().GetName().Version.ToString());

            commandLineApplication.HelpOption("-h | --help");

            var ntdsPath             = commandLineApplication.Argument("NTDS file", "The path of the NTDS.dit database to be audited, required.", false);
            var systemHivePath       = commandLineApplication.Option("-s | --system <file>", "The path of the associated SYSTEM hive, required when using the pwdump option.", CommandOptionType.SingleValue);
            var pwdumpPath           = commandLineApplication.Option("-p | --pwdump <file>", "The path to output hashes in pwdump format.", CommandOptionType.SingleValue);
            var usersCsvPath         = commandLineApplication.Option("-u | --users-csv <file>", "The path to output user details in CSV format.", CommandOptionType.SingleValue);
            var computersCsvPath     = commandLineApplication.Option("-c | --computers-csv <file>", "The path to output computer details in CSV format.", CommandOptionType.SingleValue);
            var includeHistoryHashes = commandLineApplication.Option("--history-hashes", "Include history hashes in the pdwump output.", CommandOptionType.NoValue);
            var dumpReversiblePath   = commandLineApplication.Option("--dump-reversible <file>", "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 baseDate             = commandLineApplication.Option("--base-date <yyyyMMdd>", "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);

            commandLineApplication.OnExecute(() =>
            {
                var argumentsValid = true;
                var showHelp       = false;

                if (debug.HasValue())
                {
                    NtdsAudit.ShowDebugOutput = true;
                }

                if (string.IsNullOrWhiteSpace(ntdsPath.Value))
                {
                    ConsoleEx.WriteError($"Missing NTDS file argument.");
                    argumentsValid = false;
                    showHelp       = true;
                }
                else if (!File.Exists(ntdsPath.Value))
                {
                    ConsoleEx.WriteError($"NTDS file \"{ntdsPath.Value}\" does not exist.");
                    argumentsValid = false;
                }

                if (pwdumpPath.HasValue() && !systemHivePath.HasValue())
                {
                    ConsoleEx.WriteError($"SYSTEM file argument is required when using the pwdump option.");
                    argumentsValid = false;
                    showHelp       = true;
                }
                else if (pwdumpPath.HasValue() && !File.Exists(systemHivePath.Value()))
                {
                    ConsoleEx.WriteError($"SYSTEM file \"{systemHivePath.Value()}\" does not exist.");
                    argumentsValid = false;
                }

                if (pwdumpPath.HasValue() && !string.IsNullOrEmpty(Path.GetDirectoryName(pwdumpPath.Value())) && !Directory.Exists(Path.GetDirectoryName(pwdumpPath.Value())))
                {
                    ConsoleEx.WriteError($"pwdump output directory \"{Path.GetDirectoryName(pwdumpPath.Value())}\" does not exist.");
                    argumentsValid = false;
                }

                if (usersCsvPath.HasValue() && !string.IsNullOrEmpty(Path.GetDirectoryName(usersCsvPath.Value())) && !Directory.Exists(Path.GetDirectoryName(usersCsvPath.Value())))
                {
                    ConsoleEx.WriteError($"Users CSV output directory \"{Path.GetDirectoryName(usersCsvPath.Value())}\" does not exist.");
                    argumentsValid = false;
                }

                if (computersCsvPath.HasValue() && !string.IsNullOrEmpty(Path.GetDirectoryName(computersCsvPath.Value())) && !Directory.Exists(Path.GetDirectoryName(computersCsvPath.Value())))
                {
                    ConsoleEx.WriteError($"Computers CSV output directory \"{Path.GetDirectoryName(computersCsvPath.Value())}\" does not exist.");
                    argumentsValid = false;
                }

                if (dumpReversiblePath.HasValue() && !string.IsNullOrEmpty(Path.GetDirectoryName(dumpReversiblePath.Value())) && !Directory.Exists(Path.GetDirectoryName(dumpReversiblePath.Value())))
                {
                    ConsoleEx.WriteError($"Dump Reverible output directory \"{Path.GetDirectoryName(dumpReversiblePath.Value())}\" does not exist.");
                    argumentsValid = false;
                }

                if (wordlistPath.HasValue() && !File.Exists(wordlistPath.Value()))
                {
                    ConsoleEx.WriteError($"Wordlist file \"{wordlistPath.Value()}\" does not exist.");
                    argumentsValid = false;
                }

                if (showHelp)
                {
                    commandLineApplication.ShowHelp();
                }

                if (!showHelp && argumentsValid)
                {
                    var ntdsAudit = new NtdsAudit(ntdsPath.Value, pwdumpPath.HasValue(), includeHistoryHashes.HasValue(), systemHivePath.Value(), wordlistPath.Value());

                    var baseDateTime = baseDate.HasValue() ? DateTime.ParseExact(baseDate.Value(), "yyyyMMdd", null, DateTimeStyles.AssumeUniversal) : new FileInfo(ntdsPath.Value).LastWriteTimeUtc;

                    PrintConsoleStatistics(ntdsAudit, baseDateTime);

                    if (pwdumpPath.HasValue())
                    {
                        WritePwDumpFile(pwdumpPath.Value(), ntdsAudit, baseDateTime, includeHistoryHashes.HasValue(), wordlistPath.HasValue(), dumpReversiblePath.Value());
                    }

                    if (usersCsvPath.HasValue())
                    {
                        WriteUsersCsvFile(usersCsvPath.Value(), ntdsAudit, baseDateTime);
                    }

                    if (computersCsvPath.HasValue())
                    {
                        WriteComputersCsvFile(computersCsvPath.Value(), ntdsAudit, baseDateTime);
                    }
                }

                return(argumentsValid ? 0 : -1);
            });

            try
            {
                commandLineApplication.Execute(args);
            }
            catch (CommandParsingException ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
Пример #4
0
        private void DecryptSecretData(string systemKeyPath, bool includeHistoryHashes)
        {
            Stopwatch stopwatch = null;

            if (ShowDebugOutput)
            {
                ConsoleEx.WriteDebug($"Called: {nameof(NtdsAudit)}::{nameof(DecryptSecretData)}");
                stopwatch = new Stopwatch();
                stopwatch.Start();
            }

            var systemKey = SystemHive.LoadSystemKeyFromHive(systemKeyPath);

            var encryptedPek     = _datatable.Single(x => x.PekList != null).PekList;
            var decryptedPekList = NTCrypto.DecryptPekList(systemKey, encryptedPek);

            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.EncryptedLmHash != null)
                    {
                        row.LmHash = ByteArrayToHexString(NTCrypto.DecryptHashes(decryptedPekList, row.EncryptedLmHash, row.Rid));
                    }
                    else
                    {
                        row.LmHash = EMPTY_LM_HASH;
                    }

                    if (row.EncryptedNtHash != null)
                    {
                        row.NtHash = ByteArrayToHexString(NTCrypto.DecryptHashes(decryptedPekList, row.EncryptedNtHash, row.Rid));
                    }
                    else
                    {
                        row.NtHash = EMPTY_NT_HASH;
                    }

                    if (includeHistoryHashes)
                    {
                        if (row.EncryptedLmHistory != null)
                        {
                            var hashStrings = new List <string>();

                            var decryptedHashes = NTCrypto.DecryptHashes(decryptedPekList, row.EncryptedLmHistory, row.Rid);

                            // The first hash is the same as the current hash, so skip it
                            for (var i = 16; i < decryptedHashes.Length; i += 16)
                            {
                                // If lm hash is disabled for the account, the lm history will contain junk data, ignore it
                                if (row.LmHash == EMPTY_LM_HASH)
                                {
                                    hashStrings.Add(EMPTY_LM_HASH);
                                }
                                else
                                {
                                    hashStrings.Add(ByteArrayToHexString(decryptedHashes.Skip(i).Take(16).ToArray()));
                                }
                            }

                            row.LmHistory = hashStrings.ToArray();
                        }

                        if (row.EncryptedNtHistory != null)
                        {
                            var hashStrings = new List <string>();

                            var decryptedHashes = NTCrypto.DecryptHashes(decryptedPekList, row.EncryptedNtHistory, row.Rid);

                            // The first hash is the same as the current hash, so skip it
                            for (var i = 16; i < decryptedHashes.Length; i += 16)
                            {
                                hashStrings.Add(ByteArrayToHexString(decryptedHashes.Skip(i).Take(16).ToArray()));
                            }

                            row.NtHistory = hashStrings.ToArray();
                        }
                    }

                    if (row.SupplementalCredentialsBlob != null)
                    {
                        row.SupplementalCredentials = NTCrypto.DecryptSupplementalCredentials(decryptedPekList, row.SupplementalCredentialsBlob);
                    }
                }
            }

            if (ShowDebugOutput)
            {
                stopwatch.Stop();
                ConsoleEx.WriteDebug($"  Completed in {stopwatch.Elapsed}");
            }
        }
Пример #5
0
        private static void WritePwDumpFile(string pwdumpPath, NtdsAudit ntdsAudit, DateTime baseDateTime, bool includeHistoryHashes, bool wordlistInUse, string dumpReversiblePath)
        {
            DomainInfo domain = null;

            // NTDS will only contain hashes for a single domain, even when NTDS was dumped from a global catalog server, ensure we only print hashes for that domain, and warn the user if there are other domains in NTDS
            if (ntdsAudit.Domains.Length > 1)
            {
                var usersWithHashes = ntdsAudit.Users.Where(x => x.LmHash != NtdsAudit.EMPTY_LM_HASH || x.NtHash != NtdsAudit.EMPTY_NT_HASH).ToList();
                domain = ntdsAudit.Domains.Single(x => x.Sid.Equals(usersWithHashes[0].DomainSid));

                ConsoleEx.WriteWarning($"WARNING:");
                ConsoleEx.WriteWarning($"The NTDS file has been retrieved from a global catalog (GC) server. Whilst GCs store information for other domains, they only store password hashes for their primary domain.");
                ConsoleEx.WriteWarning($"Password hashes have only been dumped for the \"{domain.Fqdn}\" domain.");
                ConsoleEx.WriteWarning($"If you require password hashes for other domains, please obtain the NTDS and SYSTEM files for each domain.");
                Console.WriteLine();
            }
            else
            {
                domain = ntdsAudit.Domains[0];
            }

            var users = ntdsAudit.Users.Where(x => domain.Sid.Equals(x.DomainSid)).ToArray();

            if (users.Any(x => !string.IsNullOrEmpty(x.ClearTextPassword)))
            {
                ConsoleEx.WriteWarning($"WARNING:");
                ConsoleEx.WriteWarning($"The NTDS file contains user accounts with passwords stored using reversible encryption. Use the --dump-reversible option to output these users and passwords.");
                Console.WriteLine();
            }

            var activeUsers                            = users.Where(x => !x.Disabled && (!x.Expires.HasValue || x.Expires.Value > baseDateTime)).ToArray();
            var activeUsersWithLMs                     = activeUsers.Where(x => !string.IsNullOrEmpty(x.LmHash) && x.LmHash != NtdsAudit.EMPTY_LM_HASH).ToArray();
            var activeUsersWithWeakPasswords           = activeUsers.Where(x => !string.IsNullOrEmpty(x.Password)).ToArray();
            var activeUsersWithDuplicatePasswordsCount = activeUsers.Where(x => x.NtHash != NtdsAudit.EMPTY_NT_HASH).GroupBy(x => x.NtHash).Where(g => g.Count() > 1).Sum(g => g.Count());
            var activeUsersWithPasswordStoredUsingReversibleEncryption = activeUsers.Where(x => !string.IsNullOrEmpty(x.ClearTextPassword)).ToArray();

            Console.WriteLine($"Password stats for: {domain.Fqdn}");
            WriteStatistic("Active users using LM hashing", activeUsersWithLMs.Length, activeUsers.Length);
            WriteStatistic("Active users with duplicate passwords", activeUsersWithDuplicatePasswordsCount, activeUsers.Length);
            WriteStatistic("Active users with password stored using reversible encryption", activeUsersWithPasswordStoredUsingReversibleEncryption.Length, activeUsers.Length);
            if (wordlistInUse)
            {
                WriteStatistic("Active user accounts with very weak passwords", activeUsersWithWeakPasswords.Length, activeUsers.Length);
            }

            Console.WriteLine();

            // <username>:<uid>:<LM-hash>:<NTLM-hash>:<comment>:<homedir>:
            using (var file = new StreamWriter(pwdumpPath, false))
            {
                for (var i = 0; i < users.Length; i++)
                {
                    var comments = $"Disabled={users[i].Disabled}," +
                                   $"Expired={!users[i].Disabled && users[i].Expires.HasValue && users[i].Expires.Value < baseDateTime}," +
                                   $"PasswordNeverExpires={users[i].PasswordNeverExpires}," +
                                   $"PasswordNotRequired={users[i].PasswordNotRequired}," +
                                   $"PasswordLastChanged={users[i].PasswordLastChanged.ToString("yyyyMMddHHmm")}," +
                                   $"LastLogonTimestamp={users[i].LastLogon.ToString("yyyyMMddHHmm")}," +
                                   $"IsAdministrator={users[i].RecursiveGroupSids.Contains(domain.AdministratorsSid)}," +
                                   $"IsDomainAdmin={users[i].RecursiveGroupSids.Contains(domain.DomainAdminsSid)}," +
                                   $"IsEnterpriseAdmin={users[i].RecursiveGroupSids.Intersect(ntdsAudit.Domains.Select(x => x.EnterpriseAdminsSid)).Any()}";
                    var homeDir = string.Empty;
                    file.Write($"{domain.Fqdn}\\{users[i].SamAccountName}:{users[i].Rid}:{users[i].LmHash}:{users[i].NtHash}:{comments}:{homeDir}:");

                    if (includeHistoryHashes && users[i].NtHistory != null && users[i].NtHistory.Length > 0)
                    {
                        file.Write(Environment.NewLine);
                    }
                    else if (i < users.Length - 1)
                    {
                        file.Write(Environment.NewLine);
                    }

                    if (includeHistoryHashes && users[i].NtHistory != null && users[i].NtHistory.Length > 0)
                    {
                        for (var j = 0; j < users[i].NtHistory.Length; j++)
                        {
                            var lmHash = (users[i].LmHistory?.Length > j) ? users[i].LmHistory[j] : NtdsAudit.EMPTY_LM_HASH;
                            file.Write($"{domain.Fqdn}\\{users[i].SamAccountName}__history_{j}:{users[i].Rid}:{lmHash}:{users[i].NtHistory[j]}:::");

                            if (j < users[i].NtHistory.Length || i < users.Length - 1)
                            {
                                file.Write(Environment.NewLine);
                            }
                        }
                    }
                }
            }

            if (users.Any(x => !string.IsNullOrEmpty(x.ClearTextPassword)) && !string.IsNullOrWhiteSpace(dumpReversiblePath))
            {
                using (var file = new StreamWriter(dumpReversiblePath, false))
                {
                    for (var i = 0; i < users.Length; i++)
                    {
                        if (!string.IsNullOrEmpty(users[i].ClearTextPassword))
                        {
                            file.Write($"{domain.Fqdn}\\{users[i].SamAccountName}:{users[i].ClearTextPassword}");

                            if (i < users.Length - 1)
                            {
                                file.Write(Environment.NewLine);
                            }
                        }
                    }
                }
            }
        }