예제 #1
0
        private static void WriteComputersCsvFile(string computersCsvPath, NtdsAudit ntdsAudit, DateTime baseDateTime)
        {
            using (var file = new StreamWriter(computersCsvPath, false))
            {
                var headerRow = new
                {
                    domain    = "Domain",
                    guid      = "GUID",
                    recordId  = "Record ID",
                    computer  = "Computer",
                    disabled  = "Disabled",
                    lastLogon = "Last Logon"
                };
                file.WriteLine(ToCsvRow(headerRow));
                foreach (var computer in ntdsAudit.Computers)
                {
                    var domain = ntdsAudit.Domains.Single(x => x.Sid == computer.DomainSid);
                    var csvRow = new
                    {
                        domain    = domain.Fqdn,
                        guid      = computer.Sid,
                        recordId  = computer.RecordId,
                        computer  = computer.Name,
                        disabled  = computer.Disabled,
                        lastLogon = computer.LastLogon
                    };

                    file.WriteLine(ToCsvRow(csvRow));
                }
            }
        }
예제 #2
0
        private static void PrintConsoleStatistics(NtdsAudit ntdsAudit, DateTime baseDateTime)
        {
            Console.WriteLine();
            Console.WriteLine($"The base date used for statistics is {baseDateTime}");
            Console.WriteLine();

            foreach (var domain in ntdsAudit.Domains)
            {
                Console.WriteLine($"Account stats for: {domain.Fqdn}");

                var users                                = ntdsAudit.Users.Where(x => x.DomainSid.Equals(domain.Sid)).ToList();
                var totalUsersCount                      = users.Count;
                var disabledUsersCount                   = users.Count(x => x.Disabled);
                var expiredUsersCount                    = users.Count(x => !x.Disabled && x.Expires.HasValue && x.Expires.Value < baseDateTime);
                var activeUsers                          = users.Where(x => !x.Disabled && (!x.Expires.HasValue || x.Expires.Value > baseDateTime)).ToList();
                var activeUsersCount                     = activeUsers.Count;
                var activeUsersUnusedIn1Year             = activeUsers.Count(x => x.LastLogon + TimeSpan.FromDays(365) < baseDateTime);
                var activeUsersUnusedIn90Days            = activeUsers.Count(x => x.LastLogon + TimeSpan.FromDays(90) < baseDateTime);
                var activeUsersWithPasswordNotRequired   = activeUsers.Count(x => x.PasswordNotRequired);
                var activeUsersWithPasswordNeverExpires  = activeUsers.Count(x => !x.PasswordNeverExpires);
                var activeUsersPasswordUnchangedIn1Year  = activeUsers.Count(x => x.PasswordLastChanged + TimeSpan.FromDays(365) < baseDateTime);
                var activeUsersPasswordUnchangedIn90Days = activeUsers.Count(x => x.PasswordLastChanged + TimeSpan.FromDays(90) < baseDateTime);

                var activeUsersWithAdministratorMembership = activeUsers.Where(x => x.RecursiveGroupSids.Contains(domain.AdministratorsSid)).ToArray();
                var activeUsersWithDomainAdminMembership   = activeUsers.Where(x => x.RecursiveGroupSids.Contains(domain.DomainAdminsSid)).ToArray();

                // Unlike Domain Admins and Adminsitrators, Enterprise Admins is not domain local, so include all users.
                var activeUsersWithEnterpriseAdminMembership = ntdsAudit.Users.Where(x => !x.Disabled && (!x.Expires.HasValue || x.Expires.Value > baseDateTime) && x.RecursiveGroupSids.Contains(domain.EnterpriseAdminsSid)).ToArray();

                WriteStatistic("Disabled users", disabledUsersCount, totalUsersCount);
                WriteStatistic("Expired users", expiredUsersCount, totalUsersCount);
                WriteStatistic("Active users unused in 1 year", activeUsersUnusedIn1Year, activeUsersCount);
                WriteStatistic("Active users unused in 90 days", activeUsersUnusedIn90Days, activeUsersCount);
                WriteStatistic("Active users which do not require a password", activeUsersWithPasswordNotRequired, activeUsersCount);
                WriteStatistic("Active users with non-expiring passwords", activeUsersWithPasswordNeverExpires, activeUsersCount);
                WriteStatistic("Active users with password unchanged in 1 year", activeUsersPasswordUnchangedIn1Year, activeUsersCount);
                WriteStatistic("Active users with password unchanged in 90 days", activeUsersPasswordUnchangedIn90Days, activeUsersCount);
                WriteStatistic("Active users with Administrator rights", activeUsersWithAdministratorMembership.Length, activeUsersCount);
                WriteStatistic("Active users with Domain Admin rights", activeUsersWithDomainAdminMembership.Length, activeUsersCount);
                WriteStatistic("Active users with Enterprise Admin rights", activeUsersWithEnterpriseAdminMembership.Length, activeUsersCount);
                Console.WriteLine();

                var computers                     = ntdsAudit.Computers.Where(x => x.DomainSid.Equals(domain.Sid)).ToList();
                var totalComputersCount           = computers.Count;
                var disabledComputersCount        = computers.Count(x => x.Disabled);
                var activeComputers               = computers.Where(x => !x.Disabled).ToList();
                var activeComputersCount          = activeComputers.Count;
                var activeComputersUnusedIn1Year  = activeComputers.Count(x => x.LastLogon + TimeSpan.FromDays(365) < baseDateTime);
                var activeComputersUnusedIn90Days = activeComputers.Count(x => x.LastLogon + TimeSpan.FromDays(90) < baseDateTime);

                WriteStatistic("Disabled computers", disabledComputersCount, totalComputersCount);
                WriteStatistic("Active computers unused in 1 year", activeComputersUnusedIn1Year, activeComputersCount);
                WriteStatistic("Active computers unused in 90 days", activeComputersUnusedIn90Days, activeComputersCount);
                Console.WriteLine();
            }
        }
예제 #3
0
 private static void WriteUsersCsvFile(string usersCsvPath, NtdsAudit ntdsAudit, DateTime baseDateTime)
 {
     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");
         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}");
         }
     }
 }
예제 #4
0
 private static void WriteComputersCsvFile(string computersCsvPath, NtdsAudit ntdsAudit, DateTime baseDateTime)
 {
     using (var file = new StreamWriter(computersCsvPath, false))
     {
         file.WriteLine("Domain,Computer,Disabled,Last Logon");
         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}");
         }
     }
 }
예제 #5
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 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 (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);
                    }
                }

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

            try
            {
                commandLineApplication.Execute(args);
            }
            catch (CommandParsingException ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
예제 #6
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);
                            }
                        }
                    }
                }
            }
        }
예제 #7
0
        private static void WriteUsersCsvFile(string usersCsvPath, NtdsAudit ntdsAudit, DateTime baseDateTime)
        {
            using (var file = new StreamWriter(usersCsvPath, false))
            {
                var headerRow = new
                {
                    domain               = "Domain",
                    guid                 = "GUID",
                    recordId             = "Record ID",
                    samAccountName       = "SAM account name",
                    memberOf             = "Member of",
                    userAccountControl   = "User Account Control",
                    username             = "******",
                    administrator        = "Administrator",
                    domainAdmin          = "Domain Admin",
                    enterpriseAdmin      = "Enterprise Admin",
                    disabled             = "Disabled",
                    expired              = "Expired",
                    passwordNeverExpires = "Password Never Expires",
                    passwordNotRequired  = "Password Not Required",
                    passwordLastSet      = "Password last set",
                    accountExpires       = "Account expires",
                    lastLogon            = "Last logon",
                    lastLogonTimestamp   = "Last logon timestamp",
                    whenCreated          = "When created",
                    whenChanged          = "When changed",
                };
                file.WriteLine(ToCsvRow(headerRow));
                foreach (var user in ntdsAudit.Users)
                {
                    var domain             = ntdsAudit.Domains.Single(x => x.Sid == user.DomainSid);
                    var guid               = user.DomainSid.AccountDomainSid?.Value;
                    var userAccountControl = user.UserAccountControlString;
                    //groups
                    foreach (var group in user.RecursiveGroups)
                    {
                        var groupName = group.Name;
                        var csvRow    = ToCsvRow(new
                        {
                            domainFqdn           = domain.Fqdn,
                            guid                 = user.Sid,
                            recordId             = user.Rid,
                            samAccountName       = user.SamAccountName,
                            memberOf             = groupName,
                            userAccountControl   = userAccountControl,
                            username             = user.SamAccountName,
                            administrator        = user.RecursiveGroupSids.Contains(domain.AdministratorsSid),
                            domainAdmin          = user.RecursiveGroupSids.Contains(domain.DomainAdminsSid),
                            enterpriseAdmin      = user.RecursiveGroupSids.Intersect(ntdsAudit.Domains.Select(x => x.EnterpriseAdminsSid)).Any(),
                            disabled             = user.Disabled,
                            expired              = !user.Disabled && user.Expires.HasValue && user.Expires.Value < baseDateTime,
                            passwordNeverExpires = user.PasswordNeverExpires,
                            passwordNotRequired  = user.PasswordNotRequired,
                            passwordLastChanged  = user.PasswordLastChanged,
                            accountExpires       = user.Expires,
                            lastLogon            = user.LastLogon,
                            lastLogonTimestamp   = user.LastLogon.ToFileTime(),
                            whenCreated          = "---",
                            whenChanged          = "---"
                        });
                        file.WriteLine(csvRow);
                    }

                    if (user.RecursiveGroupSids.Length == 0)
                    {
                        var groupName = "";
                        var csvRow    = ToCsvRow(new
                        {
                            domainFqdn           = domain.Fqdn,
                            guid                 = user.Sid,
                            recordId             = user.Rid,
                            samAccountName       = user.SamAccountName,
                            memberOf             = groupName,
                            userAccountControl   = userAccountControl,
                            username             = user.SamAccountName,
                            administrator        = user.RecursiveGroupSids.Contains(domain.AdministratorsSid),
                            domainAdmin          = user.RecursiveGroupSids.Contains(domain.DomainAdminsSid),
                            enterpriseAdmin      = user.RecursiveGroupSids.Intersect(ntdsAudit.Domains.Select(x => x.EnterpriseAdminsSid)).Any(),
                            disabled             = user.Disabled,
                            expired              = !user.Disabled && user.Expires.HasValue && user.Expires.Value < baseDateTime,
                            passwordNeverExpires = user.PasswordNeverExpires,
                            passwordNotRequired  = user.PasswordNotRequired,
                            passwordLastChanged  = user.PasswordLastChanged,
                            accountExpires       = user.Expires,
                            lastLogon            = user.LastLogon,
                            lastLogonTimestamp   = user.LastLogon.ToFileTime(),
                            whenCreated          = "---",
                            whenChanged          = "---"
                        });
                        file.WriteLine(csvRow);
                    }
                }
            }
        }
예제 #8
0
        private static void WriteUsersCsvFile(string usersCsvPath, NtdsAudit ntdsAudit, DateTime baseDateTime, string potFile)
        {
            int nbUsers = ntdsAudit.Users.Length;

            Console.WriteLine($"[*] Extracting {nbUsers} users...");
            var progress = new ProgressBar("Performing users extraction...");

            // Buffer des group
            Dictionary <string, string> groupAssoc = new Dictionary <string, string>();

            foreach (var group in ntdsAudit.Groups)
            {
                try {
                    groupAssoc.Add(group.Sid.ToString(), group.Name);
                }
                catch
                {
                    if (groupAssoc[group.Sid.ToString()] != group.Name)
                    {
                        Console.WriteLine($"[*] Double SID with different name ! {group.Sid.ToString()} => \"{group.Name}\" and \"{groupAssoc[group.Sid.ToString()]}\"");
                    }
                }
            }

            // Buffer des hash
            Console.WriteLine($"[*] Reading potfile potFile={potFile}");
            Dictionary <string, string> hashAssoc = new Dictionary <string, string>();

            if (File.Exists(potFile))
            {
                using (var file = new StreamReader(potFile))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        if (line.Length == 33)
                        {
                            hashAssoc.Add(line.Substring(0, 32), string.Empty);
                        }
                        else
                        {
                            try
                            {
                                hashAssoc.Add(line.Substring(0, 32).ToLower(), line.Substring(33));
                            }
                            catch
                            {
                                Console.WriteLine("Invalid hash size for " + line);
                            }
                        }
                    }
                }
            }
            else
            {
                Console.WriteLine("[!] No potfile not found or --potfile missing !");
            }

            var samePassword                    = new Dictionary <string, AssocPassHash>();
            int nbCompromised                   = 0;
            int nbCompromisedAndEnabled         = 0;
            int nbCompromisedAndEnabledAndAdmin = 0;
            int nbCompromisedAndAdmin           = 0;

            using (var file = new StreamWriter(usersCsvPath, false))
            {
                int i = 1;
                file.WriteLine("Sid,Domain,Username,PasswordFound,IsAdminWithLowPassword,isAdminInAnyGroup,Administrator,Domain Admin,Enterprise Admin,Disabled,Expired,Password Never Expires,Password Not Required,Password Last Changed,Last Logon,Hash,Password,PasswordLen,MemberOf,Count in group");
                foreach (var user in ntdsAudit.Users)
                {
                    var    domain = ntdsAudit.Domains.Single(x => x.Sid == user.DomainSid);
                    string password;
                    string hash        = user.NtHash.ToLower();
                    int    passwordLen = -1;
                    bool   isAdmin     = user.RecursiveGroupSids.Contains(domain.AdministratorsSid) || user.RecursiveGroupSids.Contains(domain.DomainAdminsSid) || user.RecursiveGroupSids.Intersect(ntdsAudit.Domains.Select(x => x.EnterpriseAdminsSid)).Any();
                    try
                    {
                        password    = hashAssoc[hash];
                        passwordLen = password.Length;
                        if (passwordLen == 0)
                        {
                            password = "******";
                        }
                        nbCompromised += 1;
                        if (user.Disabled != true)
                        {
                            nbCompromisedAndEnabled += 1;
                            if (isAdmin)
                            {
                                nbCompromisedAndEnabledAndAdmin += 1;
                            }
                        }
                        if (isAdmin)
                        {
                            nbCompromisedAndAdmin += 1;
                        }
                    }
                    catch (Exception e)
                    {
                        password    = "******";
                        passwordLen = -1;
                    }
                    bool isAdminWithLowPassword = isAdmin && passwordLen > -1;
                    bool isPasswordFound        = passwordLen > -1;
                    file.Write($"{user.DomainSid}-{user.Rid},{domain.Fqdn},{user.SamAccountName},{isPasswordFound},{isAdminWithLowPassword},{isAdmin},{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.LmHash}:{user.NtHash},\"{password}\",{passwordLen},\"");
                    foreach (SecurityIdentifier si in user.RecursiveGroupSids)
                    {
                        try
                        {
                            file.Write(groupAssoc[si.ToString()] + " / ");
                        }
                        catch
                        {
                            file.Write($"<unk {si.ToString()}> / ");
                        }
                    }
                    file.Write($"\",{user.RecursiveGroupSids.Length}\n");


                    if (!samePassword.ContainsKey(hash))
                    {
                        samePassword.Add(hash, new AssocPassHash(hash, password, passwordLen));
                    }
                    samePassword[hash].count += 1;

                    progress.Report((100 * i / nbUsers) / 100.0);
                    ++i;
                }
            }

            var stats = samePassword.OrderByDescending(x => x.Value.count);

            using (var file = new StreamWriter(usersCsvPath.Replace(".csv", string.Empty) + "-PasswordReuse.csv", false))
            {
                int i = 1;
                file.WriteLine("Hash,Password,Count");
                foreach (var h in stats)
                {
                    if (h.Value.count <= 1)
                    {
                        break;
                    }
                    file.WriteLine($"\"{h.Value.hash}\",\"{h.Value.pass.Replace("\"","\\\\\"")}\",{h.Value.count}");
                }
            }
            using (var file = new StreamWriter(usersCsvPath.Replace(".csv", string.Empty) + "-Stats.csv", false))
            {
                int nbAccount                            = ntdsAudit.Users.Count();
                var expiredUsersCount                    = ntdsAudit.Users.Count(x => !x.Disabled && x.Expires.HasValue && x.Expires.Value < baseDateTime);
                var activeUsers                          = ntdsAudit.Users.Where(x => !x.Disabled && (!x.Expires.HasValue || x.Expires.Value > baseDateTime)).ToList();
                var activeUsersCount                     = activeUsers.Count;
                var activeUsersUnusedIn1Year             = activeUsers.Count(x => x.LastLogon + TimeSpan.FromDays(365) < baseDateTime);
                var activeUsersUnusedIn90Days            = activeUsers.Count(x => x.LastLogon + TimeSpan.FromDays(90) < baseDateTime);
                var activeUsersWithPasswordNotRequired   = activeUsers.Count(x => x.PasswordNotRequired);
                var activeUsersWithPasswordNeverExpires  = activeUsers.Count(x => !x.PasswordNeverExpires);
                var activeUsersPasswordUnchangedIn1Year  = activeUsers.Count(x => x.PasswordLastChanged + TimeSpan.FromDays(365) < baseDateTime);
                var activeUsersPasswordUnchangedIn90Days = activeUsers.Count(x => x.PasswordLastChanged + TimeSpan.FromDays(90) < baseDateTime);
                file.WriteLine($"\"Number of account\",{nbAccount}");
                file.WriteLine($"\"Active account (Enabled and not password not expired)\",{activeUsersCount}");
                file.WriteLine(GetCSVStatistic("Disabled users", ntdsAudit.Users.Count(x => x.Disabled), nbAccount));
                file.WriteLine(GetCSVStatistic("Expired users", expiredUsersCount, nbAccount));
                file.WriteLine(GetCSVStatistic("Active users unused in 1 year", activeUsersUnusedIn1Year, activeUsersCount));
                file.WriteLine(GetCSVStatistic("Active users unused in 90 days", activeUsersUnusedIn90Days, activeUsersCount));
                file.WriteLine(GetCSVStatistic("Active users which do not require a password", activeUsersWithPasswordNotRequired, activeUsersCount));
                file.WriteLine(GetCSVStatistic("Active users with non-expiring passwords", activeUsersWithPasswordNeverExpires, activeUsersCount));
                file.WriteLine(GetCSVStatistic("Active users with password unchanged in 1 year", activeUsersPasswordUnchangedIn1Year, activeUsersCount));
                file.WriteLine(GetCSVStatistic("Active users with password unchanged in 90 days", activeUsersPasswordUnchangedIn90Days, activeUsersCount));

                foreach (var domain in ntdsAudit.Domains)
                {
                    var activeUsersWithAdministratorMembership = activeUsers.Where(x => x.RecursiveGroupSids.Contains(domain.AdministratorsSid)).ToArray();
                    var activeUsersWithDomainAdminMembership   = activeUsers.Where(x => x.RecursiveGroupSids.Contains(domain.DomainAdminsSid)).ToArray();
                    // Unlike Domain Admins and Adminsitrators, Enterprise Admins is not domain local, so include all users.
                    var activeUsersWithEnterpriseAdminMembership = ntdsAudit.Users.Where(x => !x.Disabled && (!x.Expires.HasValue || x.Expires.Value > baseDateTime) && x.RecursiveGroupSids.Contains(domain.EnterpriseAdminsSid)).ToArray();
                    file.WriteLine(GetCSVStatistic($"Active users with Administrator rights (domain={domain.Name})", activeUsersWithAdministratorMembership.Length, activeUsersCount));
                    file.WriteLine(GetCSVStatistic($"Active users with Domain Admin rights (domain={domain.Name})", activeUsersWithDomainAdminMembership.Length, activeUsersCount));
                    file.WriteLine(GetCSVStatistic($"Active users with Enterprise Admin rights (domain={domain.Name})", activeUsersWithEnterpriseAdminMembership.Length, activeUsersCount));
                }
                file.WriteLine($"\"Number of compromised accounts\",\"{nbCompromised} ({nbCompromised*100/ nbAccount}%)\"");
                file.WriteLine($"\"Number of compromised accounts with enabled flag\",\"{nbCompromisedAndEnabled} ({nbCompromisedAndEnabled * 100 / nbAccount}%)\"");
                file.WriteLine($"\"Number of compromised accounts with admin priviledge\",\"{nbCompromisedAndAdmin} ({nbCompromisedAndAdmin * 100 / nbAccount}%)\"");
                file.WriteLine($"\"Number of compromised accounts, enabled and admin\",\"{nbCompromisedAndEnabledAndAdmin} ({nbCompromisedAndEnabledAndAdmin * 100 / nbAccount}%)\"");
                file.WriteLine(string.Empty);
                for (int i = 0; i <= 30; ++i)
                {
                    file.WriteLine($"\"Passwword with a length of {i}\",{samePassword.Where(x => x.Value.len == i).Count()}");
                }
            }
            progress.Report(1);
        }