PowerShell: SID Walker, Texas Ranger (Part 1)

SID Walker

First things first:

  1. If Chuck Norris wrote a PowerShell script it would be a one-liner, because Chuck Norris can do anything in one strike.
  2. Chuck Norris does not have SID history, because there is only one Chuck Norris.
  3. Chuck Norris' ACL only has one ACE: Chuck Norris – Full Control.

Do you remember SIDWALK?  This resource kit utility was written back in the NT 4.0 days to assist with domain migrations.  It used a mapping file to rewrite old SIDs with new SIDs across ACLs in a number of areas:  files shares, printer shares, registry paths, NTFS permissions, etc.  That utility is a teenager now (born 1998).  It's time we rewrite this and bring it up to date… in PowerShell.

Sidwalk V1.0
Copyright (C) 1998 Microsoft Corporation
Usage: Sidwalk <profile file> [<profile file> ..] [/t /f [<path>] /r /s /p /g /l <file>]

In part one of this series we will learn how to parse SIDs out of SDDL that we receive from Get-ACL.  This handy cmdlet works with many of these permissions.  Once we get the SIDs parsed out of SDDL, a future post will walk us through swapping them out and updating them.

Why SID Walker?  Why PowerShell?

One of my favorite Active Directory topics is SID history and its impact on token size.  In my former post I discussed creating a SID mapping file by querying SID history data from AD.  Eventually I plan to release a PowerShell module for remediating token size issues due to SID history.  This series on SID translation will be the centerpiece of that module.

Whether you are doing a domain migration, cleaning up SID history, or doing a search-and-replace for a group migration project this script will become your BFF.

Terms of Security

Most of us already know these terms, but let me define a few of these up front for anyone who is not familiar with them:

  • ACLAccess Control List – This is the list of permissions on a resource (file, folder, registry key, etc.).  ACLs contain ACEs.
  • ACEAccess Control Entry – This is an individual permission entry on an ACL.  An example would be TEXAS\cnorris Allow FullControl.
  • SDDLSecurity Descriptor Definition Language – This is a short-hand string that represents the entire ACL in a single, encoded string.  See MSDN for the specifics.
  • SIDSecurity Identifier – This is the unique security GUID that is assign to all security principles in a domain (users, computers, groups).  SIDs live in ACEs to represent the users who get the access.

Get-ACL

Editing ACLs with VBScript was a royal pain, but now we have the Get-ACL and Set-ACL cmdlets in PowerShell.  Setting and getting permissions on the file system, registry, and even AD organizational units has become a one-liner.  Look at the sample output for Get-ACL below:

PS C:\Users\ashley> Get-ACL | gm

TypeName: System.Security.AccessControl.DirectorySecurity

Name            MemberType      Definition
----            ----------      ----------
Access          CodeProperty    System.Security.AccessControl.Auth...
Sddl            CodeProperty    System.String Sddl{get=GetSddl;}
AccessToString  ScriptProperty  System.Object AccessToString {get=...

<<output trimmed>>

 

PS C:\Users\ashley> Get-ACL | fl *

PSPath                  : Microsoft.PowerShell.Core\FileSystem::C:\Users\ashley
PSParentPath            : Microsoft.PowerShell.Core\FileSystem::C:\Users
PSChildName             : ashley
PSDrive                 : C
PSProvider              : Microsoft.PowerShell.Core\FileSystem
AccessToString : NT AUTHORITY\SYSTEM Allow FullControl
BUILTIN\Administrators Allow FullControl
NADOMAIN\ashley Allow FullControl
AuditToString           :
Path                    : Microsoft.PowerShell.Core\FileSystem::C:\Users\ashley
Owner                   : NT AUTHORITY\SYSTEM
Group                   : NT AUTHORITY\SYSTEM
Access : {System.Security.AccessControl.FileSystemAccessRule, System.Security.AccessControl.FileSystemAccessRule, System.Security.AccessControl.FileSystemAccessRule}
Sddl : O:SYG:SYD:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;S-1-5-21-468525095-708123637-3513119021-43309)
AccessRightType         : System.Security.AccessControl.FileSystemRights
AccessRuleType          : System.Security.AccessControl.FileSystemAccessRule
AuditRuleType           : System.Security.AccessControl.FileSystemAuditRule
AreAccessRulesProtected : True
AreAuditRulesProtected  : False
AreAccessRulesCanonical : True
AreAuditRulesCanonical  : True

 

PS C:\Users\ashley> (Get-ACL).Access | fl *

FileSystemRights  : FullControl
AccessControlType : Allow
IdentityReference : NT AUTHORITY\SYSTEM
IsInherited       : False
InheritanceFlags  : ContainerInherit, ObjectInherit
PropagationFlags  : None

FileSystemRights  : FullControl
AccessControlType : Allow
IdentityReference : BUILTIN\Administrators
IsInherited       : False
InheritanceFlags  : ContainerInherit, ObjectInherit
PropagationFlags  : None

FileSystemRights  : FullControl
AccessControlType : Allow
IdentityReference : NORTHAMERICA\asmcglon
IsInherited       : False
InheritanceFlags  : ContainerInherit, ObjectInherit
PropagationFlags  : None

Get-ACL gives us the same ACL in three different properties:

  1. AccessToString – This is a simplified representation to get a summary of the ACEs.
  2. Access – This is an array of access rule objects with rich properties.  Enumerating these is the easiest way to script and report against ACLs.
  3. SDDL – This is the only place where you can see the actual SIDs behind the ACEs.  For our purposes of SID translation we'll have to use this property.

You can pass any file, registry, or OU path to Get-ACL.  Without the path it defaults to the local context.  See "Get-Help Get-ACL -Full" for more information.

SDDL

Although it looks cryptic, a few minutes of study on MSDN will help you crack the code and break down what is happening in the SDDL string.  Notice the string in the example below (note that the SID has been changed to protect the innocent):

O:SYG:SYD:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;S-1-5-21-468525095-708123637-3513119021-43309)

Look what happens when we break it down by the parentheses:

  1. O:SYG:SYD:P
  2. (A;OICI;FA;;;SY)
  3. (A;OICI;FA;;;BA)
  4. (A;OICI;FA;;;S-1-5-21-468525095-708123637-3513119021-43309)

The first piece contains owner and primary group information.  We're going to skip this for now; SIDs can live here, but we are targeting ACEs specifically.  The next three pieces delimited in parentheses correspond to each of the three ACEs in the ACL.  Note that the last one contains a SID, because it references a non-default account in the ACE.

Now let's break down that last ACE.  Observe that ACEs are delimited with semicolons:

  1. A;
  2. OICI;
  3. FA;
  4. ;
  5. ;
  6. S-1-5-21-468525095-708123637-3513119021-43309

The "A" means "allow".  "OI" and "CI" reference "ObjectInherit" and "ContainerInherit" as observed in the full output of the Access property listed above.  These denote that permissions set at this level will be inherited downward.  If this value contained "ID", then we would know that the values where inherited from above and not explicitly defined at this level.  You can read about the other specific SDDL coding on MSDN.

The last element of the ACE array contains the SID, and that is our target for this script.  The user or group SID will always live in the 5th array index of each ACE.  If there is no SID here, then you'll find an alpha code representing a well-known group.

Finding SIDs in ACLs

To summarize our breakdown of SDDL above you can find the ACL SIDs using the process below:

  1. Get the ACL
  2. Get the SDDL string of the ACL
  3. Split the SDDL on parentheses to get the ACEs
  4. Split the ACEs on semicolons
  5. Reference the last index of the ACE array to find the SID

While this may seem like a lot of effort that is exactly what PowerShell does best.  We'll wrap this process into a function to reuse and call for each ACL that we want to parse.

Why all of this trouble? Why not use the robust Access property array? Remember that we are translating SIDs, and this is the only place where we can find them. Both the regular account SID and the SID history will appear the same in the DOMAIN\username representation of the Access property ACE objects. This is an important distinction.

The Code

The script attached to this blog gives you a basic function to parse SDDL as described above.  As an educational script it includes much verbose output and comments.  Study the output to learn more about how ACLs, ACEs, and SDDL work in different security contexts (file system vs. registry vs. AD).  Customize it for your own needs.  Note that we ignore any ACEs that are inherited, because we only want to target ACEs with explicit permissions for translation.

Coming Up Next

The next post in this series will adapt this function into a larger script that mimics SIDWALK by translating SIDs on file resources.  We'll learn how to unleash your inner Chuck Norris round house kick on SID history.

  <#---------------------------------------------------------------------------
  Ashley McGlone, Microsoft PFE
  https://blogs.technet.com/b/ashleymcglone
  August, 2011
  Parse-SDDL function.
  https://msdn.microsoft.com/en-us/library/aa374928.aspx
  ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid
  0 ;1 ;2 ;3 ;4 ;5
  0 ;ID ;2 ;3 ;4 ;SID
  Split the SDDL on the characer: (
  Process indexes 1 to end
  Split on character ;
  If index 1 contains "ID" then ignore because inherited
  If index 5 contains a SID then process it
 ---------------------------------------------------------------------------#>            
function Parse-SDDL {            
    [CmdletBinding()]            
    param ([Parameter(valueFromPipelineByPropertyName=$true)]$SDDL)            
            
    $SDDLSplit = $SDDL.Split("(")            
            
    "`n---SDDL Split:"            
    $SDDLSplit            
            
    "`n---SDDL SID Parsing:"            
    # Skip index 0 where owner and/or primary group are stored            
    For ($i=1;$i -lt $SDDLSplit.Length;$i++) {            
        $ACLSplit = $SDDLSplit[$i].Split(";")            
        If ($ACLSplit[1].Contains("ID")) {            
            "Inherited"            
        } Else {            
            $ACLEntrySID = $null            
            # Remove the trailing ")"            
            $ACLEntry = $ACLSplit[5].TrimEnd(")")            
            # Parse out the SID using a handy RegEx            
            $ACLEntrySIDMatches = [regex]::Matches($ACLEntry,"(S(-\d+){2,8})")            
            $ACLEntrySIDMatches | ForEach-Object {$ACLEntrySID = $_.value}            
            If ($ACLEntrySID) {            
                $ACLEntrySID            
            } Else {            
                "Not inherited - No SID"            
            }            
        }            
    }            
    #return $null            
}            
            
# Experiment with these different path values to see what the ACL objects do            
$path = "C:\users\username\"          #Not inherited            
$path = "C:\users\username\desktop\"  #Inherited            
$path = "HKCU:\"                      #Not Inherited            
$path = "HKCU:\Software"              #Inherited            
$path = "HKLM:\"                      #Not Inherited            
            
"`n---Path:"            
$Path            
            
$ACL = Get-ACL $path            
            
"`n---Access To String:"            
$ACL.AccessToString            
            
"`n---Access entry details:"            
$ACL.Access | fl *            
            
"`n---SDDL:"            
$ACL.SDDL            
            
# Call with named parameter binding            
$ACL | Parse-SDDL            
# Call with parameter string            
#Parse-SDDL $ACL.SDDL            
            
# ><>

Parse SDDL for SIDs.p-s-1.txt