PowerShell Pipeline
Reporting on Local Accounts Using PowerShell
Keep an eye on user accounts whether you're local or not.
One of the things that PowerShell doesn't have is a way to view local accounts on local and remote systems. Fortunately for us, we have a couple of options at our disposal that can get around this to view what accounts are built on a system as well as various details about those accounts.
The two approaches that I will be covering to view local accounts are done using Windows Management Instrumentation (WMI) and Active Directory Services Interfaces (ADSI). Both approaches offer some differences in what can be viewed in regards to local accounts when used against a local or remote system.
WMI Approach
The first method that I will introduce you to is using WMI and the Win32_UserAccount class. When using this class, we need to make sure that we use a filter to only look at local accounts. Otherwise we will pull all of the accounts that are on the domain.
Get-WmiObject -Class Win32_UserAccount -Filter "LocalAccount='True'"
From here, we can see a few things about the account such as the name and SID as well as the account type. But, I want to see a little bit more information about these accounts, so I will opt to select a few more properties to view.
Get-WmiObject -Class Win32_UserAccount -Filter "LocalAccount='True'" |
Select PSComputername, Name, Status, Disabled, AccountType, Lockout, PasswordRequired, PasswordChangeable, SID
Here we can see the same properties that were originally shown, but now we are able to look at whether the accounts have been disabled, locked out and what the password restrictions are. This is great and all, but it would be nice to see some other bits of information about the accounts such as any user flags and other password requirements that we cannot see using WMI. Fortunately, we can do this using ADSI as a means to query the local accounts.
ADSI Approach
Using ADSI is not just for querying Active Directory! We can use this to perform queries or changes against local and remote computers as well. We just have to adjust from using LDAP:// to WinNT:// instead and supply the computer name.
$Computername = $env:COMPUTERNAME
$adsi = [ADSI]"WinNT://$Computername"
Pulling user accounts is just a matter of filtering out for users by looking at the SchemaClassName.
$Users = $adsi.Children | where {$_.SchemaClassName -eq 'user'}
$Users
Of course, this isn't much to look at by itself, so we will use Select-Object with a wildcard to look at all of the available properties of an account to get a better idea of what is available to us.
$Users[0] | Select *
We definitely have a lot more information available to us here than with WMI. You may have noticed there is no defined SID here, but a property called ObjectSid which has something that looks like it could be the SID that we are looking for. The ObjectSid is actually a byte array that needs to be converted to its string representation of a SID. We can do this in one line as shown below:
(New-Object System.Security.Principal.SecurityIdentifier($Users[0].objectSid.value,0)).Value
S-1-5-21-1315970109-1571106365-1434827950-500 is the SID that gets returned I run this command.
We aren't done yet with converting values to something more usable. Take note of the UserFlags property which shows a value of 8389121, which doesn't really mean a whole lot. We have to do use a bitwise operator, -BOR along with knowledge of what values we need to perform the bitwise operation with in order to determine the more human readable values that the userflags property represents. We can find a list of all of these values out on MSDN here. Rather than do this each time to find the data, I have a simple function that can do this for me.
Function Convert-UserFlag {
Param ($UserFlag)
$List = New-Object System.Collections.ArrayList
Switch ($UserFlag) {
($UserFlag -BOR 0x0001) {[void]$List.Add('SCRIPT')}
($UserFlag -BOR 0x0002) {[void]$List.Add('ACCOUNTDISABLE')}
($UserFlag -BOR 0x0008) {[void]$List.Add('HOMEDIR_REQUIRED')}
($UserFlag -BOR 0x0010) {[void]$List.Add('LOCKOUT')}
($UserFlag -BOR 0x0020) {[void]$List.Add('PASSWD_NOTREQD')}
($UserFlag -BOR 0x0040) {[void]$List.Add('PASSWD_CANT_CHANGE')}
($UserFlag -BOR 0x0080) {[void]$List.Add('ENCRYPTED_TEXT_PWD_ALLOWED')}
($UserFlag -BOR 0x0100) {[void]$List.Add('TEMP_DUPLICATE_ACCOUNT')}
($UserFlag -BOR 0x0200) {[void]$List.Add('NORMAL_ACCOUNT')}
($UserFlag -BOR 0x0800) {[void]$List.Add('INTERDOMAIN_TRUST_ACCOUNT')}
($UserFlag -BOR 0x1000) {[void]$List.Add('WORKSTATION_TRUST_ACCOUNT')}
($UserFlag -BOR 0x2000) {[void]$List.Add('SERVER_TRUST_ACCOUNT')}
($UserFlag -BOR 0x10000) {[void]$List.Add('DONT_EXPIRE_PASSWORD')}
($UserFlag -BOR 0x20000) {[void]$List.Add('MNS_LOGON_ACCOUNT')}
($UserFlag -BOR 0x40000) {[void]$List.Add('SMARTCARD_REQUIRED')}
($UserFlag -BOR 0x80000) {[void]$List.Add('TRUSTED_FOR_DELEGATION')}
($UserFlag -BOR 0x100000) {[void]$List.Add('NOT_DELEGATED')}
($UserFlag -BOR 0x200000) {[void]$List.Add('USE_DES_KEY_ONLY')}
($UserFlag -BOR 0x400000) {[void]$List.Add('DONT_REQ_PREAUTH')}
($UserFlag -BOR 0x800000) {[void]$List.Add('PASSWORD_EXPIRED')}
($UserFlag -BOR 0x1000000) {[void]$List.Add('TRUSTED_TO_AUTH_FOR_DELEGATION')}
($UserFlag -BOR 0x04000000) {[void]$List.Add('PARTIAL_SECRETS_ACCOUNT')}
}
$List -join ', '
}
Now I can send the value of UserFlags into the function and find out what flags are set.
Convert-UserFlag -UserFlag $Users[0].UserFlags.Value
SCRIPT, NORMAL_ACCOUNT, PASSWORD_EXPIRED
We can see that this account has an expired password and is just a normal account. This is much easier than trying to go through each possible enumeration and performing the bitwise operation.
Now that we have this available, we can create a function which can be used report on the local accounts that will work on both local and remote systems!
Function Get-LocalUser {
[Cmdletbinding()]
Param(
[Parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[String[]]$Computername = $Env:Computername
)
Begin {
#region Helper Functions
Function ConvertTo-SID {
Param([byte[]]$BinarySID)
(New-Object System.Security.Principal.SecurityIdentifier($BinarySID,0)).Value
}
Function Convert-UserFlag {
Param ($UserFlag)
$List = New-Object System.Collections.ArrayList
Switch ($UserFlag) {
($UserFlag -BOR 0x0001) {[void]$List.Add('SCRIPT')}
($UserFlag -BOR 0x0002) {[void]$List.Add('ACCOUNTDISABLE')}
($UserFlag -BOR 0x0008) {[void]$List.Add('HOMEDIR_REQUIRED')}
($UserFlag -BOR 0x0010) {[void]$List.Add('LOCKOUT')}
($UserFlag -BOR 0x0020) {[void]$List.Add('PASSWD_NOTREQD')}
($UserFlag -BOR 0x0040) {[void]$List.Add('PASSWD_CANT_CHANGE')}
($UserFlag -BOR 0x0080) {[void]$List.Add('ENCRYPTED_TEXT_PWD_ALLOWED')}
($UserFlag -BOR 0x0100) {[void]$List.Add('TEMP_DUPLICATE_ACCOUNT')}
($UserFlag -BOR 0x0200) {[void]$List.Add('NORMAL_ACCOUNT')}
($UserFlag -BOR 0x0800) {[void]$List.Add('INTERDOMAIN_TRUST_ACCOUNT')}
($UserFlag -BOR 0x1000) {[void]$List.Add('WORKSTATION_TRUST_ACCOUNT')}
($UserFlag -BOR 0x2000) {[void]$List.Add('SERVER_TRUST_ACCOUNT')}
($UserFlag -BOR 0x10000) {[void]$List.Add('DONT_EXPIRE_PASSWORD')}
($UserFlag -BOR 0x20000) {[void]$List.Add('MNS_LOGON_ACCOUNT')}
($UserFlag -BOR 0x40000) {[void]$List.Add('SMARTCARD_REQUIRED')}
($UserFlag -BOR 0x80000) {[void]$List.Add('TRUSTED_FOR_DELEGATION')}
($UserFlag -BOR 0x100000) {[void]$List.Add('NOT_DELEGATED')}
($UserFlag -BOR 0x200000) {[void]$List.Add('USE_DES_KEY_ONLY')}
($UserFlag -BOR 0x400000) {[void]$List.Add('DONT_REQ_PREAUTH')}
($UserFlag -BOR 0x800000) {[void]$List.Add('PASSWORD_EXPIRED')}
($UserFlag -BOR 0x1000000) {[void]$List.Add('TRUSTED_TO_AUTH_FOR_DELEGATION')}
($UserFlag -BOR 0x04000000) {[void]$List.Add('PARTIAL_SECRETS_ACCOUNT')}
}
$List -join ', '
}
#endregion Helper Functions
}
Process {
ForEach ($Computer in $Computername) {
$adsi = [ADSI]"WinNT://$Computername"
$adsi.Children | where {$_.SchemaClassName -eq 'user'} | ForEach {
[pscustomobject]@{
UserName = $_.Name[0]
SID = ConvertTo-SID -BinarySID $_.ObjectSID[0]
PasswordAge = [math]::Round($_.PasswordAge[0]/86400)
LastLogin = If ($_.LastLogin[0] -is [datetime]){$_.LastLogin[0]}Else{'Never logged on'}
UserFlags = Convert-UserFlag -UserFlag $_.UserFlags[0]
MinPasswordLength = $_.MinPasswordLength[0]
MinPasswordAge = [math]::Round($_.MinPasswordAge[0]/86400)
MaxPasswordAge = [math]::Round($_.MaxPasswordAge[0]/86400)
BadPasswordAttempts = $_.BadPasswordAttempts[0]
MaxBadPasswords = $_.MaxBadPasswordsAllowed[0]
}
}
}
}
}
Using this function, we can see more information about the accounts.
$env:Computername | Get-LocalUser
With that, we can use this to report on accounts in our network and provide a report if needed. We could even use this and filter for passwords that have expired, accounts that haven't been used, etc. That's all for today! Hopefully this information helps out in providing a good means to look at your accounts on your systems.
About the Author
Boe Prox is a Microsoft MVP in Windows PowerShell and a Senior Windows System Administrator. He has worked in the IT field since 2003, and he supports a variety of different platforms. He is a contributing author in PowerShell Deep Dives with chapters about WSUS and TCP communication. He is a moderator on the Hey, Scripting Guy! forum, and he has been a judge for the Scripting Games. He has presented talks on the topics of WSUS and PowerShell as well as runspaces to PowerShell user groups. He is an Honorary Scripting Guy, and he has submitted a number of posts as a to Microsoft's Hey, Scripting Guy! He also has a number of open source projects available on Codeplex and GitHub. His personal blog is at http://learn-powershell.net.