Last active
January 6, 2025 17:30
-
-
Save SamErde/718242d2e8b801f4c26da093965e908f to your computer and use it in GitHub Desktop.
This function retrieves all dynamic groups in Entra ID and checks if the membership rule contains any user-modifiable attributes. If it does, the group is considered unsafe.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Get-UnsafeDynamicGroups { | |
<# | |
.SYNOPSIS | |
Get unsafe dynamic groups in Entra ID. | |
.DESCRIPTION | |
This function retrieves all dynamic groups in Entra ID and checks if the membership rule contains any user-modifiable attributes. If it does, the group is considered unsafe. | |
.EXAMPLE | |
Get-UnsafeDynamicGroups | |
.NOTES | |
Author: Sam Erde | |
Company: Sentinel Technologies, Inc. | |
Modified: 2025-01-01 | |
Version: 0.1.0 | |
To Do: | |
- Confirm list of user-modifiable attributes. [RED HERRING?] | |
- Add instructions for remediating or mitigating the unsafe group configuration. | |
- Check for dynamic groups with failed processing status or paused processing. | |
- Check for nested groups. | |
For now, this script is based on identifying dynamic membership rules that target user-modifiable attributes. The | |
script does not yet check for permissions on custom attributes. There are also other ways to create unsafe dynamic, | |
which I would like to check for in the future. Other examples that I would like to add in the future are described | |
in the blog posts referenced below. This script was inspired by blog posts about dynamic groups from Nairuz Abulhul | |
(@nairuzabulhul) and Cody Burkard (@codyburkard). | |
- <https://medium.com/r3d-buck3t/abusing-dynamic-groups-in-azuread-part-1-ff12e328c8c0> | |
- <https://mnemonic.no/blog/abusing-dynamic-groups-in-azure-ad-for-privilege-escalation/> | |
Potential General Checks: | |
- Check if the tenant allows users to invite guests. | |
- Check if any dynamic groups use any of these attributes in a membership rule (especially a partial match) | |
- mail, mailNickname, userPrincipalName, proxyAddresses, otherMails | |
Attributes of Potential Interest: | |
- Get groups where memberOf or TransitiveMemberOf has a value. | |
- Get groups where TransitiveMembers has a value. | |
- Get groups where ServiceProvisioningErrors has a value. | |
#> | |
# Requires -Version 5.1 | |
[CmdletBinding()] | |
[OutputType([PSCustomObject[]])] | |
param () | |
$RequiredModules = @( | |
'Microsoft.Graph.Authentication', | |
'Microsoft.Graph.Groups' | |
) | |
foreach ($module in $RequiredModules) { | |
if (-not (Get-Module -ListAvailable -Name $module)) { | |
try { | |
Install-Module -Name $module -Scope CurrentUser | |
} catch { | |
throw "Failed to install module: $module. Error: $_" | |
} | |
} | |
Import-Module -Name $module | |
} | |
# [ NEED TO CONFIRM ] List of built-in user-modifiable attributes that are considered unsafe. | |
$UserModifiableAttributes = ('displayName', 'givenName', 'surname', 'preferredLanguage', 'mobilePhone', 'businessPhones') | |
# Find one or more instances of a leading '(' that is followed by an "objectType.propertyName" string (e.g. 'user.surname'). | |
[string]$RulePattern = '\((\w+\.\w+)\s-[a-zA-Z]+' | |
# Pre-create a list of objects to store the results. | |
[System.Collections.Generic.List[object]]$UnsafeDynamicGroups = @() | |
try { | |
Connect-MgGraph -Scopes 'User.Read.All', 'Group.Read.All' -NoWelcome | |
Get-MgContext | Select-Object TokenCredentialType, Account, AppName, ContextScope, TenantId | Write-Verbose | |
} catch { | |
throw "Failed to connect to Microsoft Graph. Error: $_" | |
} | |
# Get all dynamic groups and check if their membership rules contain any user-modifiable attributes. | |
[Microsoft.Graph.PowerShell.Models.MicrosoftGraphGroup[]]$DynamicGroups = Get-MgGroup -Filter "groupTypes/any(c:c eq 'DynamicMembership')" -All:$true -ConsistencyLevel eventual | |
Write-Verbose "Found $($DynamicGroups.Count) dynamic groups in this tenant." | |
foreach ($group in $DynamicGroups) { | |
Write-Verbose "+ Inspecting dynamic group: $($group.DisplayName)" | |
Remove-Variable UnsafeRuleProperty, matches -ErrorAction SilentlyContinue | |
[string[]]$UnsafeRuleProperty = @() | |
[string]$RuleString = $group.MembershipRule | |
Write-Verbose " + Inspecting dynamic group membership rule: $RuleString" | |
$RuleMatches = [regex]::matches($RuleString, $RulePattern) | |
[string[]]$RuleProperty = $RuleMatches | ForEach-Object { $_.Groups[1].Value } | |
foreach ($property in $RuleProperty) { | |
Write-Verbose " + Inspecting dynamic rule property: $($property)" | |
$ObjectProperty = $property.Split('.')[1] | |
if ($UserModifiableAttributes -contains $ObjectProperty) { | |
$UnsafeRuleProperty += $property | |
Write-Verbose " - Found unsafe dynamic rule property: ''$property'' in rule: ''$RuleString''." | |
} else { | |
Write-Verbose " - The property ''$property'' is not considered unsafe." | |
} | |
} | |
# Create a list of custom objects that contain the results. | |
if ($UnsafeRuleProperty.Count -gt 0) { | |
# Create custom object that contains the group DisplayName, the list of unsafe properties, and the group object. | |
$UnsafeDynamicGroups.Add( [PSCustomObject]@{ | |
DisplayName = $group.DisplayName | |
UnsafeRuleProperty = $UnsafeRuleProperty | |
UnsafeRule = $RuleString | |
MembershipProcessingState = $group.MembershipRuleProcessingState | |
Group = $group | |
}) | |
} | |
} | |
$UnsafeDynamicGroups | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment