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}"); } }
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()); }
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); } }
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}"); } }
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); } } } } } }