beawolf - Fotolia

Find and lock down lax Windows share permissions

With help from PowerShell, you can identify the shares that need adjustments across your infrastructure then use a script to fix them to protect sensitive data.

Keeping your data secure and away from unauthorized users is a complex task, which can be even more difficult if a default setting in Windows gets in your way.

Trying to secure Windows share permissions is a big challenge due to a setting called bypass traverse checking that the OS enables by default. This setting gives access to folders even if the user does not have access rights to any of its parents.

We can remove this authorization with group policy object setting, but it's there for a reason. Without this setting enabled, you will see a big drop in performance since Windows will check every parent folder to see if the user is allowed to go to the target.

This article will explain how to create a report on Windows share permissions to determine which users have excessive authorizations and how to mend it using PowerShell and Sysinternals.

To continue with this article, there are a few requirements:

  • Have at least Windows Management Framework version 4.0.
  • Have one or several file shares to scan running on Windows 2012 R2 and newer. (This tutorial might work on older versions of Windows Server, but it's not tested.)
  • Have Local Administrator rights on the file servers or computers to scan.
  • Have PowerShell remoting and Windows Management Instrumentation on the computers to scan.
  • Have basic knowledge about NTFS and file shares.

Gathering file shares and their authorized users

First, we need to find the file shares on the servers and client systems. We could do this either by using the Get-SmbShare command or by calling the win32_share namespace using either Get-CimInstance or Get-WmiObject.

For this example, Get-WmiObject is the preferred way to fetch our shares because it's a more streamlined approach. Launch the PowerShell Terminal as an admin on a file server and enter the following command:

Get-WMIObject -Class win32_share

Name       Path                              Description   
----       ----                              -----------   
MyShare    C:\demo\share                     Demo share                   
ADMIN$     C:\WINDOWS                        Remote Admin  
C          C:\                                             
C$         C:\                               Default share 
D$         D:\                               Default share 
E$         E:\                               Default share 
IPC$                                         Remote IPC    
print$     C:\WINDOWS\system32\spool\drivers Printer Drivers
scripts    C:\scripts

The PowerShell command outputs all the shares, but it doesn't show the users with access to them. That's because the Windows share permissions reside in another namespace called Win32_LogicalShareSecuritySetting:

Get-WmiObject -Class Win32_LogicalShareSecuritySetting

This resulting output doesn't tell us much either. We need a more comprehensive PowerShell script to generate something more useful:

# Get all shares on the computer
$Shares = Get-WMIObject -Class win32_share

# Variable to processed shares to.
$NetworkShares = [System.Collections.Generic.List[PSCustomObject]]::new()

# Ignore default shares by filtering out '2147483648'
foreach ($Share in $Shares | ? {$_.Type -ne '2147483648' -and $_.Name -ne 'print$'}) {

    # Create an object that we'll return
    $ShareObject = [PSCustomObject]@{
        Name = $Share.Name
        Description = $Share.Description
        LocalPath = $Share.Path
        ACL = [System.Collections.ArrayList]::new()

    }
    # Get the security settings for the share
    $ShareSecurity = Get-WmiObject -Class Win32_LogicalShareSecuritySetting -Filter "name='$($Share.Name)'"

    # If security settings exists, build a list with ACLs
    if($Null -ne $ShareSecurity){
        Try{ 
            $SecurityDescriptor = $ShareSecurity.GetSecurityDescriptor().Descriptor   
       
            foreach($AccessControl in $SecurityDescriptor.DACL){  
           
                $UserName = $AccessControl.Trustee.Name     
                $Trustee = $AccessControl.Trustee

                If ($Trustee.Domain -ne $Null) {
                    $UserName = "$($Trustee.Domain)\$UserName"
                }
           
                If ($Trustee.Name -eq $Null) {
                    $UserName = $Trustee.SIDString
                }

                $ShareObject.ACL.Add(
                    [System.Security.AccessControl.FileSystemAccessRule]::new(
                        $UserName,
                        $AccessControl.AccessMask,
                        $AccessControl.AceType
                    )
                ) | Out-Null
            }

            # Return the share object with the ACLs
            $NetworkShares.Add($ShareObject)
        }
        Catch{
            Write-Error $Error[0]
        }
    }
    Else {
        Write-Information "No permissions found for $($Share.Name) on $ComputerName"
    }
}

The content of the $NetworkShares variable should end up looking similar to the following:

PS51> $NetworkShares

Name       Description LocalPath     ACL                                                                                                    
----       ----------- ---------     ---                                                                                                    
DemoShare  Demo share  C:\demo\share {System.Security.AccessControl.FileSystemAccessRule}                                                   
scripts                C:\scripts    {System.Security.AccessControl.FileSystemAccessRule, System.Security.AccessControl.FileSystemAccessRule}

PS51> $NetworkShares[0].ACL

FileSystemRights  : FullControl
AccessControlType : Allow
IdentityReference : Everyone
IsInherited       : False
InheritanceFlags  : None
PropagationFlags  : None

We've successfully gathered data about our Windows share permissions, showing who has access to what. That might not be enough because administrators usually assign network share permissions on the NTFS level, not the network share level.

We also need to check the files and folders in the share if there are excessive permissions for other groups, such as Everyone or Domain Users.

Scanning file permissions using AccessChk

We have a list of our file shares. Next, we need to get all the file permissions. The fastest way to do this is by using the AccessChk file utility from the Sysinternals suite and parse the output with PowerShell.

Put AccessChk on your file server and copy the AccessChk64.exe file to your system32 folder. You can either download the utility from the link above or use the following PowerShell code to download it and copy it to your system32 folder:

Invoke-WebRequest -OutFile $env:TEMP\AccessChk.zip -Uri https://download.sysinternals.com/files/AccessChk.zip 
Expand-Archive -Path $env:TEMP\AccessChk.zip -DestinationPath $env:TEMP -Force
Copy-Item -Path $env:TEMP\AccessChk64.exe C:\Windows\System32\AccessChk64.exe

We can use PowerShell to create a wrapper function around AccessChk for use in a script:

Function Invoke-AccessChk {
    param(
        $Path,
        $Principals,
        $AccessChkPath = "$env:windir\system32\accesschk64.exe",
        [switch]$DirectoriesOnly,
        [switch]$AcceptEula
       
    )

    # Accept EULA
    if($AcceptEula){
        & $AccessChkPath /accepteula | Out-Null
    }

    $Argument = "uqs"
    if($DirectoriesOnly){
        $Argument = "udqs"
    }

    $Output = & $AccessChkPath -nobanner -$Argument $Path
   
    Foreach($Row in $Output){

        # If it's a row with a file path output the previous object and create a new one
        if($Row -match "^\S"){
            If($Null -ne $Object){
                if($Object.Access.Keys.Count -gt 0){
                    $Object
                }
            }
            $Object = [PSCustomObject]@{
                Path = $Row
                Access = @{}
            }
        }

        # If it's a row with permissions
        if($Row -match "^  [R ][W ]"){
            If($Row -match ($Principals -replace "\\",'\\' -join "|")){

                $Row -match "^  (?<Read>[R ])(?<Write>[W ]) (?<Principal>.*)" | Out-Null       
               
                $Object.Access[$Matches.Principal] = @{
                    Read = $Matches.Read -eq 'R'
                    Write = $Matches.Read -eq 'W'
                }

            }
        }
    }
    # If it's the last row - output the object once more
    if($Object.Access.Keys.Count -gt 0){
        $Object
    }
}

We can now run Invoke-AccessChk with the network shares stored in the $NetworkShares variable from the previous step. We add to a list of the security principals -- without "domain" -- to find:

# Invoke-AccessChk will only output files/folders where the following principals have permission:
$RiskPrincipals = @(
    'Everyone',
    'Domain Users',
    'Domain Computers',
    'Authenticated Users',
    'Users'
)

$RiskyPermissions = Foreach($NetworkShare in $NetworkShares | Select -First 1){
   
    # Only scan directory if it's shared to one of the principals in $RiskPrincipals
    $RiskPrincipalExist = $Null -ne ($NetworkShare.ACL.IdentityReference.Value -replace ".*\\" | ? {$_ -in $RiskPrincipals})
   
    if($RiskPrincipalExist){
        Invoke-AccessChk -Path $NetworkShare.LocalPath -Principals $RiskPrincipals
    }

}

The $RiskyPermissions variable will give output similar to this:

PS51> $RiskyPermissions

Path                                          Access                                          
----                                          ------                                           
C:\demo\share\File1.txt                       {BUILTIN\Users, NT AUTHORITY\Authenticated Users}
C:\demo\share\Folder1\picture.png             {NT AUTHORITY\Authenticated Users}              
C:\demo\share\Folder1\Folder2                 {NT AUTHORITY\Authenticated Users}

PS51> $RiskyPermissions[0].Access

Creating a report from several computers and servers

Thus far, you can get a list of all the file shares and check all the files with the PowerShell wrapper for Invoke-AccessChk. One of PowerShell's many strengths is its ability to scale. PowerShell remoting will take the code we've produced to the next level to gather the information from several computers at once.

First, we need a list of computers and servers to scan. If possible, the easiest way is through the Active Directory module from RSAT:

$Computers = (Get-ADComputer -Filter *).dnsHostName

This method might not be an option in larger environments that are heavily segmented. Another approach is to get data from your configuration management database or entering it manually using the following example:

$Computers = @(
    'Server1',
    'Server2',
    'Server3',
    'Server4',
    'Server5',
  'PC1'
  # etc
)

Now it's time to tie all these components in a script that uses PowerShell background jobs to do the following actions on the machines specified in the $Computers parameter:

  • Get all shares that are shared out to one of the principals in $RiskPrincipals.
  • Download AccessChk if it does not already exist.
  • Check the NTFS permission of all shares gathered by AccessChk.
  • Return an object with a list with all files where the security principals in $RiskPrincipals have either read or write permissions.

The computer running the script will then collect the results of all jobs and output it to a CSV file with the name ShareAccessReport.

Remember to run the following as an admin on a computer that has network access to said machines and to accept the EULA for AccessChk by changing $AcceptEula to true:

$Computers = @(
    'Server-1',
    'Server-2',
    'PC-1'
)

# Accept EULA for AccessChk
# CHANGE TO TRUE
$AcceptEula = $false

if(!$AcceptEula){
    Write-Warning "Did not accept EULA for AccessChk, can't continue"
  break
}

# Principals  that we want to scan for
$RiskPrincipals = @(
    'Everyone',
    'Domain Users',
    'Domain Computers',
    'Authenticated Users',
    'Users'
)

# List of shares that we want to ignore.
# Setting a share name tied to it just in case since it should almost always be that path
$IgnoreShares = @(
    'print$'
)

# Scriptblock that we'll send with Invoke-Command
$Scriptblock = {

    $RiskPrincipals = $args[0].RiskPrincipals
    $IgnoreShares = $args[1].IgnoreShares
    $AcceptEula = $args[2].AcceptEula

    # Functions to download and use AccessChk
    # It utilizes a shell object instead of Expand-Archive for backward compatability
    Function Download-AccessChk {
        param(
            $Url = "https://download.sysinternals.com/files/AccessChk.zip",
            $Dest = $env:temp
        )
        if(Test-Path "$dest\accesschk.zip"){
            rm $Dest\AccessCHK.zip -Force
        }
        (New-Object System.Net.WebClient).DownloadFile($url, "$env:temp\AccessChk.zip")
        $Shell = New-Object -ComObject Shell.Application
        $Zip = $shell.NameSpace("$env:temp\AccessChk.zip")
        $Destination = $shell.NameSpace("$env:windir\system32\")

        $copyFlags = 0x00
        $copyFlags += 0x04
        $copyFlags += 0x10

        $Destination.CopyHere($Zip.Items(), $copyFlags)
    }

    # The function that utilizes accesschk from part 2
    Function Invoke-AccessChk {
        param(
            $Path,
            $Principals,
            $AccessChkPath = "$env:windir\system32\accesschk64.exe",
            [switch]$DirectoriesOnly,
            [switch]$AcceptEula
       
        )

        if(!(Test-Path "$env:windir\system32\accesschk64.exe")){
            Download-AccessChk
        }

        # Accept EULA
        if($AcceptEula){
            & $AccessChkPath /accepteula | Out-Null
        }

        $Argument = "uqs"
        if($DirectoriesOnly){
            $Argument = "udqs"
        }

        $Output = & $AccessChkPath -nobanner -$Argument $Path
   
        Foreach($Row in $Output){

            # If it's a row with a file path output the previous object and create a new one
            if($Row -match "^\S"){
                If($Null -ne $Object){
                    if($Object.Access.Keys.Count -gt 0){
                        $Object
                    }
                }
                $Object = [PSCustomObject]@{
                    Path = $Row
                    Access = @{}
                }
            }

            # If it's a row with permissions
            if($Row -match "^  [R ][W ]"){
                If($Row -match ($Principals -replace "\\",'\\' -join "|")){

                    $Row -match "^  (?<Read>[R ])(?<Write>[W ]) (?<Principal>.*)" | Out-Null       
               
                    $Object.Access[$Matches.Principal] = @{
                        Read = $Matches.Read -eq 'R'
                        Write = $Matches.Read -eq 'W'
                    }

                }
            }
        }
        # If it's the last row - output the object once more
        if($Object.Access.Keys.Count -gt 0){
            $Object
        }
    }


    # Get all the shares by using WMI
    $Shares = Get-WmiObject -Class win32_share

    # Create an object that we will later return when we're done
    $ReturnObject = [PSCustomObject]@{
        ComputerName = $ComputerName
        NetworkShares = [System.Collections.Generic.List[PSCustomObject]]::new()
        AccessibleObjects = @{}
    }

    # Ignore default shares by filtering out '2147483648'
    # Ignore shares in $IgnoreShares
    foreach ($Share in $Shares | ? {$_.Type -ne '2147483648'} | ? {$_.Name -notin $IgnoreShares}) {
        $ShareObject = [PSCustomObject]@{
            Name = $Share.Name
            Description = $Share.Description
            LocalPath = $Share.Path
            ACL = [System.Collections.ArrayList]::new()

        }

        $ShareSecurity = Get-WMIObject -Class Win32_LogicalShareSecuritySetting -Filter "name='$($Share.Name)'"
        if($Null -ne $ShareSecurity){
            Try{ 
                $SecurityDescriptor = $ShareSecurity.GetSecurityDescriptor().Descriptor   
       
                foreach($AccessControl in $SecurityDescriptor.DACL){  
           
                    $UserName = $AccessControl.Trustee.Name     
                    $Trustee = $AccessControl.Trustee

                    If ($Trustee.Domain -ne $Null) {
                        $UserName = "$($Trustee.Domain)\$UserName"
                    }
           
                    If ($Trustee.Name -eq $Null) {
                        $UserName = $Trustee.SIDString
                    }
           
                    $ShareObject.ACL.Add(
                        [System.Security.AccessControl.FileSystemAccessRule]::new(
                            $UserName,
                            $AccessControl.AccessMask,
                            $AccessControl.AceType
                        )
                    ) | Out-Null
                }

                # Only add network share if it contains a risk user/group
           
                $Match = $False
                Foreach($IdentityReference in $ShareObject.ACL.IdentityReference.Value){
                    Foreach($Pattern in $RiskPrincipals){
                        if($IdentityReference -Match $Pattern){               
                            $Match = $True
                        }
                    }
                }
                if($Match){
                    $ReturnObject.NetworkShares.Add($ShareObject)   
                }
                Else {
                    Write-Verbose "No match for risky groups, not adding"
                }
              
            }
            Catch{
                Write-Error $Error[0]
            }
        }
        Else {
            Write-Information "No permissions found for $($Share.Name) on $ComputerName"
        }
     
    }
    # Get all files from NetworkShares where a principal from $RiskPrincipals have either read or write access
    $ReturnObject.NetworkShares | Foreach {
        $ReturnObject.AccessibleObjects[$_.Name] = Invoke-AccessChk -Path $_.LocalPath -Principals $RiskPrincipals -AcceptEula:$AcceptEula
    }

    # Done! Lets return the returnobject:
    $ReturnObject
}

# To add to the argument list of Invoke-Job because the remote PowerShell job doesn't have access to our variable space.
$InvokeParam = @{
    RiskPrincipals = $RiskPrincipals
    IgnoreShares = $IgnoreShares
    AcceptEula     = $AcceptEULA
}

# Start jobs
$Job = Invoke-Command -AsJob -ComputerName $Computers -ArgumentList $InvokeParam -ScriptBlock $Scriptblock

# Wait for jobs to finish
$Job | Wait-Job

# Collect data from all jobs
$Output = Get-Job | Receive-Job

# Output the output into a CSV
$ToCSV = Foreach($Result in $Output){

    Foreach($Key in $Result.AccessibleObjects.Keys) {

        # For using Select-Object expressions to get the data out of $Result.AccessibleObjects
        # The downside of working a lot with hashtables
        $ReadAccess = @{
            Name='ReadAccess'
            Expression={
                $Base = $_.Access
                ($Base.Keys | ? {$Base[$_].Read}) -join ","
            }
        }

        $WriteAccess = @{
            Name='WriteAccess'
            Expression={
                $Base = $_.Access
                ($Base.Keys | ? {$Base[$_].Write}) -join ","
            }
        }
       
        # Select from AccessibleObjects and create property for the principals with ReadAccess and WriteAccess
        $Result.AccessibleObjects[$Key] | Select @{Name='ShareName';Expression={$Key}},Path,$ReadAccess,$WriteAccess
    }
}
# Export the CSV
$ToCSV | Export-Csv -Path .\ShareAccessReport.csv

When the PowerShell job finishes, it will create a full report of the access of the principals in the $RiskyPrincipals variable.

Fixing Windows share permissions

After you review the CSV and find the permissions that need adjusting, there are two ways to correct them. If there are only a few, then the best way is through the GUI. But if there are thousands, then the following command will use the CSV output to speed this along:

# This needs to run locally on the server with the file share.

$UserToRemove = 'Guest'
$CSV = Import-Csv -Path .\ShareAccessReport.csv | ? {}
$CSV | ? {$_.ComputerName -eq $env:COMPUTERNAME} | Foreach {
    $ACL = Get-Acl -Path $_.Path
    $ACL.Access | ? {($_.IdentityReference.Value -replace '.*\\') -eq $UserToRemove} | Foreach {
        $ACL.Access.Remove($_)
    }
}

This PowerShell script will remove all permissions for the Guest security principal.

The first report will usually bring a lot of work though because it will discover a lot of oddities and risks when it comes to your Windows share permissions. But running a solution like this regularly, especially targeted toward shares with sensitive information, will pay off in the end.

Dig Deeper on IT operations and infrastructure management