Skip to content

Commit

Permalink
Merge pull request #1734 from microsoft/bilong-aclchecknew
Browse files Browse the repository at this point in the history
Add script to verify permissions to Exchange property sets
  • Loading branch information
dpaulson45 committed Dec 20, 2023
2 parents 7f34298 + 5721b3a commit 8cedc50
Show file tree
Hide file tree
Showing 8 changed files with 492 additions and 0 deletions.
1 change: 1 addition & 0 deletions Admin/Test-ExchangePropertyPermissions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

$propertySets = @(
[PSCustomObject]@{
Name = "Exchange-Information"
RightsGuid = [Guid]::Parse("1F298A89-DE98-47b8-B5CD-572AD53D267E")
MemberAttributes = New-Object System.Collections.ArrayList
},
[PSCustomObject]@{
Name = "Exchange-Personal-Information"
RightsGuid = [Guid]::Parse("B1B3A417-EC55-4191-B327-B72E33E38AF2")
MemberAttributes = New-Object System.Collections.ArrayList
},
[PSCustomObject]@{
Name = "Personal-Information"
RightsGuid = [Guid]::Parse("77B5B886-944A-11d1-AEBD-0000F80367C1")
MemberAttributes = New-Object System.Collections.ArrayList
},
[PSCustomObject]@{
Name = "Public-Information"
RightsGuid = [Guid]::Parse("E48D0154-BCF8-11D1-8702-00C04FB96050")
MemberAttributes = New-Object System.Collections.ArrayList
}
)

$rootDSE = [ADSI]("LDAP://$([System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name)/RootDSE")
$schemaContainer = [ADSI]("LDAP://" + $rootDSE.schemaNamingContext)

foreach ($propertySet in $propertySets) {
$rightsGuidByteString = ""
$propertySet.RightsGuid.ToByteArray() | ForEach-Object { $rightsGuidByteString += ("\$($_.ToString("X"))") }
$searcher = New-Object System.DirectoryServices.directorySearcher($schemaContainer, "(&(objectClass=attributeSchema)(attributeSecurityGuid=$rightsGuidByteString))")
$searcher.PageSize = 100
$results = $searcher.FindAll()
foreach ($result in $results) {
[void]$propertySet.MemberAttributes.Add($result.Properties["cn"][0])
}
}

$getPropertySetInfoBuilder = New-Object System.Text.StringBuilder
[void]$getPropertySetInfoBuilder.AppendLine("# Copyright (c) Microsoft Corporation.")
[void]$getPropertySetInfoBuilder.AppendLine("# Licensed under the MIT License.")
[void]$getPropertySetInfoBuilder.AppendLine("")
[void]$getPropertySetInfoBuilder.AppendLine("# This is a generated function. Do not manually modify.")
[void]$getPropertySetInfoBuilder.AppendLine("function Get-PropertySetInfo {")
[void]$getPropertySetInfoBuilder.AppendLine(" [CmdletBinding()]")
[void]$getPropertySetInfoBuilder.AppendLine(" [OutputType([System.Object[]])]")
[void]$getPropertySetInfoBuilder.AppendLine(" param ()")
[void]$getPropertySetInfoBuilder.AppendLine("")
[void]$getPropertySetInfoBuilder.AppendLine(" # cSpell:disable")
[void]$getPropertySetInfoBuilder.AppendLine(" `$propertySetInfo = @(")
for ($i = 0; $i -lt $propertySets.Count; $i++) {
$propertySet = $propertySets[$i]
$memberAttributeString = [string]::Join(", ", ($propertySet.MemberAttributes | ForEach-Object { "`"$_`"" }))
[void]$getPropertySetInfoBuilder.AppendLine(" [PSCustomObject]@{")
[void]$getPropertySetInfoBuilder.AppendLine(" Name = `"$($propertySet.Name)`"")
[void]$getPropertySetInfoBuilder.AppendLine(" RightsGuid = [Guid]::Parse(`"$($propertySet.RightsGuid)`")")
[void]$getPropertySetInfoBuilder.AppendLine(" MemberAttributes = $memberAttributeString")
[void]$getPropertySetInfoBuilder.Append(" }")
if ($i + 1 -lt $propertySets.Count) {
[void]$getPropertySetInfoBuilder.AppendLine(",")
}
}
[void]$getPropertySetInfoBuilder.AppendLine(" )")
[void]$getPropertySetInfoBuilder.AppendLine(" # cSpell:enable")
[void]$getPropertySetInfoBuilder.AppendLine(" `$propertySetInfo")
[void]$getPropertySetInfoBuilder.AppendLine("}")
[void]$getPropertySetInfoBuilder.AppendLine("")

Set-Content $PSScriptRoot\Get-PropertySetInfo.ps1 $getPropertySetInfoBuilder.ToString()
34 changes: 34 additions & 0 deletions Admin/Test-ExchangePropertyPermissions/Get-PropertySetInfo.ps1

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]
$TargetObjectDN,

[Parameter(Mandatory = $true, Position = 1)]
[string]
$ComputerAccountDN,

[Parameter(Mandatory = $false, Position = 2)]
[string]
$DomainController,

[Parameter(Mandatory = $false, Position = 3)]
[switch]
$SaveReport,

[Parameter(Mandatory = $false, Position = 3)]
[switch]
$OutputDebugInfo
)

begin {
. $PSScriptRoot\..\..\Shared\Out-Columns.ps1
. $PSScriptRoot\..\..\Shared\ActiveDirectoryFunctions\Get-ActiveDirectoryAcl.ps1
. $PSScriptRoot\..\..\Shared\ActiveDirectoryFunctions\Get-ExchangeADSplitPermissionsEnabled.ps1
. $PSScriptRoot\..\..\Shared\ActiveDirectoryFunctions\Get-ExchangeOtherWellKnownObjects.ps1
. $PSScriptRoot\..\..\Shared\ActiveDirectoryFunctions\Get-ObjectTypeDisplayName.ps1
. $PSScriptRoot\..\..\Shared\ActiveDirectoryFunctions\Get-TokenGroupsGlobalAndUniversal.ps1
. $PSScriptRoot\Get-PropertySetInfo.ps1
. $PSScriptRoot\Test-ExchangeSchema.ps1

$requiredWellKnownGroupsInToken = "Exchange Trusted Subsystem", "Exchange Servers"

$report = [PSCustomObject]@{
TargetObjectDN = $TargetObjectDN
ComputerAccountDN = $ComputerAccountDN
DomainController = $DomainController
RequiredInToken = @()
Token = $null
ACL = $null
ProblemsFound = @()
}
}

process {
if (-not (Test-ExchangeSchema)) {
Write-Warning "Schema validation failed. Exiting."
return
}

if (Get-ExchangeADSplitPermissionsEnabled) {
Write-Host "Split permissions is enabled. In this scenario, it is expected that the Exchange server
computer account does not have write permission to many recipient attributes. The script will
report these as problems, although they may be normal for this configuration."
}

$token = Get-TokenGroupsGlobalAndUniversal -DistinguishedName $ComputerAccountDN
$report.Token = $token
Write-Host "Token groups: $ComputerAccountDN"
$token | Out-Columns

$wellKnownObjects = Get-ExchangeOtherWellKnownObjects
foreach ($wellKnownName in $requiredWellKnownGroupsInToken) {
$groupDN = ($wellKnownObjects | Where-Object { $_.WellKnownName -eq $wellKnownName }).DistinguishedName
$objectSidBytes = ([ADSI]("LDAP://$groupDN")).Properties["objectSID"][0]
$objectSid = New-Object System.Security.Principal.SecurityIdentifier($objectSidBytes, 0)
$report.RequiredInToken += [PSCustomObject]@{
WellKnownName = $wellKnownName
DistinguishedName = $groupDN
ObjectSid = $objectSid.ToString()
}

$matchFound = $token | Where-Object { $_.SID -eq $objectSid.ToString() }
if ($null -eq $matchFound) {
$report.ProblemsFound += "The group $wellKnownName is not in the token."
}
}

$params = @{
DistinguishedName = $TargetObjectDN
}

if (-not [string]::IsNullOrEmpty($DomainController)) {
$params.DomainController = $DomainController
}

$acl = Get-ActiveDirectoryAcl @params
$objectTypeCache = @{}
$displayAces = @()
for ($i = 0; $i -lt $acl.Access.Count; $i++) {
$ace = $acl.Access[$i]
if ($ace.ObjectType -ne [Guid]::Empty) {
if ($null -ne $objectTypeCache[$ace.ObjectType]) {
$ace | Add-Member -NotePropertyName ObjectTypeDisplay -NotePropertyValue $objectTypeCache[$ace.ObjectType]
} else {
$objectTypeDisplay = Get-ObjectTypeDisplayName -ObjectType $ace.ObjectType
$objectTypeCache[$ace.ObjectType] = $objectTypeDisplay
$ace | Add-Member -NotePropertyName ObjectTypeDisplay -NotePropertyValue $objectTypeDisplay
}
}

if ($ace.InheritedObjectType -ne [Guid]::Empty) {
if ($null -ne $objectTypeCache[$ace.InheritedObjectType]) {
$ace | Add-Member -NotePropertyName InheritedObjectTypeDisplay -NotePropertyValue $objectTypeCache[$ace.InheritedObjectType]
} else {
$objectTypeDisplay = Get-ObjectTypeDisplayName -ObjectType $ace.InheritedObjectType
$objectTypeCache[$ace.InheritedObjectType] = $objectTypeDisplay
$ace | Add-Member -NotePropertyName InheritedObjectTypeDisplay -NotePropertyValue $objectTypeDisplay
}
}

$ace | Add-Member -MemberType NoteProperty -Name "Index" -Value $i
$displayAces += $ace
}

$report.ACL = $displayAces
Write-Host "ACL: $TargetObjectDN"
$displayAces | Where-Object { $_.PropagationFlags -ne "InheritOnly" } | Out-Columns -Properties Index, IdentityReference, AccessControlType, ActiveDirectoryRights, ObjectTypeDisplay, IsInherited

$propertySetInfo = Get-PropertySetInfo
$attributeCount = $propertySetInfo.MemberAttributes.Count
$progressCount = 0
$sw = New-Object System.Diagnostics.Stopwatch
$sw.Start()
$schemaPath = ([ADSI]("LDAP://$([System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name)/RootDSE")).Properties["schemaNamingContext"][0]
$identityReferenceCache = @{}
foreach ($propertySet in $propertySetInfo) {
foreach ($attributeName in $propertySet.MemberAttributes) {
$progressCount++
if ($sw.ElapsedMilliseconds -gt 1000) {
$sw.Restart()
Write-Progress -Activity "Checking permissions" -PercentComplete $((($progressCount * 100) / $attributeCount))
}

$attributeSchemaEntry = [ADSI]("LDAP://CN=$attributeName,$schemaPath")
if ($attributeSchemaEntry.Properties["attributeSecurityGuid"].Count -lt 1) {
# This schema validation failure should be extremely rare, but we have seen a few
# cases in lab/dev/test environments, such as when ADSchemaAnalyzer has been used to
# copy schema between forests.
$report.ProblemsFound += "The attribute $attributeName is not in the $($propertySet.Name) property set."
continue
}

$schemaIdGuid = New-Object Guid(, $attributeSchemaEntry.Properties["schemaIDGuid"][0])

# We need to hit a write allow ACE for a SID in the token on one of the following:
# - The rightsGuid from the property set
# - The schemaIdGuid from the attributeSchemaEntry
# We must hit the allow before we hit a deny on the same thing.

$found = $false
$problemAceIndex = $null
for ($i = 0; $i -lt $displayAces.Count; $i++) {
$ace = $displayAces[$i]
if ($ace.PropagationFlags -eq "InheritOnly") {
continue
}

$sidToFind = $null
if ($null -eq $ace.IdentityReference.SID) {
if ($null -ne $identityReferenceCache[$ace.IdentityReference.Value]) {
$sidToFind = $identityReferenceCache[$ace.IdentityReference.Value]
} else {
$sidToFind = $ace.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
$identityReferenceCache[$ace.IdentityReference.Value] = $sidToFind
}
} else {
$sidToFind = $ace.IdentityReference.SID
}

$matchingSid = $token | Where-Object { $_.SID -eq $sidToFind.ToString() }
if ($null -ne $matchingSid) {
# The ACE affects this token.
# Does it affect this property?
if ($ace.ObjectType -eq $propertySet.RightsGuid -or $ace.ObjectType -eq $schemaIdGuid -or $ace.ObjectType -eq [Guid]::Empty) {
if ($ace.ActiveDirectoryRights -contains "WriteProperty" -or $ace.ActiveDirectoryRights -contains "GenericAll") {
if ($ace.AccessControlType -eq "Allow") {
$found = $true
break
} else {
$problemAceIndex = $i
break
}
}
}
}
}

if (-not $found) {
if ($null -ne $problemAceIndex) {
$report.ProblemsFound += "The property $attributeName is denied Write by ACE $problemAceIndex."
} else {
$report.ProblemsFound += "The property $attributeName is not allowed Write by any ACE."
}
}
}
}

if ($report.ProblemsFound.Count -gt 0) {
foreach ($problem in $report.ProblemsFound) {
Write-Warning $problem
}
} else {
Write-Host "No problems found."
}

if ($SaveReport) {
$reportPath = $PSScriptRoot + "\" + "PermissionReport-$([DateTime]::Now.ToString("yyMMddHHmmss")).xml"
$report | Export-Clixml $reportPath
Write-Host "Report saved to $reportPath"
}

if ($OutputDebugInfo) {
$debugInfo = @{
ACL = $acl
DisplayAces = $displayAces
IdentityReferenceCache = $identityReferenceCache
Token = $token
TargetObjectDN = $TargetObjectDN
Report = $report
}

$debugInfoPath = Join-Path $PSScriptRoot "DebugInfo.xml"
$debugInfo | Export-Clixml -Path $debugInfoPath
Write-Host "Debug info saved to $debugInfoPath"
}
}
Loading

0 comments on commit 8cedc50

Please sign in to comment.