Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ConanChiles/3d3a5703f9737e5f90f554bd325fe3e2 to your computer and use it in GitHub Desktop.
Save ConanChiles/3d3a5703f9737e5f90f554bd325fe3e2 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"
@ashishmgupta
Copy link

ashishmgupta commented Oct 3, 2022

Very well done!
Just one feedback - looks like the below will filter out the admins who have mailbox?
$roleGroupDirectMembers | Where-Object -Property 'RecipientType' -EQ -Value 'User'
should be below?
$roleGroupDirectMembers | Where-Object {($_.RecipientType -contains 'User') -OR ($_.RecipientType -contains 'UserMailbox')}

In the below screenshot you see the, bruce.banner being with a recipient type of "User", their remote powershell is not getting disabled. However, users "labadmin" and "tony.stark" are in the org. management and security admin roles respectively with recipienttype of "UserMailbox" but their exchange PowerShell access is also getting disabled.

image

After changing to :
$roleGroupDirectMembers | Where-Object {($_.RecipientType -contains 'User') -OR ($_.RecipientType -contains 'UserMailbox')}
All the users - labadmin, tony.stark and bruce.banner in the exchange roles are being excluded from disable of remote Powershell acess.
Remote powershell access of only Non-admins are getting disabled.

image

@ConanChiles
Copy link
Author

oh, thank you! good find, will fix

@ConanChiles
Copy link
Author

take 2
see how that goes

@cvocvo
Copy link

cvocvo commented Oct 4, 2022

The only other item that I've noticed is that disabled users are enumerated by this command:
Get-User -ResultSize Unlimited -Filter 'RemotePowerShellEnabled -eq $true'
However, the disabled users are not configured with RemotePowerShell $false after running the script. I don't think this is a problem because if they're disabled you can't auth with them, but figured I'd mention it.
Thank you for building this!

@swally089
Copy link

This script is not expanding all groups, you can remove this line 118 as its not doing anything
$roleGroupSubGroups = $roleGroupDirectMembers | Where-Object -Property 'RecipientType' -EQ -Value 'Group'
and change line 134 to
$roleGroupDirectMembers | Where-Object -Property 'RecipientType' -like 'Group' | ForEach-Object -Process {

@ConanChiles
Copy link
Author

ConanChiles commented Oct 5, 2022

thanks, yep, you're right

@xasz
Copy link

xasz commented Oct 6, 2022

I did not use this script as is but it helped alot for us. Thank your.

Line 175+
For multilingual support you could just put there something like. We have no customer with cross forest or multidomain, so i removed all the sections, but i added this, because the names are not the same, but this works because of well known SIDs.

$Domain = Get-ADDomain
$DomainAdminGroup = Get-ADGroup -Filter ("SID -eq '{0}'" -f ("{0}-512" -f $Domain.DomainSID) )
$OrgAdminGroup = Get-ADGroup -Filter ("SID -eq '{0}'" -f ("{0}-519" -f $Domain.DomainSID) )
$adminGroup = Get-ADGroup -Filter ("SID -eq '{0}'" -f "S-1-5-32-544" )```

@xasz
Copy link

xasz commented Oct 6, 2022

I created a simpler version of the script, which need alot of more "thinking" "before" you click and does not support multidomain and other fancy stuff your amazing script does.
But if someone does need a more dangerous but simpler version: https://gist.github.com/xasz/53754812e48311a65c7d0b6bc3376634

@Niehweune
Copy link

Also should take care not to disable RPS for health mailboxes. I.e. : exclude users with recipienttypedetails = MonitoringMailbox (549755813888)
Otherwise this will cause the RPS healthset to fail (with a very ugly error message in the eventlog)

@Networkinger
Copy link

Is the exclusion was modified for the Health Mailboxes ? Has anyone tested this script in Multi-domain environment ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment