Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save hxlxmjxbbxs/93753b2f9a2ac7f5c6b43fc8680276a5 to your computer and use it in GitHub Desktop.
Save hxlxmjxbbxs/93753b2f9a2ac7f5c6b43fc8680276a5 to your computer and use it in GitHub Desktop.
ProxyNotShell - disable Exchange PowerShell access for all users, excluding Exchange admins (derived from Exchange roles)
<# block non-Exchange admins from PowerShell access in Exchange
ProxyNotShell
CVE-2022-41040
CVE-2022-41082f
some bypasses have been found for the IIS block rules.
need to hard block PowerShell for those that don't **need** it.
Exchange allows PowerShell by default, block by exception. Not ideal, but workable.
https://msrc-blog.microsoft.com/2022/09/29/customer-guidance-for-reported-zero-day-vulnerabilities-in-microsoft-exchange-server/
https://techcommunity.microsoft.com/t5/exchange-team-blog/customer-guidance-for-reported-zero-day-vulnerabilities-in/ba-p/3641494
"we strongly recommend Exchange Server customers to disable remote PowerShell access for non-admin users in your organization"
https://learn.microsoft.com/en-us/powershell/exchange/control-remote-powershell-access-to-exchange-servers
"By default, all user accounts have access to remote PowerShell."
https://learn.microsoft.com/en-us/powershell/scripting/windows-powershell/wmf/whats-new/compatibility
Systems that are running the following server applications should not run Windows Management Framework 5.1
Microsoft Exchange Server 2013
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!! !!!
read this -----> !!! don't just don't blindly run this, please !!! <----- oi, read this, really
!!! !!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
have a read through, run in sections, inspect the variables, do your own sanity checking
might be bugs, bad assumptions, edge cases, etc
treat this thing as potentially hostile
bricking your exchange is potentially a résumé generating event
changes:
https://gist.github.com/ConanChiles/3d3a5703f9737e5f90f554bd325fe3e2/revisions
2022-10-04
bugfix
thank you Ashish Gupta ("https://twitter.com/ashishrocks", "https://github.com/ashishmgupta")
https://gist.github.com/ConanChiles/3d3a5703f9737e5f90f554bd325fe3e2?permalink_comment_id=4323409#gistcomment-4323409
Ashish found a bug where admins with a mailbox (RecipientType "UserMailbox" as opposed to "User") would filtered out, resulting in their PowerShell access being blocked (not the intended outcome)
-----
2022-10-04
added attribute "adObjEnabled" to $allUsersExch for the corresponding AD user enabled/disabled status
added attribute "exchAdminRoles" to $allUsersExch in include their Exchange admin roles
bunch of refactoring, comments, tweaks, etc
-----
2022-10-04
added T0 AD groups to "admin roles" to avoid blocking those
added detection of self (AD account running the script) to "admin roles" to avoid locking out yourself
#>
$ErrorActionPreference = 'stop'
Set-StrictMode -Version 'latest'
Add-PSSnapin -Name 'Microsoft.Exchange.Management.PowerShell.SnapIn'
Import-Module -Name 'ActiveDirectory'
# high level
# enumerate the exchange admin roles to get a list of exchange admin users.
# set the block flag on every account, excluding the above admins.
# to do: some "admins" might not need PowerShell, block those also.
$ExchangeUserPropsExport = @(
'UserPrincipalName'
'RemotePowerShellEnabled'
'adObjEnabled'
'SamAccountName'
'FirstName'
'LastName'
'DisplayName'
'exchAdminRoles'
'RecipientType'
'RecipientTypeDetails'
'Company'
'Department'
'Title'
'DistinguishedName'
'Identity'
'Guid'
'Sid'
)
# get all users from all domains in forrest
$allUsersAD = (Get-ADForest).Domains | ForEach-Object -Process {
"getting all users from domain: $PSItem" | Write-Host -ForegroundColor Cyan
Get-ADUser -Server $PSItem -Filter * -Properties *
}
# putting into hashtable (GUID:isEnabled) faster than $x | Where-Object -FilterScript {...}
'building hashtable of (AD User GUID : isEnabled)' | Write-Host -ForegroundColor Cyan
$htAdGuidEnabled = New-Object -TypeName 'hashtable'
$allUsersAD | ForEach-Object -Process {
[void]$htAdGuidEnabled.Add( $PSItem.ObjectGUID.Guid, $PSItem.Enabled )
}
# more complicated than i'd like, but allows for cross forrest / domain lookups
# get the roles, members of (users/groups) recursively
# for groups, need to lookup their members in AD, pointing at the right domain
# https://exchange.server/ecp/
# permissions > admin roles
$allRoleGroups = Get-RoleGroup
$htExchangeAdminRoles = New-Object -TypeName 'hashtable'
foreach ( $roleGroup in $allRoleGroups ) {
"enum members of role: $($roleGroup.Name)" | Write-Host -ForegroundColor Cyan
$roleGroupDirectMembers = $roleGroup | Get-RoleGroupMember
$roleGroupSubGroups = $roleGroupDirectMembers | Where-Object -Property 'RecipientType' -EQ -Value 'Group'
# just add users to this. directly users assigned the role, enumerate member groups and add those users
$roleGroupRecurMembers = New-Object -TypeName 'System.Collections.ArrayList'
$roleGroupDirectMembers | Where-Object -Property 'RecipientType' -Like -Value '*user*' | ForEach-Object -Process {
[void]$roleGroupRecurMembers.Add(
($PSItem | Select-Object -Property (
'DistinguishedName',
@{ label = 'DomainId'; expression = {$PSItem.Identity.DomainId.ToString()} },
@{ label = 'ObjCategory'; expression = {$PSItem.ObjectCategory.Name} }
))
)
}
# get the AD groups assigned to the Exchange role, lookup the AD members against the appropriate domain name
$roleGroupDirectMembers | Where-Object -Property 'RecipientType' -EQ -Value 'Group' | ForEach-Object -Process {
Get-ADGroupMember -Recursive -Server ($PSItem.Identity.DomainId.ToString()) -Identity ($PSItem.DistinguishedName) | Get-ADUser -Properties ('CanonicalName', 'ObjectCategory')
} | Select-Object -Property @(
'DistinguishedName',
@{ label = 'DomainId'; expression = {($PSItem.CanonicalName -split '/')[0]} },
@{ label = 'ObjCategory'; expression = {($PSItem.ObjectCategory | Get-ADObject).Name} }
) | ForEach-Object -Process {
[void]$roleGroupRecurMembers.Add( $PSItem )
}
[void]$htExchangeAdminRoles.Add( $roleGroup.Name, $roleGroupRecurMembers )
}
#<#
# don't lock yourself out, or members of Domain Admins, Enterprise Admins, etc
$htDomainSidDomainName = New-Object -TypeName 'hashtable'
(Get-ADForest).Domains | Get-ADDomain | ForEach-Object -Process {
[void]$htDomainSidDomainName.Add( $PSItem.DomainSID.Value, $PSItem.DNSRoot )
}
$whoami = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
$thisisme = Get-ADUser -Server $htDomainSidDomainName.($whoami.AccountDomainSid.Value) -LDAPFilter "(objectSid=$($whoami.Value))" -Properties ('CanonicalName', 'ObjectCategory')
$htExchangeAdminRoles.Add(
'DoNotLockOutYourself', [array]($thisisme | Select-Object -Property @(
'DistinguishedName',
@{ label = 'DomainId'; expression = {($PSItem.CanonicalName -split '/')[0]} },
@{ label = 'ObjCategory'; expression = {($PSItem.ObjectCategory | Get-ADObject).Name} }
))
)
# don't lock out the other admins
$SuperImportantGroupNames = @(
'Administrators'
'Domain Admins'
'Enterprise Admins'
)
foreach ( $groupName in $SuperImportantGroupNames ) {
$groupMembers = New-Object -TypeName 'System.Collections.ArrayList'
foreach ( $domainName in ((Get-ADForest).Domains) ) {
try {
$adGroupMembers = Get-ADGroupMember -Recursive -Server $domainName -Identity $groupName
} catch {
"group $([char](34))$groupName$([char](34)) not found in domain $([char](34))$domainName$([char](34))"
continue
}
$adGroupMembers | Get-ADUser -Properties ('CanonicalName', 'ObjectCategory') | Select-Object -Property @(
'DistinguishedName',
@{ label = 'DomainId'; expression = {($PSItem.CanonicalName -split '/')[0]} },
@{ label = 'ObjCategory'; expression = {($PSItem.ObjectCategory | Get-ADObject).Name} }
) | ForEach-Object -Process {
[void]$groupMembers.Add( $PSItem )
}
}
$htExchangeAdminRoles.Add( $groupName, $groupMembers )
}
#>
# summary of admins roles / groups and members
$htExchangeAdminRoles.GetEnumerator() | Select-Object -Property @(
@{ label = 'RoleName'; expression = {($PSItem.Name)} },
@{ label = 'CountUsers'; expression = {( (($PSItem.Value) | Measure-Object).Count)} }
) | Sort-Object -Descending -Property 'CountUsers' | Format-Table -AutoSize
$allUsersExch = Get-User -ResultSize 'unlimited' -Filter *
# map exchange users to AD users, with AD object enabled/disabled status
$allUsersExch | Add-Member -Force -MemberType ScriptProperty -Name 'adObjEnabled' -Value {
if ( $htAdGuidEnabled.ContainsKey($this.Guid.Guid) ) {
return ( $htAdGuidEnabled.($this.Guid.Guid) )
} else {
return ($null)
}
}
# map exchange users with their Exchange admin roles
# check these memberships. probably have more than needed/intended with nested groups, or just plain overprivileged (SIEM shoudld monitor changes also)
$allUsersExch | Add-Member -Force -MemberType ScriptProperty -Name 'exchAdminRoles' -Value {
$foundRoles = New-Object -TypeName 'System.Collections.ArrayList'
foreach ( $ExchAdminRole in $htExchangeAdminRoles.Keys) {
if ( 0 -ne $htExchangeAdminRoles.$ExchAdminRole.Count ) {
if ( $htExchangeAdminRoles.$ExchAdminRole.DistinguishedName.Contains($this.DistinguishedName) ) {
[void]$foundRoles.Add($ExchAdminRole)
}
}
}
if ( $foundRoles.Count -eq 0 ) {
return ( $null )
} else {
return ( $foundRoles )
}
}
break
# anything weird / unexpected / bad there?
$allUsersExch | Where-Object -FilterScript {
![string]::IsNullOrWhiteSpace($PSItem.exchAdminRoles)
} | Sort-Object -Property @(
'adObjEnabled'
'RemotePowerShellEnabled'
'UserPrincipalName'
) | Select-Object -Property @(
'UserPrincipalName'
'RemotePowerShellEnabled'
'adObjEnabled'
'exchAdminRoles'
'DisplayName'
'FirstName'
'LastName'
'RecipientType'
'RecipientTypeDetails'
'Company'
'Department'
'Title'
'Identity'
'DistinguishedName'
) | Out-GridView
pause
# backup / log pre-change state
$allUsersExch | Select-Object -Property $ExchangeUserPropsExport | Sort-Object -Property @(
'RemotePowerShellEnabled'
'Department'
'Title'
'DisplayName'
'LastName'
'FirstName'
) | Export-Csv -NoTypeInformation -Encoding UTF8 -LiteralPath 'C:\temp\ProxyNotShell_BlockNonAdmins_PreChangeState.csv'
$htExchangeAdminRoles | ConvertTo-Json | Out-String | Out-File -Encoding utf8 -LiteralPath 'C:\temp\ProxyNotShell_BlockNonAdmins_AdminRolesMembers.json'
# come up with some logic to find what accounts **need** PowerShell
# !!! highly suggest disabling in smallish chunks, one big hit is likley to end in tears and regret !!!
# ------------------------------------------------------------------------------------------------------
$allUsersExch | Where-Object -FilterScript {
$PSItem.RemotePowerShellEnabled -eq $true -and
$PSItem.adObjEnabled -eq $true -and
[string]::IsNullOrWhiteSpace($PSItem.exchAdminRoles)
} | Select-Object -Property $ExchangeUserPropsExport |
Tee-Object -Variable 'usersToRemovePowerShellAccess' |
Out-GridView
$usersToRemovePowerShellAccess.Count
pause
# Danger Zone !!!
$usersToRemovePowerShellAccess | ForEach-Object -Process {
"disabling PowerShell for: $($PSItem.UserPrincipalName)"
# ---> did you read the bit up the top?
# Set-User -Identity $PSItem.DistinguishedName -RemotePowerShellEnabled $false
}
$usersToRemovePowerShellAccess | Select-Object -Property $ExchangeUserPropsExport | Export-Csv -NoTypeInformation -Encoding UTF8 -LiteralPath "C:\temp\ProxyNotShell_BlockNonAdmins_BlockedAccounts_($((Get-Date).ToString('yyyy-MM-dd hh-mm-ss tt'))).csv"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment