Now, and then I get small requests to build seemingly simple PowerShell scripts. This one I had to build had requirements as seen below:
Even with PowerShell 5.1 simple task isn't that simple. We need to deal with group names through SIDs rather than names because each group name is different in different languages. The second problem is to distinguish whether a user is a local or domain user. Finally, I need to connect to Active Directory to verify if the user I am about to remove has ExtensionAttribute10 (or any other field in AD) filled in or not. This is a similar topic to my earlier post. Create a local user or administrator account in Windows using PowerShell. In last few weeks I have been exposed to PowerShell 2.0 and well, I don't like it! When you work with PowerShell 5.1 or higher, you're getting spoiled every single day.
While the stuff above is easy if you have Windows 10 with RSAT installed, it's a different game with PowerShell 2.0 and no tools installed to connect to AD. I've created functions below that can help with this small task
function Get-InvariantGroup { [CmdletBinding()] param( [ValidateSet( 'Access Control Assistance Operators', 'Administrators' , 'Backup Operators' , 'Cryptographic Operators' , 'Device Owners' , 'Distributed COM Users' , 'Event Log Readers' , 'Guests' , 'Hyper-V Administrators' , 'IIS_IUSRS' , 'Network Configuration Operators' , 'Performance Log Users' , 'Performance Monitor Users' , 'Power Users' , 'Remote Desktop Users' , 'Remote Management Users' , 'Replicator' , 'System Managed Accounts Group' , 'Users' )] $GroupName ) $GroupSID = @{ 'Access Control Assistance Operators' = 'S-1-5-32-579' 'Administrators' = 'S-1-5-32-544' 'Backup Operators' = 'S-1-5-32-551' 'Cryptographic Operators' = 'S-1-5-32-569' 'Device Owners' = 'S-1-5-32-583' 'Distributed COM Users' = 'S-1-5-32-562' 'Event Log Readers' = 'S-1-5-32-573' 'Guests' = 'S-1-5-32-546' 'Hyper-V Administrators' = 'S-1-5-32-578' 'IIS_IUSRS' = 'S-1-5-32-568' 'Network Configuration Operators' = 'S-1-5-32-556' 'Performance Log Users' = 'S-1-5-32-559' 'Performance Monitor Users' = 'S-1-5-32-558' 'Power Users' = 'S-1-5-32-547' 'Remote Desktop Users' = 'S-1-5-32-555' 'Remote Management Users' = 'S-1-5-32-580' 'Replicator' = 'S-1-5-32-552' 'System Managed Accounts Group' = 'S-1-5-32-581' 'Users' = 'S-1-5-32-545' } $GroupSID[$GroupName] } function Get-LocalUserFromDomain { [cmdletbinding()] param( [string] $NetBiosName, [Object] $Member ) $Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() foreach ($Domain in $Forest.Domains) { #if ($Domain.Name -eq $DomainName) { $Root = $Domain.GetDirectoryEntry() $DomainDN = ($Root.distinguishedName) # Use the NameTranslate object. $objTrans = New-Object -comObject "NameTranslate" $objNT = $objTrans.GetType() # Invoke the Init method to Initialize NameTranslate by locating # the Global Catalog. Note the constant 3 is ADS_NAME_INITTYPE_GC. $objNT.InvokeMember("Init", "InvokeMethod", $Null, $objTrans, (3, $Null)) # Use the Set method to specify the Distinguished Name of the current domain. # Note the constant 1 is ADS_NAME_TYPE_1779. $objNT.InvokeMember("Set", "InvokeMethod", $Null, $objTrans, (1, "$DomainDN")) # Use the Get method to retrieve the NetBIOS name of the current domain. # Note the constant 3 is ADS_NAME_TYPE_NT4. # The value retrieved includes a trailing backslash. $strDomain = $objNT.InvokeMember("Get", "InvokeMethod", $Null, $objTrans, 3) $strDomain = $strDomain -replace '\\' if ($strDomain -eq $NetBiosName) { break } } if ($domainDN) { #$domainDN = $domain.GetDirectoryEntry().distinguishedName $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]"LDAP://$DomainDN") # Loop each user $Results = ForEach ($Name in $Member) { # $Searcher.filter = "(&(objectClass=user)(sAMAccountName= $SAMName))" $Searcher.filter = "(&(objectClass=user)(sAMAccountName=$($Name.SamAccountName)))" $User = $Searcher.FindOne() If ($User) { # Create PS Object $Object = New-Object PSObject -Property @{ Samaccountname = (($User.Properties).samaccountname | Out-String) Email = (($User.Properties).mail | Out-String) Mobile = (($User.Properties).mobile | Out-String) ExtensionAttribute10 = (($User.Properties).extensionattribute10 | Out-String) LocalPath = $Name.Path } $Object } $User } # Display results $Results } } function Remove-LocalUserFromGroup { [CmdletBinding()] param( [string] $Computer = $Env:ComputerName, [ValidateSet( 'Access Control Assistance Operators', 'Administrators' , 'Backup Operators' , 'Cryptographic Operators' , 'Device Owners' , 'Distributed COM Users' , 'Event Log Readers' , 'Guests' , 'Hyper-V Administrators' , 'IIS_IUSRS' , 'Network Configuration Operators' , 'Performance Log Users' , 'Performance Monitor Users' , 'Power Users' , 'Remote Desktop Users' , 'Remote Management Users' , 'Replicator' , 'System Managed Accounts Group' , 'Users' )] $GroupName, [Array] $Users ) foreach ($_ in $Users) { try { $User = $_.LocalPath $groupObj = [ADSI]"WinNT://$Computer/$GroupName,group" $groupObj.Remove($User) } catch { $ErrorMessage = $_.Exception.Message -replace [System.Environment]::NewLine if ($ErrorMessage -like '*An invalid directory pathname was passed*') { # This happens if you run the script more than once, and the object has already been removed from group but still in cache } elseif ($ErrorMessage -like '*Access is denied*') { Write-Warning "Remove-LocalUserFromGroup - Access denied. Are you running as administrator?" } else { Write-Warning "Remove-LocalUserFromGroup - $ErrorMessage" } } } } function Get-LocalUserFromGroup { [CmdletBinding()] param( [string] $Computer = $Env:ComputerName, [ValidateSet( 'Access Control Assistance Operators', 'Administrators' , 'Backup Operators' , 'Cryptographic Operators' , 'Device Owners' , 'Distributed COM Users' , 'Event Log Readers' , 'Guests' , 'Hyper-V Administrators' , 'IIS_IUSRS' , 'Network Configuration Operators' , 'Performance Log Users' , 'Performance Monitor Users' , 'Power Users' , 'Remote Desktop Users' , 'Remote Management Users' , 'Replicator' , 'System Managed Accounts Group' , 'Users' )] $GroupName ) # "Running Add-LocalUserToGroup" | Add-Content -Path 'C:\Temp\ScriptRunning.txt' $LocalUsers = Get-WmiObject Win32_UserAccount -Filter "LocalAccount=true" if ($LocalUsers) { $SID = Get-InvariantGroup -GroupName $GroupName $ObjSID = New-Object System.Security.Principal.SecurityIdentifier($SID) $GroupProvider = (($ObjSID.Translate([System.Security.Principal.NTAccount]) ).Value).Split("\")[1] try { #Write-Verbose " security principal: $User to the $GroupProvider group..." #"Add-LocalUserToGroup - Adding security principal: $User to the $GroupProvider group..." | Add-Content -Path 'C:\Temp\ScriptRunning.txt' $group = [ADSI]"WinNT://$Computer/$GroupProvider,group" $Users = $group.Invoke("Members") | ForEach-Object { #$_.GetType.Invoke().InvokeMember($null, 'GetProperty', $null, $_, $null) $LocalUser = ([ADSI]$_) $AdsPath = $LocalUser.InvokeGet('AdsPath').Replace('WinNT://', '') $IsGroup = ($LocalUser.SchemaClassName -like 'group') if (([regex]::Matches($AdsPath, '/')).count -eq 1) { # DOMAIN\user $MemberIsDomain = $True $Name = $AdsPath.Replace('/', '\') } else { # DOMAIN\machine\user $MemberIsDomain = $False $Name = $AdsPath.Substring($AdsPath.IndexOf('/') + 1).Replace('/', '\') } $NETBIOSName = $AdsPath.Split('/')[0] $SamAccountName = $Name.Split('\')[1] $Member = New-Object PSObject $Member | Add-Member Noteproperty 'GroupName' -Value $GroupProvider $Member | Add-Member Noteproperty 'AccountName' -Value $Name $Member | Add-Member NoteProperty 'SamAccountName' -Value $SamAccountName $Member | Add-Member Noteproperty 'SID' -Value ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.InvokeGet('ObjectSID'), 0)).Value) $Member | Add-Member Noteproperty 'IsGroup' -Value $IsGroup $Member | Add-Member Noteproperty 'IsDomain' -Value $MemberIsDomain $Member | Add-Member NoteProperty 'NetbiosName' -Value $NETBIOSName $Member | Add-Member NoteProperty 'Path' -Value $LocalUser.Path $Member } foreach ($_ in $Users) { if ($_.IsDomain -eq $true -and $_.IsGroup -eq $false) { Get-LocalUserFromDomain -NetBiosName $_.NetbiosName -Member $_ } } } Catch { $ErrorMessage = $_.Exception.Message Write-Verbose $ErrorMessage #"Add-LocalUserToGroup - Adding $User to $GroupProvider failed: $ErrorMessage" | Add-Content -Path 'C:\Temp\ScriptRunning.txt' } } else { Write-Warning "Add-LocalUserToGroup - $User doesn't exists. Terminating." #"Add-LocalUserToGroup - $User doesn't exists. Terminating." | Add-Content -Path 'C:\Temp\ScriptRunning.txt' } }
After you have those functions, you can do something like code below to remove user based on a non-empty ExtensionAttribute10.
# Code Execution $DomainUsers = Get-LocalUserFromGroup -GroupName Administrators -Verbose $UsersToRemove = foreach ($_ in $DomainUsers) { if (!$_.ExtensionAttribute10 -eq '') { $_ } } Remove-LocalUserFromGroup -Users $UsersToRemove -GroupName Administrators
With small modifications, it's entirely possible to get other fields from AD and do your decisions based on that. With the help of ADSI, you don't need any other tools installed other than PowerShell 2.0. Hope this helps in case you ever get a similar task to do.