Identifying Well-Known Security Principals with confidence
Once in a while, you may find yourself in need of identifying and/or interacting with well-known and well-defined security principals, like the builtin Administrator account, the SYSTEM account or a generic principal group like Everyone - let's explore safe ways to do this with PowerShell!
Originally published on December 31, 2014
Assumptions and Expectations
There are numerous shortcuts to getting to the Administrator account on a Windows machine. First thing that comes to mind is using a DirectoryEntry with the WinNT provider, like so:
$Administrator = [ADSI]"WinNT://$($env:COMPUTERNAME)/Administrator,user"
Now, this is all fine and dandy if you're on an English-language, Out-of-the-Box Windows installation.
But what if the builtin Administrator is not called Administrator? What if the local account had been renamed?
LocaleID and ümlaüts
You might be using Finnish-language Windows, in which case the Administrator account is localized as "Järjestelmänvalvoja" - not kidding! Now our script will have to take that into account:
$AdminName = switch((Get-Culture).LCID){
# LCID = local administrator name
# Finnish
11 {"Järjestelmänvalvoja"}
1035 {"Järjestelmänvalvoja"}
# French
12 {"Administrateur"}
1036 {"Administrateur"}
# Hungarian
14 {"Rendszergazda"}
1038 {"Rendszergazda"}
# Portuguese
22 {"Administrador"}
1046 {"Administrador"}
2070 {"Administrador"}
# Russian
25 {"Администратор"}
1049 {"Администратор"}
# Spanish
10 {"Administrador"}
3082 {"Administrador"}
# Swedish
29 {"Administratör"}
1053 {"Administratör"}
# Default, assumes "Administrator"
default {"Administrator"}
}
$Administrator = [ADSI]"WinNT://$($env:COMPUTERNAME)/$AdminName,user"
Gosh, that's horrible - another snowflake we now need to maintain (who knows, these localizations might not persist in Windows forever).
SIDs to the rescue!
Now, the SAM Account Name is not the only identifier by which we can get to the Administrator account (or any account for that matter). A much more interesting (and distinct) identifier would be the account Security Identifier, often abbreviated "SID".
Working with SIDs offers a few interesting characteristics:
- A SID is unique!
- A SID is persistent and won't change for the lifetime of the principal!
- A SID contains information about the Principal AND the LogonDomain*!
- Internally, a SID is just a variable-length byte array!
Now, in Windows-land, a number of SIDs are pre-defined, either universally, like "Everyone" (S-1-1-0), or domain specific, like the builtin "Administrator" account (S-1-5-21-500). These Security Identifiers are called "Well-Known" SIDs, and consistently identifies builtin principals like the examples above.
The constructor for the .NET implementation of the Security Identifier type has an overload that takes a so-called WellKnownSidType object and a Domain SID as it's arguments, meaning that we can simply target a "well-known" principal using the WellKnownSidType enum, without caring about the name of the account/group! Yay!
$LogonDomainSID = "S-1-5-21-1682721775-3705583687-167823271"
$WellKnownUser = [System.Security.Principal.WellKnownSidType]::AccountAdministratorSid
$Administrator = New-Object System.Security.Principal.SecurityIdentifier -ArgumentList $WellKnownUser,$LogonDomainSID
Much better - no need to take the Locale into account anymore!
Now we just need a way to identify the LogonDomain SID, and we can reference the builtin Administrator account on any system or in any domain!
Figuring out the LogonDomain SID
For local users, the LogonDomain is the Machine SID, that is - the SID of the local SAM database - not the SID of the computer account object, should the machine happen to be a member of a domain.
Windows keeps this piece of information in the not-very-accessible HKLM:\SECURITY hive of the registry. There are other ways to deduce this without obtaining access to said hive though. One of my favorites, but a somewhat hacky approach is to retrieve a local user account using WMI, and then removing the last portion (the "relative identifier") of the account SID, resulting the Domain SID. I've thrown together a simple sample below:
function Get-MachineSID
{
param(
[switch]
$DomainSID
)
$WmiComputerSystem = Get-WmiObject -Class Win32_ComputerSystem
$IsDomainController = $WmiComputerSystem.DomainRole -ge 4
if($DomainSID -or $IsDomainController)
{
$Domain = $WmiComputerSystem.Domain
$SIDBytes = ([ADSI]"LDAP://$Domain").objectSid |%{$_}
$ByteOffset = 0
New-Object System.Security.Principal.SecurityIdentifier -ArgumentList ([Byte[]]$SIDBytes),$ByteOffset
}
else
{
$LocalAccountSID = Get-WmiObject -Query "SELECT SID FROM Win32_UserAccount WHERE LocalAccount = 'True'" |Select-Object -First 1 -ExpandProperty SID
$MachineSID = ($p = $LocalAccountSID -split "-")[0..($p.Length-2)]-join"-"
New-Object System.Security.Principal.SecurityIdentifier -ArgumentList $MachineSID
}
}
Great! Now we can simply do:
$WellKnownSid = [System.Security.Principal.WellKnownSidType]::AccountAdministratorSid
$MachineSID = Get-MachineSID
$AdminAccount = New-Object System.Security.Principal.SecurityIdentifier -ArgumentList $WellKnownSid,$MachineSID
And that's it! .NET <3