IISSecurity.psm1
Write-Verbose 'Importing from [C:\MyProjects\IISSecurity\IISSecurity\private]' # .\IISSecurity\private\CheckPathExists.ps1 function CheckPathExists([string] $Path) { Set-StrictMode -Version Latest if ([string]::IsNullOrWhiteSpace($Path)) { return $true } if (-not(Test-Path $Path)) { throw "Path '$Path' not found" } $true } # .\IISSecurity\private\Invoke-WithRetry.ps1 function Invoke-WithRetry { param( [Parameter(Mandatory)] [ScriptBlock] $Command, [int]$MaxRetries = 3, [int]$SleepBetweenFailures = 2 ) [int]$attemptCount = 0 [bool]$operationIncomplete = $true while ($operationIncomplete -and $attemptCount -lt ($MaxRetries + 1)) { $attemptCount += 1 if ($attemptCount -ge 2) { Write-Verbose "Waiting for $SleepBetweenFailures seconds before retrying..." Start-Sleep -s $SleepBetweenFailures Write-Verbose "Retrying..." } try { & $Command $operationIncomplete = $false } catch [System.Exception] { if ($attemptCount -lt ($MaxRetries)) { Write-Warning ("Attempt $attemptCount of $MaxRetries failed: " + $_.Exception.Message) } else { throw } } } } # .\IISSecurity\private\Start-Executable.ps1 function Start-Executable { <# .SYNOPSIS Execute executable and pipe the output to the powershell pipeline .DESCRIPTION executable and pipe the output to the powershell pipeline .PARAMETER FilePath The path to the executable .PARAMETER ArgumentList The arguments to pass to the executable .EXAMPLE $params = @( '`"C:\Some Path\node_modules\gulp\bin\gulp.js`"' '--gulpfile' "`"$somePath`"" ) Start-Executable node $params Description ----------- Runs nodeJS passing the JS file (gulp.js) to execute and the arguments that this JS file requires .NOTES $LASTEXITCODE PS variable will be assigned the exit returned by the invocation of the executable #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [String] $FilePath, [String[]] $ArgumentList ) begin { Set-StrictMode -Version 'Latest' $callerEA = $ErrorActionPreference $ErrorActionPreference = 'Stop' } process { try { $OFS = " " $process = New-Object System.Diagnostics.Process $process.StartInfo.FileName = $FilePath $process.StartInfo.Arguments = $ArgumentList $process.StartInfo.UseShellExecute = $false $process.StartInfo.RedirectStandardOutput = $true $process.StartInfo.RedirectStandardError = $true if ($PSCmdlet.ShouldProcess("$FilePath $ArgumentList", 'Execute command line') -and $process.Start() ) { $output = $process.StandardOutput.ReadToEnd() -replace "\r\n$", "" $pipelineOutput = if ( $output ) { if ( $output.Contains("`r`n") ) { $output -split "`r`n" } elseif ( $output.Contains("`n") ) { $output -split "`n" } else { $output } } $pipelineOutput $process.WaitForExit() & "$Env:SystemRoot\system32\cmd.exe" ` /c exit $process.ExitCode if ($process.ExitCode -gt 0) { $errorOutput = $process.StandardError.ReadToEnd() if ([string]::IsNullOrWhiteSpace($errorOutput)) { $errorOutput = $pipelineOutput | Where-Object { $_ -Like '*error*' } | Select-Object -Last 5 | Out-String } $errorMsg = "Error executing '$FilePath'$([System.Environment]::NewLine)" $errorMsg += "Command parameters '$ArgumentList'$([System.Environment]::NewLine)" $errorMsg += "Exit code: $($process.ExitCode)$([System.Environment]::NewLine)" if (![string]::IsNullOrWhiteSpace($errorOutput)) { $errorMsg += "Error details:$([System.Environment]::NewLine)" $errorMsg += $errorOutput } throw [System.Exception]::new($errorMsg) } } } catch { Write-Error -ErrorRecord $_ -EA $callerEA } } } # .\IISSecurity\private\Test-SID.ps1 function Test-SID { param( [Parameter(Mandatory)] [string] $Name ) try { [System.Security.Principal.SecurityIdentifier]::new($Name) $true } catch [System.ArgumentException] { $false } } # .\IISSecurity\private\ValidateAclPaths.ps1 function ValidateAclPaths { param( [Parameter(Mandatory)] [ValidateNotNull()] [PsCustomObject[]] $Permissions, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ErrorMessage ) Set-StrictMode -Version Latest $Permissions | Select-Object -Exp Path | Where-Object { -not(Test-Path $_ ) } -OutVariable missingPaths | ForEach-Object { Write-Warning "Path not found: '$_'" } if ($missingPaths) { throw $ErrorMessage } } Write-Verbose 'Importing from [C:\MyProjects\IISSecurity\IISSecurity\public]' # .\IISSecurity\public\Get-IISSiteDesiredAcl.ps1 function Get-IISSiteDesiredAcl { <# .SYNOPSIS Returns the least privilege file/folder permissions that should be granted to an IIS AppPool useracount .DESCRIPTION Returns the least privilege file/folder permissions that should be granted to an IIS AppPool useracount .PARAMETER SitePath The physical Website path. Omit this path when configuring the permissions of a child web application only .PARAMETER AppPath The physical Web application path. A path relative to SitePath can be supplied. Defaults to SitePath .PARAMETER ModifyPaths Additional paths to remove permissions. Path(s) relative to AppPath can be supplied .PARAMETER ExecutePaths Additional paths to remove permissions. Path(s) relative to AppPath can be supplied .PARAMETER SiteShellOnly Permissions used for 'SitePath' should only be to that folder and it's files but NOT subfolders .PARAMETER SkipTempAspNetFiles Permissions should not be granted to Temporary ASP.NET Files folder(s) .EXAMPLE Get-CaccaIISSiteDesiredAcl -SitePath 'C:\inetpub\wwwroot' Description ----------- Return file permissions for a site .EXAMPLE Get-CaccaIISSiteDesiredAcl -SitePath 'C:\inetpub\wwwroot' -AppPath 'MyWebApp1' Description ----------- Return file permissions for a site and child web application .EXAMPLE Get-CaccaIISSiteDesiredAcl -AppPath 'C:\Apps\MyWebApp1' -ModifyPaths 'App_Data' Description ----------- Return file permissions for a child web application only #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [string] $SitePath, [Parameter(ValueFromPipeline)] [string] $AppPath, [Parameter(ValueFromPipeline)] [ValidateNotNull()] [string[]] $ModifyPaths = @(), [Parameter(ValueFromPipeline)] [ValidateNotNull()] [string[]] $ExecutePaths = @(), [switch] $SiteShellOnly, [switch] $SkipTempAspNetFiles ) begin { Set-StrictMode -Version Latest Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $callerEA = $ErrorActionPreference $ErrorActionPreference = 'Stop' function ToIcaclsPermission ([string] $Path, [string] $Permission, [string] $Description) { [PsCustomObject] @{ Path = $Path Permission = $Permission Description = $Description } } function Add([System.Collections.Specialized.OrderedDictionary]$Results, [PSCustomObject] $Permission) { $key = Join-Path $Permission.Path '\' if ($Results.Contains($key)) { $original = $Results[$key] Write-Warning "Path '$($Permission.Path)' is the target of multiple permission assignments; was: '$($original.Description)'; now: '$($Permission.Description)'" } $Results[$key] = $Permission } } process { try { if ([string]::IsNullOrWhiteSpace($SitePath) -and ![System.IO.Path]::IsPathRooted($AppPath)) { throw "AppPath must be a full path if SitePath is omitted" } if ([string]::IsNullOrWhiteSpace($SitePath) -and [string]::IsNullOrWhiteSpace($AppPath)) { throw "SitePath and/or AppPath must be supplied" } if ([string]::IsNullOrWhiteSpace($SitePath) -and $SiteShellOnly.IsPresent) { throw "SiteShellOnly must be used in conjunction with the SitePath parameter" } # ensure consistent trailing backslashs if (![string]::IsNullOrWhiteSpace($SitePath)) { $SitePath = Join-Path $SitePath '\' } if (![string]::IsNullOrWhiteSpace($AppPath)) { $AppPath = Join-Path $AppPath '\' } $appFullPath = if ([string]::IsNullOrWhiteSpace($SitePath) -or [System.IO.Path]::IsPathRooted($AppPath)) { $AppPath } else { Join-Path $SitePath $AppPath } $getAppSubPath = { if ([System.IO.Path]::IsPathRooted($_)) { $_ } else { Join-Path $appFullPath $_ } } $permissions = [ordered]@{} if ([string]::IsNullOrWhiteSpace($AppPath) -or $SitePath -eq $appFullPath) { # Site only... if ($SiteShellOnly) { Add $permissions (ToIcaclsPermission $SitePath '(OI)(NP)R' 'read permission to this folder and files (no inherit)') } else { Add $permissions (ToIcaclsPermission $appFullPath '(OI)(CI)R' 'read permission (inherit)') } } elseif ([string]::IsNullOrWhiteSpace($SitePath)) { # App only... Add $permissions (ToIcaclsPermission $appFullPath '(OI)(CI)R' 'read permission (inherit)') } else { # Site and app... if ($PSBoundParameters.ContainsKey('SiteShellOnly') -and !$SiteShellOnly) { Add $permissions (ToIcaclsPermission $SitePath '(OI)(CI)R' 'read permission (inherit)') } else { Add $permissions (ToIcaclsPermission $SitePath '(OI)(NP)R' 'read permission to this folder and files (no inherit)') } Add $permissions (ToIcaclsPermission $appFullPath '(OI)(CI)R' 'read permission (inherit)') } $ExecutePaths | ForEach-Object $getAppSubPath | ForEach-Object { Add $permissions (ToIcaclsPermission $_ '(OI)(CI)(RX)' 'read+execute permission (inherit)') } $ModifyPaths | ForEach-Object $getAppSubPath | ForEach-Object { Add $permissions (ToIcaclsPermission $_ '(OI)(CI)M' 'modify permission (inherit)') } if (!$SkipTempAspNetFiles) { Get-TempAspNetFilesPath | ForEach-Object { Add $permissions (ToIcaclsPermission $_ '(OI)(CI)R' 'read permission (inherit)') } } $permissions.Values } catch { Write-Error -ErrorRecord $_ -EA $callerEA } } } # .\IISSecurity\public\Get-TempAspNetFilesPath.ps1 function Get-TempAspNetFilesPath { <# .SYNOPSIS Return path(s) to every 'Temporary ASP.NET Files' folder .DESCRIPTION Return path(s) to every 'Temporary ASP.NET Files' folder .EXAMPLE Get-CaccaTempAspNetFilesPaths #> [CmdletBinding()] param() Set-StrictMode -Version Latest Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $aspNetTempFolder = 'C:\Windows\Microsoft.NET\Framework*\v*\Temporary ASP.NET Files' Get-ChildItem $aspNetTempFolder | Select-Object -Exp FullName } # .\IISSecurity\public\Remove-IISSiteAcl.ps1 function Remove-IISSiteAcl { <# .SYNOPSIS Remove the permissions that Set-IISSiteAcl grants to the AppPool Identity .DESCRIPTION Remove the permissions that Set-IISSiteAcl grants to the AppPool Identity .PARAMETER SitePath The physical Website path. Omit this path when configuring the permissions of a child web application only .PARAMETER AppPath The physical Web application path. A path relative to SitePath can be supplied. Defaults to SitePath .PARAMETER AppPoolIdentity The name of the User account whose permissions are to be removed .PARAMETER ModifyPaths Additional paths to remove permissions. Path(s) relative to AppPath can be supplied .PARAMETER ExecutePaths Additional paths to remove permissions. Path(s) relative to AppPath can be supplied .PARAMETER SiteShellOnly Ignored .PARAMETER SkipMissingPaths Skip check that the file path(s) supplied must exist? .PARAMETER SkipTempAspNetFiles Don't remove permissions from 'Temporary ASP.NET Files'? .EXAMPLE Remove-CaccaIISSiteAcl -SitePath 'C:\inetpub\wwwroot' -AppPoolIdentity 'IIS AppPool\MyWebApp1-AppPool' Description ----------- Remove AppPool Identity file permissions from a site .EXAMPLE Remove-CaccaIISSiteAcl -SitePath 'C:\inetpub\wwwroot' -AppPath 'MyWebApp1' -AppPoolIdentity 'IIS AppPool\MyWebApp1-AppPool' Description ----------- Remove AppPool Identity file permissions from site and a child web application .EXAMPLE Remove-CaccaIISSiteAcl -AppPath 'C:\Apps\MyWebApp1' -AppPoolIdentity 'mydomain\myuser' -ModifyPaths 'App_Data' Description ----------- Remove AppPool Identity file permissions from a child web application only #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, ValueFromPipeline)] [string] $AppPoolIdentity, [Parameter(ValueFromPipeline)] [ValidateScript( {CheckPathExists $_})] [string] $SitePath, [Parameter(ValueFromPipeline)] [string] $AppPath, [Parameter(ValueFromPipeline)] [ValidateNotNull()] [string[]] $ModifyPaths = @(), [Parameter(ValueFromPipeline)] [ValidateNotNull()] [string[]] $ExecutePaths = @(), [switch] $SiteShellOnly, [switch] $SkipMissingPaths, [switch] $SkipTempAspNetFiles ) begin { Set-StrictMode -Version Latest Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $callerEA = $ErrorActionPreference $ErrorActionPreference = 'Stop' } process { try { $paths = @{ SitePath = $SitePath AppPath = $AppPath ModifyPaths = $ModifyPaths ExecutePaths = $ExecutePaths SiteShellOnly = $SiteShellOnly SkipTempAspNetFiles = $SkipTempAspNetFiles } $permissions = Get-IISSiteDesiredAcl @paths | Where-Object { $SkipMissingPaths -eq $false -or (Test-Path $_.Path) } ValidateAclPaths $permissions 'Cannot remove permissions; missing paths detected' $permissions | Remove-UserFromAcl -IdentityReference $AppPoolIdentity } catch { Write-Error -ErrorRecord $_ -EA $callerEA } } } # .\IISSecurity\public\Remove-UserFromAcl.ps1 #Requires -RunAsAdministrator function Remove-UserFromAcl { <# .SYNOPSIS Remove a Windows account from the ACL of a specified file path .DESCRIPTION Remove a Windows account from the ACL of a specified file path. *IMPORTANT* Any ACL permissions inherited from paths higher in the tree will NOT be removed .PARAMETER IdentityReference The Windows account to remove .PARAMETER Path The target file path .EXAMPLE Remove-CaccaUserFromAcl 'mydomain\myuser' C:\Some\Path #> [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] $IdentityReference, [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)] [string] $Path ) begin { Set-StrictMode -Version 'Latest' Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $callerEA = $ErrorActionPreference $ErrorActionPreference = 'Stop' } process { try { if ($PSCmdlet.ShouldProcess($Path, "Removing '$IdentityReference'")) { # note: Where-Object we're ignoring errors. In essence we are skipping any user object # (IdentityReference) that can no longer be translated to a string, probably because it is "unknown" $acl = (Get-Item $_.Path).GetAccessControl('Access') $acl.Access | Where-Object { $_.IsInherited -eq $false -and $_.IdentityReference -eq $IdentityReference } -EA Ignore | ForEach-Object { $acl.RemoveAccessRuleAll($_) } Set-Acl -Path ($_.Path) -AclObject $acl } } catch { Write-Error -ErrorRecord $_ -EA $callerEA } } } # .\IISSecurity\public\Set-IISSiteAcl.ps1 function Set-IISSiteAcl { <# .SYNOPSIS Set least privilege file/folder permissions to an IIS AppPool Useracount .DESCRIPTION Set least privilege file folder permissions on site and/or application file path to the useraccount that is configured as the identity of an IIS AppPool. These bare minium permissions include: - SitePath: Read 'This folder', file and subfolder permissions (inherited) - Note: use 'SiteShellOnly' to reduce these permissions to just the folder and files but NOT subfolders - AppPath: Read 'This folder', file and subfolder permissions (inherited) - Temporary ASP.NET Files: Read 'This folder', file and subfolder permissions (inherited) - ModifyPaths: modify 'This folder', file and subfolder permissions (inherited) - ExecutePaths: read+execute file (no inherit) .PARAMETER SitePath The physical Website path. Omit this path when configuring the permissions of a child web application only .PARAMETER AppPath The physical Web application path. A path relative to SitePath can be supplied. Defaults to SitePath .PARAMETER AppPoolIdentity The name of the User account whose permissions are to be granted .PARAMETER ModifyPaths Additional paths to grant modify (inherited) permissions. Path(s) relative to AppPath can be supplied .PARAMETER ExecutePaths Additional paths to grant read+excute permissions. Path(s) relative to AppPath can be supplied .PARAMETER SiteShellOnly Grant permissions used for 'SitePath' to only that folder and it's files but NOT subfolders .PARAMETER MaxRetries Number of retry attempts when assigning permissions .EXAMPLE Set-CaccaIISSiteAcl -SitePath 'C:\inetpub\wwwroot' -AppPoolIdentity 'MyWebApp1-AppPool' Description ----------- Grant site file permissions to AppPoolIdentity .EXAMPLE Set-CaccaIISSiteAcl -SitePath 'C:\inetpub\wwwroot' -AppPath 'MyWebApp1' -AppPoolIdentity 'IIS AppPool\MyWebApp1-AppPool' Description ----------- Grant site and chid application file permissions to AppPoolIdentity .EXAMPLE Set-CaccaIISSiteAcl -AppPath 'C:\Apps\MyWebApp1' -AppPoolIdentity 'mydomain\myuser' -ModifyPaths 'App_Data' Description ----------- Grant child application only file permissions to a specific user. Include folders that require modify permissions #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, ValueFromPipeline)] [string] $AppPoolIdentity, [Parameter(ValueFromPipeline)] [ValidateScript( {CheckPathExists $_})] [string] $SitePath, [Parameter(ValueFromPipeline)] [string] $AppPath, [Parameter(ValueFromPipeline)] [ValidateNotNull()] [string[]] $ModifyPaths = @(), [Parameter(ValueFromPipeline)] [ValidateNotNull()] [string[]] $ExecutePaths = @(), [switch] $SiteShellOnly, [ValidateRange(0, 10)] [int]$MaxRetries = 3 ) begin { Set-StrictMode -Version Latest Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $callerEA = $ErrorActionPreference $ErrorActionPreference = 'Stop' } process { try { $paths = @{ SitePath = $SitePath AppPath = $AppPath ModifyPaths = $ModifyPaths ExecutePaths = $ExecutePaths SiteShellOnly = $SiteShellOnly } $permissions = Get-IISSiteDesiredAcl @paths ValidateAclPaths $permissions 'Cannot grant permissions; missing paths detected' $identity = if (Test-SID $AppPoolIdentity) { "*$AppPoolIdentity" } else { "`"$AppPoolIdentity`"" } $permissions | ForEach-Object { if ($PSCmdlet.ShouldProcess($_.Path, "Granting '$AppPoolIdentity' $($_.Description)")) { $sanitisedPath = $_.Path.TrimEnd('\') $params = @( "`"$sanitisedPath`"" '/grant:r' "$identity`:$($_.Permission)" ) Invoke-WithRetry { Start-Executable icacls $params } $MaxRetries | Out-Null } } } catch { Write-Error -ErrorRecord $_ -EA $callerEA } } } # .\IISSecurity\public\Set-WebHardenedAcl.ps1 function Set-WebHardenedAcl { <# .SYNOPSIS Remove default user and group file permissions added by windows .DESCRIPTION Remove from 'Path' supplied, the default user and group file permissions added by windows Users/groups file permissions removed: * Authenticated Users * Users * IIS_IUSRS * NETWORK SERVICE .PARAMETER Path The path to target permission removal .PARAMETER SiteAdminsGroup Optional user/group name to assign full permissions (inherited) to 'Path' .EXAMPLE Set-CaccaWebHardenedAcl -Path C:\inetpub -SiteAdminsGroup 'mydomain\mygroup' .NOTES This script must be run with administrator privileges. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, ValueFromPipeline)] [ValidateScript( {CheckPathExists $_})] [string] $Path, [Parameter(ValueFromPipeline)] [string] $SiteAdminsGroup ) begin { Set-StrictMode -Version Latest Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState $callerEA = $ErrorActionPreference $ErrorActionPreference = 'Stop' } process { try { # make sure the right people can administer the web server (before we start removing permissions below) if (![string]::IsNullOrWhiteSpace($SiteAdminsGroup)) { if ($PSCmdlet.ShouldProcess($Path, "Granting '$SiteAdminsGroup' full permission (inherit)")) { icacls ("$Path") /grant ("$SiteAdminsGroup" + ':(OI)(CI)F') | Out-Null } } # harden web server ACL's... $usersToRemove = 'NT AUTHORITY\Authenticated Users', 'BUILTIN\Users', 'BUILTIN\IIS_IUSRS', 'NT AUTHORITY\NETWORK SERVICE' if ($PSCmdlet.ShouldProcess($Path, 'Disabling permission inheritance')) { icacls ("$Path") /inheritance:d | Out-Null } $usersToRemove | ForEach-Object { if ($PSCmdlet.ShouldProcess($Path, "Removing user '$_'")) { icacls ("$Path") /remove:g ("$_") /remove:d ("$_") | Out-Null } } } catch { Write-Error -ErrorRecord $_ -EA $callerEA } } } Write-Verbose 'Importing from [C:\MyProjects\IISSecurity\IISSecurity\classes]' |