Window has five group policy settings related to password security:
I know how to use NetUserModalsGet
to read most of these items. But it doesn't support checking if password complexity requirement is enabled:
usrmod0_password_hist_len
usrmod0_max_passwd_age
usrmod0_min_passwd_age
usrmod0_min_passwd_len
?
I also know that WMI's RSOP ("Resultant set of policy") is unsuitable, as it only works on a domain. And i'm certainly not going to crawling through an undocumented binary blob (i.e. i want the supported way).
Note: I don't care about the "Store passwords using reversible encryption" group policy setting.
You can also use the NetUserModalsGet
API to retrieve the Account Lockout Policy settings:
usrmod3_lockout_duration
usrmod3_lockout_threshold
usrmod3_lockout_observation_window
Thus rounding out all the password related group policy options; except for "must meet complexity requirements".
For completeness, assume a non-domain joined machine (i.e. no AD server to query, no RSOP to query, etc).
This is accessible using SAM (Security Account Manager) APIs.
This API (served by SAMLIB.DLL) is not directly documented (no header, no SDK), but the "protocol" to use it is documented here: [MS-SAMR]: Security Account Manager (SAM) Remote Protocol (Client-to-Server), you "just" have to remove the r
in described SamrXXXX
methods.
The ones in question here are SamQueryInformationDomain (and associated SamSetInformationDomain) which will get you a DOMAIN_PASSWORD_INFORMATION structure
typedef struct _DOMAIN_PASSWORD_INFORMATION {
unsigned short MinPasswordLength;
unsigned short PasswordHistoryLength;
unsigned long PasswordProperties;
OLD_LARGE_INTEGER MaxPasswordAge;
OLD_LARGE_INTEGER MinPasswordAge;
} DOMAIN_PASSWORD_INFORMATION,
The PasswordProperties member can contain DOMAIN_PASSWORD_COMPLEX
flag:
DOMAIN_PASSWORD_COMPLEX
0x00000001
The server enforces password complexity policy. See section 3.1.1.7.2 for details of the password policy.
I've provided some C# samples to check this.
First one dumps the policy for all domains served by the current machine's SAM server:
using (SamServer server = new SamServer(null, SamServer.SERVER_ACCESS_MASK.SAM_SERVER_ENUMERATE_DOMAINS | SamServer.SERVER_ACCESS_MASK.SAM_SERVER_LOOKUP_DOMAIN))
{
foreach (string domain in server.EnumerateDomains())
{
Console.WriteLine("domain: " + domain);
var sid = server.GetDomainSid(domain);
Console.WriteLine(" sid: " + sid);
var pi = server.GetDomainPasswordInformation(sid);
Console.WriteLine(" MaxPasswordAge: " + pi.MaxPasswordAge);
Console.WriteLine(" MinPasswordAge: " + pi.MinPasswordAge);
Console.WriteLine(" MinPasswordLength: " + pi.MinPasswordLength);
Console.WriteLine(" PasswordHistoryLength: " + pi.PasswordHistoryLength);
Console.WriteLine(" PasswordProperties: " + pi.PasswordProperties);
}
}
Second one reads and updates the policy for the current machine's domain:
using (SamServer server = new SamServer(null, SamServer.SERVER_ACCESS_MASK.SAM_SERVER_ALL_ACCESS))
{
var sid = server.GetDomainSid(Environment.MachineName);
var pi = server.GetDomainPasswordInformation(sid);
// remove password complexity
pi.PasswordProperties &= ~SamServer.PASSWORD_PROPERTIES.DOMAIN_PASSWORD_COMPLEX;
server.SetDomainPasswordInformation(sid, pi);
}
This is the SamServer
utility:
public sealed class SamServer : IDisposable
{
private IntPtr _handle;
public SamServer(string name, SERVER_ACCESS_MASK access)
{
Name = name;
Check(SamConnect(new UNICODE_STRING(name), out _handle, access, IntPtr.Zero));
}
public string Name { get; }
public void Dispose()
{
if (_handle != IntPtr.Zero)
{
SamCloseHandle(_handle);
_handle = IntPtr.Zero;
}
}
public void SetDomainPasswordInformation(SecurityIdentifier domainSid, DOMAIN_PASSWORD_INFORMATION passwordInformation)
{
if (domainSid == null)
throw new ArgumentNullException(nameof(domainSid));
var sid = new byte[domainSid.BinaryLength];
domainSid.GetBinaryForm(sid, 0);
Check(SamOpenDomain(_handle, DOMAIN_ACCESS_MASK.DOMAIN_WRITE_PASSWORD_PARAMS, sid, out IntPtr domain));
IntPtr info = Marshal.AllocHGlobal(Marshal.SizeOf(passwordInformation));
Marshal.StructureToPtr(passwordInformation, info, false);
try
{
Check(SamSetInformationDomain(domain, DOMAIN_INFORMATION_CLASS.DomainPasswordInformation, info));
}
finally
{
Marshal.FreeHGlobal(info);
SamCloseHandle(domain);
}
}
public DOMAIN_PASSWORD_INFORMATION GetDomainPasswordInformation(SecurityIdentifier domainSid)
{
if (domainSid == null)
throw new ArgumentNullException(nameof(domainSid));
var sid = new byte[domainSid.BinaryLength];
domainSid.GetBinaryForm(sid, 0);
Check(SamOpenDomain(_handle, DOMAIN_ACCESS_MASK.DOMAIN_READ_PASSWORD_PARAMETERS, sid, out IntPtr domain));
var info = IntPtr.Zero;
try
{
Check(SamQueryInformationDomain(domain, DOMAIN_INFORMATION_CLASS.DomainPasswordInformation, out info));
return (DOMAIN_PASSWORD_INFORMATION)Marshal.PtrToStructure(info, typeof(DOMAIN_PASSWORD_INFORMATION));
}
finally
{
SamFreeMemory(info);
SamCloseHandle(domain);
}
}
public SecurityIdentifier GetDomainSid(string domain)
{
if (domain == null)
throw new ArgumentNullException(nameof(domain));
Check(SamLookupDomainInSamServer(_handle, new UNICODE_STRING(domain), out IntPtr sid));
return new SecurityIdentifier(sid);
}
public IEnumerable<string> EnumerateDomains()
{
int cookie = 0;
while (true)
{
var status = SamEnumerateDomainsInSamServer(_handle, ref cookie, out IntPtr info, 1, out int count);
if (status != NTSTATUS.STATUS_SUCCESS && status != NTSTATUS.STATUS_MORE_ENTRIES)
Check(status);
if (count == 0)
break;
var us = (UNICODE_STRING)Marshal.PtrToStructure(info + IntPtr.Size, typeof(UNICODE_STRING));
SamFreeMemory(info);
yield return us.ToString();
us.Buffer = IntPtr.Zero; // we don't own this one
}
}
private enum DOMAIN_INFORMATION_CLASS
{
DomainPasswordInformation = 1,
}
[Flags]
public enum PASSWORD_PROPERTIES
{
DOMAIN_PASSWORD_COMPLEX = 0x00000001,
DOMAIN_PASSWORD_NO_ANON_CHANGE = 0x00000002,
DOMAIN_PASSWORD_NO_CLEAR_CHANGE = 0x00000004,
DOMAIN_LOCKOUT_ADMINS = 0x00000008,
DOMAIN_PASSWORD_STORE_CLEARTEXT = 0x00000010,
DOMAIN_REFUSE_PASSWORD_CHANGE = 0x00000020,
}
[Flags]
private enum DOMAIN_ACCESS_MASK
{
DOMAIN_READ_PASSWORD_PARAMETERS = 0x00000001,
DOMAIN_WRITE_PASSWORD_PARAMS = 0x00000002,
DOMAIN_READ_OTHER_PARAMETERS = 0x00000004,
DOMAIN_WRITE_OTHER_PARAMETERS = 0x00000008,
DOMAIN_CREATE_USER = 0x00000010,
DOMAIN_CREATE_GROUP = 0x00000020,
DOMAIN_CREATE_ALIAS = 0x00000040,
DOMAIN_GET_ALIAS_MEMBERSHIP = 0x00000080,
DOMAIN_LIST_ACCOUNTS = 0x00000100,
DOMAIN_LOOKUP = 0x00000200,
DOMAIN_ADMINISTER_SERVER = 0x00000400,
DOMAIN_ALL_ACCESS = 0x000F07FF,
DOMAIN_READ = 0x00020084,
DOMAIN_WRITE = 0x0002047A,
DOMAIN_EXECUTE = 0x00020301
}
[Flags]
public enum SERVER_ACCESS_MASK
{
SAM_SERVER_CONNECT = 0x00000001,
SAM_SERVER_SHUTDOWN = 0x00000002,
SAM_SERVER_INITIALIZE = 0x00000004,
SAM_SERVER_CREATE_DOMAIN = 0x00000008,
SAM_SERVER_ENUMERATE_DOMAINS = 0x00000010,
SAM_SERVER_LOOKUP_DOMAIN = 0x00000020,
SAM_SERVER_ALL_ACCESS = 0x000F003F,
SAM_SERVER_READ = 0x00020010,
SAM_SERVER_WRITE = 0x0002000E,
SAM_SERVER_EXECUTE = 0x00020021
}
[StructLayout(LayoutKind.Sequential)]
public struct DOMAIN_PASSWORD_INFORMATION
{
public short MinPasswordLength;
public short PasswordHistoryLength;
public PASSWORD_PROPERTIES PasswordProperties;
private long _maxPasswordAge;
private long _minPasswordAge;
public TimeSpan MaxPasswordAge
{
get
{
return -new TimeSpan(_maxPasswordAge);
}
set
{
_maxPasswordAge = value.Ticks;
}
}
public TimeSpan MinPasswordAge
{
get
{
return -new TimeSpan(_minPasswordAge);
}
set
{
_minPasswordAge = value.Ticks;
}
}
}
[StructLayout(LayoutKind.Sequential)]
private class UNICODE_STRING : IDisposable
{
public ushort Length;
public ushort MaximumLength;
public IntPtr Buffer;
public UNICODE_STRING()
: this(null)
{
}
public UNICODE_STRING(string s)
{
if (s != null)
{
Length = (ushort)(s.Length * 2);
MaximumLength = (ushort)(Length + 2);
Buffer = Marshal.StringToHGlobalUni(s);
}
}
public override string ToString() => Buffer != IntPtr.Zero ? Marshal.PtrToStringUni(Buffer) : null;
protected virtual void Dispose(bool disposing)
{
if (Buffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(Buffer);
Buffer = IntPtr.Zero;
}
}
~UNICODE_STRING() => Dispose(false);
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
private static void Check(NTSTATUS err)
{
if (err == NTSTATUS.STATUS_SUCCESS)
return;
throw new Win32Exception("Error " + err + " (0x" + ((int)err).ToString("X8") + ")");
}
private enum NTSTATUS
{
STATUS_SUCCESS = 0x0,
STATUS_MORE_ENTRIES = 0x105,
STATUS_INVALID_HANDLE = unchecked((int)0xC0000008),
STATUS_INVALID_PARAMETER = unchecked((int)0xC000000D),
STATUS_ACCESS_DENIED = unchecked((int)0xC0000022),
STATUS_OBJECT_TYPE_MISMATCH = unchecked((int)0xC0000024),
STATUS_NO_SUCH_DOMAIN = unchecked((int)0xC00000DF),
}
[DllImport("samlib.dll", CharSet = CharSet.Unicode)]
private static extern NTSTATUS SamConnect(UNICODE_STRING ServerName, out IntPtr ServerHandle, SERVER_ACCESS_MASK DesiredAccess, IntPtr ObjectAttributes);
[DllImport("samlib.dll", CharSet = CharSet.Unicode)]
private static extern NTSTATUS SamCloseHandle(IntPtr ServerHandle);
[DllImport("samlib.dll", CharSet = CharSet.Unicode)]
private static extern NTSTATUS SamFreeMemory(IntPtr Handle);
[DllImport("samlib.dll", CharSet = CharSet.Unicode)]
private static extern NTSTATUS SamOpenDomain(IntPtr ServerHandle, DOMAIN_ACCESS_MASK DesiredAccess, byte[] DomainId, out IntPtr DomainHandle);
[DllImport("samlib.dll", CharSet = CharSet.Unicode)]
private static extern NTSTATUS SamLookupDomainInSamServer(IntPtr ServerHandle, UNICODE_STRING name, out IntPtr DomainId);
[DllImport("samlib.dll", CharSet = CharSet.Unicode)]
private static extern NTSTATUS SamQueryInformationDomain(IntPtr DomainHandle, DOMAIN_INFORMATION_CLASS DomainInformationClass, out IntPtr Buffer);
[DllImport("samlib.dll", CharSet = CharSet.Unicode)]
private static extern NTSTATUS SamSetInformationDomain(IntPtr DomainHandle, DOMAIN_INFORMATION_CLASS DomainInformationClass, IntPtr Buffer);
[DllImport("samlib.dll", CharSet = CharSet.Unicode)]
private static extern NTSTATUS SamEnumerateDomainsInSamServer(IntPtr ServerHandle, ref int EnumerationContext, out IntPtr EnumerationBuffer, int PreferedMaximumLength, out int CountReturned);
}