In my first Month of PowerShell getting started article I talked about a common PowerShell process: running a cmdlet and piping the results to Get-Member or Select-Object -Property * to get a list of the object properties.
It's standard PowerShell practice, and I've done it hundreds of times throughout the Month of PowerShell. That is, until I started working with Active Directory user properties:
PS C:\Users\jwright> Get-ADUser -Identity jwright | Select-Object -Property * DistinguishedName : CN=jwright,CN=Users,DC=falsimentis,DC=local Enabled : True GivenName : Name : jwright ObjectClass : user ObjectGUID : f69b19e6-61ac-4cae-a7ac-ec88ec3bb564 SamAccountName : jwright SID : S-1-5-21-1850091285-130397106-3068105436-500 Surname : UserPrincipalName : PropertyNames : {DistinguishedName, Enabled, GivenName, Name...} AddedProperties : {} RemovedProperties : {} ModifiedProperties : {} PropertyCount : 10 PS C:\Users\jwright>
Normally, I'd expect to see all of the properties for the AD User object, but lots of properties are missing. This is not consistent with other PowerShell cmdlet behavior.
Normally we expect the cmdlet to return objects with all of the available properties. That is not the case for Get-ADUser.
Looking at the Get-Help Get-ADUser output, there is an option -Properties that accepts a string or list or strings, similar to what we see in the help for Select-Object. When we use that option with a wildcard, we get all the AD user properties:
PS C:\Users\jwright> Get-ADUser -Identity jwright -Properties * | Select-Object -Property * AccountExpirationDate : accountExpires : 0 AccountLockoutTime : AccountNotDelegated : False AllowReversiblePasswordEncryption : False AuthenticationPolicy : {} AuthenticationPolicySilo : {} BadLogonCount : 0 badPasswordTime : 0 badPwdCount : 0 CannotChangePassword : False CanonicalName : falsimentis.local/Users/jwright Certificates : {} City : CN : jwright codePage : 0 Company : CompoundIdentitySupported : {} Country : countryCode : 0 Created : 7/16/2022 7:08:49 PM createTimeStamp : 7/16/2022 7:08:49 PM Deleted : Department : Description : Built-in account for administering the computer/domain DisplayName : DistinguishedName : CN=jwright,CN=Users,DC=falsimentis,DC=local Division : DoesNotRequirePreAuth : False dSCorePropagationData : {7/16/2022 7:09:34 PM, 1/1/1601 12:00:01 AM} EmailAddress : EmployeeID : EmployeeNumber : Enabled : True Fax : GivenName : HomeDirectory : HomedirRequired : False HomeDrive : HomePage : HomePhone : Initials : instanceType : 4 isCriticalSystemObject : True isDeleted : KerberosEncryptionType : {} LastBadPasswordAttempt : LastKnownParent : lastLogoff : 0 lastLogon : 133024722383629408 LastLogonDate : 7/16/2022 7:10:23 PM lastLogonTimestamp : 133024722231721433 LockedOut : False logonCount : 25 logonHours : {255, 255, 255, 255...} LogonWorkstations : Manager : MemberOf : {CN=Group Policy Creator Owners,CN=Users,DC=falsimentis,DC=local, CN=Domain Admins,CN=Users,DC=falsimentis,DC=local, CN=Enterprise Admins,CN=Users,DC=falsimentis,DC=local, CN=Schema Admins,CN=Users,DC=falsimentis,DC=local...} MNSLogonAccount : False MobilePhone : Modified : 7/16/2022 7:10:23 PM modifyTimeStamp : 7/16/2022 7:10:23 PM msDS-User-Account-Control-Computed : 0 Name : jwright nTSecurityDescriptor : System.DirectoryServices.ActiveDirectorySecurity ObjectCategory : CN=Person,CN=Schema,CN=Configuration,DC=falsimentis,DC=local ObjectClass : user ObjectGUID : f69b19e6-61ac-4cae-a7ac-ec88ec3bb564 objectSid : S-1-5-21-1850091285-130397106-3068105436-500 Office : OfficePhone : Organization : OtherName : PasswordExpired : False PasswordLastSet : 7/16/2022 6:42:54 PM PasswordNeverExpires : False PasswordNotRequired : False POBox : PostalCode : PrimaryGroup : CN=Domain Users,CN=Users,DC=falsimentis,DC=local primaryGroupID : 513 PrincipalsAllowedToDelegateToAccount : {} ProfilePath : ProtectedFromAccidentalDeletion : False pwdLastSet : 133024705746844315 SamAccountName : jwright sAMAccountType : 805306368 ScriptPath : sDRightsEffective : 15 ServicePrincipalNames : {} SID : S-1-5-21-1850091285-130397106-3068105436-500 SIDHistory : {} SmartcardLogonRequired : False State : StreetAddress : Surname : Title : TrustedForDelegation : False TrustedToAuthForDelegation : False UseDESKeyOnly : False userAccountControl : 512 userCertificate : {} UserPrincipalName : uSNChanged : 12579 uSNCreated : 8196 whenChanged : 7/16/2022 7:10:23 PM whenCreated : 7/16/2022 7:08:49 PM PropertyNames : {AccountExpirationDate, accountExpires, AccountLockoutTime, AccountNotDelegated...} AddedProperties : {} RemovedProperties : {} ModifiedProperties : {} PropertyCount : 107
Keep it weird, PowerShell.
The question is, why? Why does Get-ADUser break from convention like other PowerShell cmdlets and not send all of the available properties to the next command in the pipeline? I think I found the answer in Mike Robbins's PowerShell 101:
Imagine if you returned every property for every user account in your production Active Directory environment. Think of the performance degradation that you could cause not only to the domain controllers themselves, but also to your network ... PowerShell 101, Mike F. Robbins
This seems like a reasonable assertion for why, and it's an important precedent for PowerShell users to keep in mind: PowerShell cmdlets may not include all available properties by default.
How do you know if the absence of a property you want to access (e.g., the parent process ID in Get-Process) is available but omitted by default, or if it's not available at all? The only reasonable way to know is to look at the Get-Help output for a given cmdlet, and see if there is an option for -Properties, and experiment with CMDLETNAME -Properties * | Select-Object -Property *.
In practice, you will rarely need all of the properties from any cmdlet in the pipeline. This brings us to the lesson Blake Regan @crash0ver1d3 tried to tell me a few weeks ago but I wasn't ready to listen yet:
When creating pipelines in PowerShell where performance matters, avoid collecting a lot of unnecessary objects. Where possible, filter closer to the left of the pipeline.
For example, if I want to build a CSV file of all domain users that includes the name, SID, password expired (Boolean), and password last set (date) elements, I can run this command:
PS C:\Users\jwright> Get-ADUser -Filter * -Properties * | Select-Object -Property Name, SID, PasswordExpired, PasswordLastSet | ConvertTo-Csv | Out-File userpassset.csv PS C:\Users\jwright>
However, this creates an unnecessarily large collection of user objects. It might not be noticeable with tens or hundreds of users, but on a domain with hundreds of thousands of users this command will create a lot of unnecessary overhead. Here's a better solution:
PS C:\Users\jwright> Get-ADUser -Filter * -Properties Name, SID, PasswordExpired, PasswordLastSet | Select-Object -Property Name, SID, PasswordExpired, PasswordLastSet | ConvertTo-Csv | Out-File userpassset.csv PS C:\Users\jwright> Get-Content -first 3 .\userpassset.csv #TYPE Selected.Microsoft.ActiveDirectory.Management.ADUser "Name","SID","PasswordExpired","PasswordLastSet" "jwright","S-1-5-21-1850091285-130397106-3068105436-500","False","7/16/2022 6:42:54 PM" PS C:\Users\jwright>
It's a little redundant to specify the object properties twice (and don't get me started on the inconsistent use of Get-ADUSer -Properties and Select-Object -Property), but it improves performance without a lot of added effort.
If you have other questions about PowerShell things, let me know! Reach out on Twitter (my DMs are open), or email josh@willhackforsushi.com.
Thank you for reading!
-Joshua Wright
Return to Getting Started With PowerShell
Joshua Wright is the author of SANS SEC504: Hacker Tools, Techniques, and Incident Handling, a faculty fellow for the SANS Institute, and a senior technical director at Counter Hack.