Test-PasswordQuality and related enhancements

This commit is contained in:
MichaelGrafnetter 2016-07-15 18:10:55 +02:00
parent 8edb414f20
commit 5a02d20b73
18 changed files with 851 additions and 42 deletions

View File

@ -1,3 +1,11 @@
Version 2.16
- [Module] Added the Test-PasswordQuality and ConvertTo-NTHashSet cmdlets.
- [Module] Added support for the the UserAccountControl attribute of user accounts.
- [Framework] Added the ability to replicate user accounts by specifying their UPN.
- [Framework] Added the ability to calculate a NT hash from both String and SecureString.
- [Framework] Added the HashEqualityComparer, which allows the hashes to be stored
in the built-in generic collections.
Version 2.15
- Removed dependency on ADSI.
- Added support for the PAM optional feature.

View File

@ -0,0 +1,99 @@
namespace DSInternals.Common.Cryptography.Test
{
using DSInternals.Common;
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class HashEqualityComparerTester
{
private HashEqualityComparer comparer = HashEqualityComparer.GetInstance();
[TestMethod]
public void HashEqualityComparer_Equals_SameSize1()
{
byte[] vector1 = "92937945B518814341DE3F726500D4FF".HexToBinary();
byte[] vector2 = (byte[]) vector1.Clone();
bool result = comparer.Equals(vector1, vector2);
Assert.AreEqual(true, result);
}
[TestMethod]
public void HashEqualityComparer_Equals_SameSize2()
{
byte[] vector1 = "92937945B518814341DE3F726500D4FF".HexToBinary();
byte[] vector2 = "92937945B518814341DE3F726500D4F0".HexToBinary();
bool result = comparer.Equals(vector1, vector2);
Assert.AreEqual(false, result);
}
[TestMethod]
public void HashEqualityComparer_Equals_DifferentSize()
{
byte[] vector1 = "92937945B518814341DE3F726500D4FF".HexToBinary();
byte[] vector2 = "92937945B518814341DE3F726500D4".HexToBinary();
bool result = comparer.Equals(vector1, vector2);
Assert.AreEqual(false, result);
}
[TestMethod]
public void HashEqualityComparer_Equals_Null1()
{
bool result = comparer.Equals(null, new byte[0]);
Assert.AreEqual(false, result);
}
[TestMethod]
public void HashEqualityComparer_Equals_Null2()
{
bool result = comparer.Equals(new byte[0], null);
Assert.AreEqual(false, result);
}
[TestMethod]
public void HashEqualityComparer_Equals_NullBoth()
{
bool result = comparer.Equals(null, null);
Assert.AreEqual(true, result);
}
[TestMethod]
public void HashEqualityComparer_GetHashCode_Vector1()
{
var hash = "92937945B518814341DE3F726500D4FF".HexToBinary();
Assert.AreEqual(1165595538, comparer.GetHashCode(hash));
}
[TestMethod]
public void HashEqualityComparer_GetHashCode_Short1()
{
var hash = "A0".HexToBinary();
Assert.AreEqual(160, comparer.GetHashCode(hash));
}
[TestMethod]
public void HashEqualityComparer_GetHashCode_Short2()
{
var hash = "A0B2".HexToBinary();
Assert.AreEqual(-19808, comparer.GetHashCode(hash));
}
[TestMethod]
public void HashEqualityComparer_GetHashCode_Short3()
{
var hash = "A0B2C3".HexToBinary();
Assert.AreEqual(-19808, comparer.GetHashCode(hash));
}
[TestMethod]
public void HashEqualityComparer_GetHashCode_Empty()
{
Assert.AreEqual(0, this.comparer.GetHashCode(new byte[0]));
}
[TestMethod]
public void HashEqualityComparer_GetHashCode_Null()
{
Assert.AreEqual(0, this.comparer.GetHashCode(null));
}
}
}

View File

@ -9,33 +9,59 @@ namespace DSInternals.Common.Cryptography.Test
public class NTHashTester
{
[TestMethod]
public void NTHash_EmptyInput()
public void NTHash_SecureString_EmptyInput()
{
SecureString password = string.Empty.ToSecureString();
string result = NTHash.ComputeHash(password).ToHex(true);
string expected = "31D6CFE0D16AE931B73C59D7E0C089C0";
Assert.AreEqual(expected, result);
}
[TestMethod]
public void NTHash_String_EmptyInput()
{
string result = NTHash.ComputeHash(string.Empty).ToHex(true);
string expected = "31D6CFE0D16AE931B73C59D7E0C089C0";
Assert.AreEqual(expected, result);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void NTHash_NullInput()
public void NTHash_SecureString_NullInput()
{
NTHash.ComputeHash(null);
NTHash.ComputeHash((SecureString)null);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void NTHash_String_NullInput()
{
NTHash.ComputeHash((string)null);
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void NTHash_LongInput()
public void NTHash_SecureString_LongInput()
{
SecureString password = "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789".ToSecureString();
string result = NTHash.ComputeHash(password).ToHex(true);
}
[TestMethod]
public void NTHash_TestVector1()
public void NTHash_SecureString_TestVector1()
{
SecureString password = "Pa$$w0rd".ToSecureString();
string result = NTHash.ComputeHash(password).ToHex(true);
string expected = "92937945B518814341DE3F726500D4FF";
Assert.AreEqual(expected, result);
}
[TestMethod]
public void NTHash_String_TestVector1()
{
string result = NTHash.ComputeHash("Pa$$w0rd").ToHex(true);
string expected = "92937945B518814341DE3F726500D4FF";
Assert.AreEqual(expected, result);
}
}
}

View File

@ -58,6 +58,7 @@
<Compile Include="Cryptography\LMHashTester.cs" />
<Compile Include="Cryptography\NTHashTester.cs" />
<Compile Include="Cryptography\OrgIdHashTester.cs" />
<Compile Include="Cryptography\HashEqualityComparerTester.cs" />
<Compile Include="DistinguishedNameTester.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Cryptography\SecureStringExtensionsTester.cs" />

View File

@ -0,0 +1,59 @@
namespace DSInternals.Common.Cryptography
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
public class HashEqualityComparer : IEqualityComparer<byte[]>
{
// Singleton
private static HashEqualityComparer instance;
public static HashEqualityComparer GetInstance()
{
if(instance == null)
{
instance = new HashEqualityComparer();
}
return instance;
}
private HashEqualityComparer() {}
public bool Equals(byte[] x, byte[] y)
{
if (x == null || y == null)
{
return x == y;
}
if(x.LongLength != y.LongLength)
{
return false;
}
return x.SequenceEqual(y);
}
public int GetHashCode(byte[] obj)
{
if(obj == null || obj.LongLength == 0)
{
return 0;
}
if(obj.LongLength >= sizeof(int))
{
return BitConverter.ToInt32(obj, 0);
}
else if(obj.LongLength >= sizeof(short))
{
// Length == 2 || Length == 3
return BitConverter.ToInt16(obj, 0);
}
else
{
// Length == 1, so we return the value of the only byte.
return obj[0];
}
}
}
}

View File

@ -16,27 +16,6 @@ namespace DSInternals.Common.Cryptography
public const int MaxChars = NativeMethods.LMPasswordMaxChars;
//public static byte[] ComputeHash(SecureString password)
//{
// Validator.AssertNotNull(password, "password");
// Validator.AssertMaxLength(password, MaxChars, "password");
// int oemPwdBufferLength = Encoding.ASCII.GetMaxByteCount(MaxChars);
// byte[] hash;
// using (SafeOemStringPointer oemPwdBuffer = SafeOemStringPointer.Allocate(oemPwdBufferLength))
// {
// using(SafeUnicodeSecureStringPointer unicodePwdBuffer = new SafeUnicodeSecureStringPointer(password))
// {
// NtStatus result1 = NativeMethods.RtlUpcaseUnicodeToOemN(oemPwdBuffer, (uint)oemPwdBufferLength, unicodePwdBuffer);
// Validator.AssertSuccess(result1);
// }
// NtStatus result2 = NativeMethods.RtlCalculateLmOwfPassword(oemPwdBuffer, out hash);
// Validator.AssertSuccess(result2);
// }
// return hash;
//}
public static byte[] ComputeHash(SecureString password)
{
Validator.AssertNotNull(password, "password");

View File

@ -13,6 +13,17 @@ namespace DSInternals.Common.Cryptography
public const int HashSize = NativeMethods.NTHashNumBytes;
public const int MaxInputLength = NativeMethods.NTPasswordMaxChars;
/// <summary>
/// Gets the NT hash of an empty password.
/// </summary>
public static byte[] Empty
{
get
{
return ComputeHash(string.Empty);
}
}
public static byte[] ComputeHash(SecureString password)
{
Validator.AssertNotNull(password, "password");
@ -24,5 +35,14 @@ namespace DSInternals.Common.Cryptography
}
return hash;
}
public static byte[] ComputeHash(string password)
{
Validator.AssertNotNull(password, "password");
byte[] hash;
NtStatus result = NativeMethods.RtlCalculateNtOwfPassword(password, out hash);
Validator.AssertSuccess(result);
return hash;
}
}
}

View File

@ -38,6 +38,7 @@
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="Cryptography\HashEqualityComparer.cs" />
<Compile Include="Cryptography\Pbkdf2.cs" />
<Compile Include="Extensions\ByteArrayExtensions.cs" />
<Compile Include="Cryptography\Crc32.cs" />

View File

@ -1,9 +1,9 @@
namespace DSInternals.Common.Data
{
using DSInternals.Common.Cryptography;
using System;
using System.Security.AccessControl;
using System.Security.Principal;
using System;
using System.Security.AccessControl;
using System.Security.Principal;
public class DSAccount
{
@ -59,12 +59,10 @@ using System.Security.Principal;
// AdminCount (Although the schema defines it as Int32, it can only have values 0 and 1, so we directly convert it to bool)
dsObject.ReadAttribute(CommonDirectoryAttributes.AdminCount, out this.adminCount);
// Enabled:
// TODO: Move to DirectoryObject?
// UAC:
int? numericUac;
dsObject.ReadAttribute(CommonDirectoryAttributes.UserAccountControl, out numericUac);
UserAccountControl uac = (UserAccountControl)numericUac.Value;
this.Enabled = !uac.HasFlag(UserAccountControl.Disabled);
this.UserAccountControl = (UserAccountControl)numericUac.Value;
// Deleted:
dsObject.ReadAttribute(CommonDirectoryAttributes.IsDeleted, out this.isDeleted);
@ -236,10 +234,25 @@ using System.Security.Principal;
/// <c>true</c> if enabled; otherwise, <c>false</c>.
/// </value>
public bool Enabled
{
get
{
return !this.UserAccountControl.HasFlag(UserAccountControl.Disabled);
}
}
/// <summary>
/// Gets the flags that control the behavior of the user account.
/// </summary>
/// <value>
/// The value can be zero or a combination of one or more flags.
/// </value>
public UserAccountControl UserAccountControl
{
get;
private set;
}
/// <summary>
/// Gets a boolean value indicating whether this <see cref="DSAccount"/> is deleted.
/// </summary>

View File

@ -45,6 +45,17 @@ namespace DSInternals.Common.Interop
/// <see>https://github.com/wine-mirror/wine/blob/master/dlls/advapi32/crypt_md4.c</see>
[DllImport(Advapi, SetLastError = true, EntryPoint = NTOwfInternalName, CharSet = CharSet.Unicode)]
private static extern NtStatus RtlCalculateNtOwfPassword([In] ref SecureUnicodeString password, [MarshalAs(UnmanagedType.LPArray, SizeConst = NTHashNumBytes), In, Out] byte[] hash);
/// <summary>
/// Takes the passed NtPassword and performs a one-way-function on it.
/// Uses the RSA MD4 function
/// </summary>
/// <param name="password">The password to perform the one-way-function on. </param>
/// <param name="hash">The hashed password is returned here.</param>
/// <returns>STATUS_SUCCESS - The function was completed successfully. The hashed password is in hash.</returns>
/// <see>https://github.com/wine-mirror/wine/blob/master/dlls/advapi32/crypt_md4.c</see>
[DllImport(Advapi, SetLastError = true, EntryPoint = NTOwfInternalName, CharSet = CharSet.Unicode)]
private static extern NtStatus RtlCalculateNtOwfPassword([In] ref UnicodeString password, [MarshalAs(UnmanagedType.LPArray, SizeConst = NTHashNumBytes), In, Out] byte[] hash);
internal static NtStatus RtlCalculateNtOwfPassword(SafeUnicodeSecureStringPointer password, out byte[] hash)
{
@ -53,6 +64,15 @@ namespace DSInternals.Common.Interop
hash = new byte[NTHashNumBytes];
return RtlCalculateNtOwfPassword(ref unicodePassword, hash);
}
internal static NtStatus RtlCalculateNtOwfPassword(string password, out byte[] hash)
{
UnicodeString unicodePassword = new UnicodeString(password);
// Allocate output buffer
hash = new byte[NTHashNumBytes];
return RtlCalculateNtOwfPassword(ref unicodePassword, hash);
}
/// <summary>
/// Takes the passed password and performs a one-way-function on it.
/// The current implementation does this by using the password as a key to encrypt a known block of text.

View File

@ -0,0 +1,86 @@
namespace DSInternals.PowerShell.Commands
{
using DSInternals.Common.Cryptography;
using DSInternals.Common.Interop;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Management.Automation;
[Cmdlet(VerbsData.ConvertTo, "NTHashDictionary")]
[OutputType(new Type[] { typeof(IDictionary<byte[], string>) })]
public class ConvertToNTHashDictionaryCommand : PSCmdlet
{
#region Parameters
// HACK: This parameter is not called assword to pass the PSScriptAnalyzer tests. The purpose of this command is to calculate the hashes of password lists stored in text files, so there is no need to protect them by SecureStrings.
[Parameter(
Mandatory = true,
ValueFromPipeline = true,
Position = 0
)]
[Alias("Password")]
[ValidateNotNullOrEmpty]
public string[] Input
{
get;
set;
}
#endregion Parameters
#region Fields
private IDictionary<byte[], string> hashDictionary;
#endregion Fields
#region Cmdlet Overrides
protected override void BeginProcessing()
{
this.hashDictionary = new Dictionary<byte[], string>(HashEqualityComparer.GetInstance());
}
protected override void ProcessRecord()
{
foreach(string password in this.Input)
{
if(string.IsNullOrEmpty(password))
{
// Skip empty lines from the input.
continue;
}
try
{
byte[] hash = NTHash.ComputeHash(password);
if(!this.hashDictionary.ContainsKey(hash))
{
// Do not try to add duplicate hashes, because the Add method would throw an ArgumentException.
this.hashDictionary.Add(hash, password);
}
}
catch (ArgumentException ex)
{
ErrorRecord error = new ErrorRecord(ex, "Error1", ErrorCategory.InvalidArgument, password);
this.WriteError(error);
}
catch (Win32Exception ex)
{
ErrorCategory category = ((Win32ErrorCode)ex.NativeErrorCode).ToPSCategory();
ErrorRecord error = new ErrorRecord(ex, "Error2", category, password);
this.WriteError(error);
}
catch (Exception ex)
{
ErrorRecord error = new ErrorRecord(ex, "Error3", ErrorCategory.NotSpecified, password);
this.WriteError(error);
}
}
}
protected override void EndProcessing()
{
this.WriteObject(this.hashDictionary, false);
}
#endregion Cmdlet Overrides
}
}

View File

@ -0,0 +1,256 @@
namespace DSInternals.PowerShell.Commands
{
using System;
using System.Linq;
using System.Management.Automation;
using DSInternals.Common.Data;
using System.Collections.Generic;
using System.Collections.Specialized;
using DSInternals.Common.Cryptography;
[Cmdlet(VerbsDiagnostic.Test, "PasswordQuality")]
[OutputType(new Type[] { typeof(PasswordQualityTestResult) })]
public class TestPasswordQualityCommand : PSCmdlet
{
#region Constants
/// <summary>
/// Expected number of users being processed
/// </summary>
private const int PasswordDictionaryInitialCapacity = 10000;
#endregion Constants
#region Parameters
[Parameter(
Mandatory = true,
Position = 0,
ValueFromPipeline = true
)]
[Alias("ADAccount","DSAccount")]
public DSAccount Account
{
get;
set;
}
[Parameter]
public SwitchParameter SkipDuplicatePasswordTest
{
get;
set;
}
[Parameter]
public SwitchParameter IncludeDisabledAccounts
{
get;
set;
}
[Parameter]
[ValidateNotNull]
public IDictionary<byte[], string> WeakPasswordHashes
{
get;
set;
}
#endregion Parameters
#region Fields
// Maps password hash to list of user names.
private IDictionary<byte[], StringCollection> duplicatePasswordDictionary;
private PasswordQualityTestResult result;
#endregion Fields
#region Cmdlet Overrides
protected override void BeginProcessing()
{
// Perform some initialization.
if(! this.SkipDuplicatePasswordTest.IsPresent)
{
this.duplicatePasswordDictionary = new Dictionary<byte[], StringCollection>(PasswordDictionaryInitialCapacity, HashEqualityComparer.GetInstance());
}
this.result = new PasswordQualityTestResult();
}
protected override void ProcessRecord()
{
if(this.Account.Enabled == false && !this.IncludeDisabledAccounts.IsPresent)
{
// The account is disabled and should be skipped.
// TODO: Move to resources.
string message = String.Format("Skipping account {0}, because it is disabled.", this.Account.SamAccountName);
this.WriteVerbose(message);
return;
}
// Verbose message
// TODO: Move to resources.
string message2 = String.Format("Processing account {0}...", this.Account.SamAccountName);
this.WriteVerbose(message2);
if (this.Account.UserAccountControl.HasFlag(UserAccountControl.PasswordNeverExpires))
{
// The account has a non-expiring password.
this.result.PasswordNeverExpires.Add(this.Account.SamAccountName);
}
if(this.Account.UserAccountControl.HasFlag(UserAccountControl.UseDesKeyOnly))
{
// Only DES kerbero encryption type is used with this account.
this.result.DESEncryptionOnly.Add(this.Account.SamAccountName);
}
if(this.Account.AdminCount && ! this.Account.UserAccountControl.HasFlag(UserAccountControl.NotDelegated))
{
// This administrative account can be delegated.
this.result.DelegatableAdmins.Add(this.Account.SamAccountName);
}
if (this.Account.UserAccountControl.HasFlag(UserAccountControl.PasswordNotRequired))
{
// The account's password is not required.
this.result.PasswordNotRequired.Add(this.Account.SamAccountName);
}
if (this.Account.UserAccountControl.HasFlag(UserAccountControl.PreAuthNotRequired))
{
// Pre-authentication is not required for this account account.
this.result.PreAuthNotRequired.Add(this.Account.SamAccountName);
}
if(this.Account.SupplementalCredentials != null)
{
if(this.Account.SupplementalCredentials.ClearText != null)
{
// Account has ClearText password (stored using reversible encryption)
this.result.ClearTextPassword.Add(this.Account.SamAccountName);
}
if(this.Account.SupplementalCredentials.KerberosNew == null)
{
// Account is missing the AES kerberos keys.
this.result.AESKeysMissing.Add(this.Account.SamAccountName);
}
}
if(this.Account.LMHash != null)
{
// Account has the LM hash present.
this.result.LMHash.Add(this.Account.SamAccountName);
}
if (this.Account.NTHash == null)
{
// The account has no password.
this.result.EmptyPassword.Add(this.Account.SamAccountName);
// All the remaining tests are based on NT hash, so we can skip them.
return;
}
if (HashEqualityComparer.GetInstance().Equals(this.Account.NTHash, NTHash.Empty))
{
// The account has an empty password.
this.result.EmptyPassword.Add(this.Account.SamAccountName);
// Skip the remaining tests, because they only make sense for non-empty passwords.
return;
}
if (this.Account.SamAccountType == SamAccountType.Computer)
{
// Check if the computer has a default password.
this.TestComputerDefaultPassword();
}
if (this.WeakPasswordHashes != null)
{
// Check if the account has a weak password.
this.TestWeakHashes();
}
if (!this.SkipDuplicatePasswordTest)
{
// Find password duplicates
this.TestDuplicateHash();
}
}
protected override void EndProcessing()
{
// Process duplicate passwords
this.result.DuplicatePasswordGroups = this.duplicatePasswordDictionary.Values.Where(list => list.Count > 1).ToList();
// The processing has finished, so return the results.
this.WriteObject(this.result);
}
#endregion Cmdlet Overrides
#region Helper Methods
private void TestWeakHashes()
{
// Check the current hash
string foundPassword;
bool isInDictionary = this.WeakPasswordHashes.TryGetValue(this.Account.NTHash, out foundPassword);
if(isInDictionary)
{
// The current password hash is on the list
this.result.WeakPassword.Add(this.Account.SamAccountName, foundPassword);
}
if (this.Account.NTHashHistory == null || this.Account.NTHashHistory.Length <= 1)
{
// The account does not contain any historical password hashes, so we skip the remaining tests.
return;
}
// The first hash in history is the current one, so we skip it.
var historicalHashes = this.Account.NTHashHistory.Skip(1);
foreach(byte[] hash in historicalHashes)
{
isInDictionary = this.WeakPasswordHashes.TryGetValue(hash, out foundPassword);
if(isInDictionary)
{
// A historical password is on the list
this.result.WeakHistoricalPassword.Add(this.Account.SamAccountName, foundPassword);
// We already found one matching hash, so we skip the rest of the hashes.
break;
}
}
}
private void TestDuplicateHash()
{
byte[] currentHash = this.Account.NTHash;
StringCollection accountList;
bool hashFoundInDictionary = this.duplicatePasswordDictionary.TryGetValue(currentHash, out accountList);
if(!hashFoundInDictionary)
{
// Create a new account list for the hash, as it does not exist yet.
accountList = new StringCollection();
this.duplicatePasswordDictionary.Add(currentHash, accountList);
}
accountList.Add(this.Account.SamAccountName);
}
private void TestComputerDefaultPassword()
{
string defaultPassword = this.Account.SamAccountName.TrimEnd('$').ToLower();
byte[] defaultHash = NTHash.ComputeHash(defaultPassword);
if (HashEqualityComparer.GetInstance().Equals(this.Account.NTHash, defaultHash))
{
// The computer has the default password.
this.result.DefaultComputerPassword.Add(this.Account.SamAccountName);
}
}
#endregion Helper Methods
}
}

View File

@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
<Controls>
<Control>
<Name>AccountList</Name>
<CustomControl>
<CustomEntries>
<CustomEntry>
<CustomItem>
<Frame>
<LeftIndent>2</LeftIndent>
<CustomItem>
<ExpressionBinding>
<EnumerateCollection />
<ScriptBlock>$PSItem | Sort-Object | ForEach-Object { "$PSItem`n" }</ScriptBlock>
</ExpressionBinding>
<NewLine />
</CustomItem>
</Frame>
</CustomItem>
</CustomEntry>
</CustomEntries>
</CustomControl>
</Control>
<Control>
<Name>AccountGroupList</Name>
<CustomControl>
<CustomEntries>
<CustomEntry>
<CustomItem>
<Frame>
<LeftIndent>2</LeftIndent>
<CustomItem>
<ExpressionBinding>
<EnumerateCollection />
<ScriptBlock>$PSItem | ForEach-Object -Begin { [int] $i = 1 } -Process { "Group {0}:`n" -f $i; $PSItem | Sort-Object | ForEach-Object { " $PSItem`n" }; $i++ }</ScriptBlock>
</ExpressionBinding>
<NewLine />
</CustomItem>
</Frame>
</CustomItem>
</CustomEntry>
</CustomEntries>
</CustomControl>
</Control>
<Control>
<Name>PasswordDictionary</Name>
<CustomControl>
<CustomEntries>
<CustomEntry>
<CustomItem>
<Frame>
<LeftIndent>2</LeftIndent>
<CustomItem>
<ExpressionBinding>
<EnumerateCollection />
<ScriptBlock>$PSItem | Sort-Object -Property Name | ForEach-Object { "{0,-20} {1}`n" -f $PSItem.Name,$PSItem.Value }</ScriptBlock>
</ExpressionBinding>
<NewLine />
</CustomItem>
</Frame>
</CustomItem>
</CustomEntry>
</CustomEntries>
</CustomControl>
</Control>
</Controls>
<ViewDefinitions>
<View>
<Name>PasswordQualityTestResult</Name>
<ViewSelectedBy>
<TypeName>DSInternals.PowerShell.PasswordQualityTestResult</TypeName>
</ViewSelectedBy>
<CustomControl>
<CustomEntries>
<CustomEntry>
<CustomItem>
<Text>Active Directory Password Quality Report</Text>
<NewLine />
<Text>----------------------------------------</Text>
<NewLine />
<NewLine />
<Text>Passwords of these accounts are stored using reversible encryption:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>ClearTextPassword</PropertyName>
<CustomControlName>AccountList</CustomControlName>
</ExpressionBinding>
<Text>LM hashes of passwords of these accounts are present:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>LMHash</PropertyName>
<CustomControlName>AccountList</CustomControlName>
</ExpressionBinding>
<Text>These accounts have no password set:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>EmptyPassword</PropertyName>
<CustomControlName>AccountList</CustomControlName>
</ExpressionBinding>
<Text>Passwords of these accounts have been found in the dictionary:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>WeakPassword</PropertyName>
<CustomControlName>PasswordDictionary</CustomControlName>
</ExpressionBinding>
<Text>Historical passwords of these accounts have been found in the dictionary:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>WeakHistoricalPassword</PropertyName>
<CustomControlName>PasswordDictionary</CustomControlName>
</ExpressionBinding>
<Text>These groups of accounts have the same passwords:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>DuplicatePasswordGroups</PropertyName>
<CustomControlName>AccountGroupList</CustomControlName>
</ExpressionBinding>
<Text>These computer accounts have default passwords:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>DefaultComputerPassword</PropertyName>
<CustomControlName>AccountList</CustomControlName>
</ExpressionBinding>
<Text>Kerberos AES keys are missing from these accounts:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>AESKeysMissing</PropertyName>
<CustomControlName>AccountList</CustomControlName>
</ExpressionBinding>
<Text>Kerberos pre-authentication is not required for these accounts:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>PreauthNotRequired</PropertyName>
<CustomControlName>AccountList</CustomControlName>
</ExpressionBinding>
<Text>Only DES encryption is allowed to be used with these accounts:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>DESEncryptionOnly</PropertyName>
<CustomControlName>AccountList</CustomControlName>
</ExpressionBinding>
<Text>These administrative accounts are allowed to be delegated to a service:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>DelegatableAdmins</PropertyName>
<CustomControlName>AccountList</CustomControlName>
</ExpressionBinding>
<Text>Passwords of these accounts will never expire:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>PasswordNeverExpires</PropertyName>
<CustomControlName>AccountList</CustomControlName>
</ExpressionBinding>
<Text>These accounts are not required to have a password:</Text>
<NewLine />
<ExpressionBinding>
<PropertyName>PasswordNotRequired</PropertyName>
<CustomControlName>AccountList</CustomControlName>
</ExpressionBinding>
</CustomItem>
</CustomEntry>
</CustomEntries>
</CustomControl>
</View>
</ViewDefinitions>
</Configuration>

View File

@ -51,7 +51,9 @@
<Compile Include="Commands\Base\ADReplObjectCommandBase.cs" />
<Compile Include="Commands\Base\ADReplCommandBase.cs" />
<Compile Include="Commands\Base\PSCmdletEx.cs" />
<Compile Include="Commands\Hash\ConvertToNTHashDictionaryCommand.cs" />
<Compile Include="Commands\Misc\ConvertFromADManagedPasswordBlobCommand.cs" />
<Compile Include="Commands\Misc\TestPasswordQualityCommand.cs" />
<Compile Include="Commands\Misc\ConvertToHexCommand.cs" />
<Compile Include="Commands\Datastore\SetADDBBootKeyCommand.cs" />
<Compile Include="Commands\Datastore\GetADDBBackupKeyCommand.cs" />
@ -80,6 +82,7 @@
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Types\DomainController.cs" />
<Compile Include="Types\PasswordQualityTestResult.cs" />
<Compile Include="Types\SchemaAttribute.cs" />
<Compile Include="Utils\ValidateHexStringAttribute.cs" />
<Compile Include="Utils\ValidatePasswordLengthAttribute.cs" />
@ -140,6 +143,10 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Include="ADDBCommandHierarchy.cd" />
<None Include="DSInternals.PasswordQualityTestResult.format.ps1xml">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="DSInternals.psd1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
@ -147,7 +154,7 @@
<SubType>Designer</SubType>
</None>
<None Include="Run-Cmdlets.ps1" />
<None Include="DSInternals.format.ps1xml">
<None Include="DSInternals.DSAccount.format.ps1xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<SubType>Designer</SubType>
</None>

View File

@ -56,7 +56,8 @@ ProcessorArchitecture = 'None'
# TypesToProcess = @()
# Format files (.ps1xml) to be loaded when importing this module
FormatsToProcess = 'DSInternals.format.ps1xml'
FormatsToProcess = 'DSInternals.DSAccount.format.ps1xml',
'DSInternals.PasswordQualityTestResult.format.ps1xml'
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
NestedModules = @('DSInternals.PowerShell.dll')
@ -75,7 +76,7 @@ CmdletsToExport = 'ConvertTo-NTHash', 'ConvertTo-LMHash', 'Set-SamAccountPasswor
'Get-ADReplAccount', 'ConvertTo-Hex',
'ConvertFrom-ADManagedPasswordBlob',
'Get-ADDBBackupKey', 'Get-ADReplBackupKey', 'Save-DPAPIBlob',
'Set-ADDBBootKey'
'Set-ADDBBootKey','ConvertTo-NTHashDictionary', 'Test-PasswordQuality'
# Variables to export from this module
# VariablesToExport = @()
@ -84,7 +85,9 @@ CmdletsToExport = 'ConvertTo-NTHash', 'ConvertTo-LMHash', 'Set-SamAccountPasswor
AliasesToExport = 'Set-WinUserPasswordHash', 'Set-ADAccountPasswordHash',
'ConvertFrom-UnattendXmlPassword', 'ConvertTo-AADHash',
'ConvertTo-MsoPasswordHash', 'Get-ADReplicationAccount',
'ConvertFrom-ManagedPasswordBlob', 'Get-SysKey', 'Set-ADDBSysKey'
'ConvertFrom-ManagedPasswordBlob', 'Get-SysKey', 'Set-ADDBSysKey',
'New-NTHashSet', 'Test-ADPasswordQuality',
'Test-ADDBPasswordQuality', 'Test-ADReplPasswordQuality'
# DSC resources to export from this module
# DscResourcesToExport = @()
@ -114,11 +117,7 @@ PrivateData = @{
# ReleaseNotes of this module
ReleaseNotes = @"
- Removed dependency on ADSI.
- Added support for the PAM optional feature.
- Added the PWDump custom view.
- Added the HashNT custom view.
- Added the HashLM custom view.
- Added the Test-PasswordQuality and ConvertTo-NTHashDictionary cmdlets.
"@
} # End of PSData hashtable

View File

@ -26,6 +26,10 @@ New-Alias -Name Get-ADReplicationAccount -Value Get-ADReplAccount
New-Alias -Name ConvertFrom-ManagedPasswordBlob -Value ConvertFrom-ADManagedPasswordBlob
New-Alias -Name Get-SysKey -Value Get-BootKey
New-Alias -Name Set-ADDBSysKey -Value Set-ADDBBootKey
New-Alias -Name New-NTHashDictionary -Value ConvertTo-NTHashDictionary
New-Alias -Name Test-ADPasswordQuality -Value Test-PasswordQuality
New-Alias -Name Test-ADDBPasswordQuality -Value Test-PasswordQuality
New-Alias -Name Test-ADReplPasswordQuality -Value Test-PasswordQuality
# Export the aliases
Export-ModuleMember -Alias * -Cmdlet *

View File

@ -0,0 +1,64 @@
namespace DSInternals.PowerShell
{
using System.Collections.Generic;
using System.Collections.Specialized;
/// <summary>
/// Contains results of Active Directory password quality analysis.
/// </summary>
public class PasswordQualityTestResult
{
/// <summary>
/// List of accounts whose passwords are stored using reversible encryption.
/// </summary>
public StringCollection ClearTextPassword = new StringCollection();
/// <summary>
/// List of accounts whose LM hashes are stored in the database.
/// </summary>
public StringCollection LMHash = new StringCollection();
/// <summary>
/// List of accounts that have no password set.
/// </summary>
public StringCollection EmptyPassword = new StringCollection();
/// <summary>
/// List of accounts that have a weak password.
/// </summary>
public StringDictionary WeakPassword = new StringDictionary();
/// <summary>
/// List of accounts that had a weak password.
/// </summary>
public StringDictionary WeakHistoricalPassword = new StringDictionary();
/// <summary>
/// List of computer accounts with default passwords.
/// </summary>
public StringCollection DefaultComputerPassword = new StringCollection();
/// <summary>
/// List of accounts that do not require a password.
/// </summary>
public StringCollection PasswordNotRequired = new StringCollection();
/// <summary>
/// List of accounts whose passwords never expire.
/// </summary>
public StringCollection PasswordNeverExpires = new StringCollection();
/// <summary>
/// List of accounts that are missing AES keys.
/// </summary>
public StringCollection AESKeysMissing = new StringCollection();
/// <summary>
/// List of accounts on which preauthentication is not enforced.
/// </summary>
public StringCollection PreAuthNotRequired = new StringCollection();
/// <summary>
/// List of accounts that can only be authenticated using DES.
/// </summary>
public StringCollection DESEncryptionOnly = new StringCollection();
/// <summary>
/// List of administrative accounts that can be delegated.
/// </summary>
public StringCollection DelegatableAdmins = new StringCollection();
/// <summary>
/// List of collections of accounts with the same password hashes.
/// </summary>
public IList<StringCollection> DuplicatePasswordGroups;
}
}