Cloudy With a Chance of Backdoors
- infiniteloop
- Apr 6
- 12 min read
Updated: Apr 29
Introduction
You know the saying, "Keep your friends close, and your enemies closer." The same principle applies here. Collaboration with external users is a legitimate business need. Vendors, contractors, and partners often need access to internal resources to keep projects moving.
Every guest account added to your tenant potentially expands your attack surface. Without tight controls and visibility, it becomes difficult to track who has access, why they were invited, and what resources they have access to. Over time, trusted collaboration and unmanaged risk start to look exactly the same.
Microsoft Entra B2B Collaboration
Entra B2B allows organizations to invite external users (partners, contractors, or anyone else, even if they don’t have a corporate identity). If they have an Entra ID or a personal Microsoft account, they sign in with those credentials. Otherwise, they can authenticate using a one-time passcode sent to their email. By default, anyone in your organization can invite guests to apps like Teams or SharePoint, which simplifies collaboration but can create security gaps if not managed carefully.
What it Looks Like Under the Hood
Entra B2B creates a guest account in your tenant that uses external credentials instead of storing new ones. An authorized user sends an email invite; the guest accepts and authenticates, then gains access based on predefined group memberships and permissions.
The invitation process goes something like this:
A user sends an invitation to an external user’s email address.
The external user receives the invitation and accepts it. If they already have a Microsoft Entra ID or a personal Microsoft account, they sign in with their existing credentials. If not, they’re prompted to create an account or use a one-time passcode.
Once accepted, a guest account is added to your Entra ID tenant, but it’s simply a guest identity rather than a fully managed account. The user continues to use their own email address to sign in, and their access is governed by the permissions and group memberships you’ve defined.

Risky Common Misconfigurations
Dangerous Dynamic Group Rules
Dynamic groups in Azure automatically add or remove members based on specific user attributes (e.g., email, department). While this can streamline group management, if the rules are too broad or poorly defined, attackers can slip into privileged groups with minimal effort. Below are three examples and how easily they can be abused:
(user.userPrincipalName -startsWith "admin") - This rule adds any user whose userPrincipalName starts with the substring "admin". For example, if someone’s userPrincipalName (often similar to an email address) is "adminsomethingsomething@domain.com" or "admin.user@domain.com", they match the rule.
(user.mail -match "contractor") - This rule adds any user whose mail attribute contains "contractor" For instance, "joe@contractor-example.com" would match.
(user.department -eq "IT") - This rule includes any user whose department attribute is exactly "IT", if the attribute is set to “IT” they get added; “IT Department” or “Information Technology” wouldn’t match unless it’s precisely “IT.”
No Domain Restrictions
When an organization doesn’t limit which email domains can receive guest invitations, anyone with a valid email address can be invited. Which is exactly how an attacker can benefit from this by creating short-lived or burner addresses (for example, from free providers like ProtonMail or Gmail) and slipping into the environment under unrecognized aliases. By the time anyone notices, these accounts may have already escalated privileges, accessed sensitive data, or added more of their guest accounts to maintain persistence in the case one gets burnt.
Anyone Can Invite Anyone
By leaving the default settings in place, where any user, including existing guest users can invite anyone they want, you practically lose visibility into who enters your tenant. An attacker that's compromised a single account can add as much persistence as he wishes this way.

Attack Paths
Guest User Injections
When any user in your organization (including guests) can invite others, attackers can slip in with fake guest accounts. Once inside, they can quietly dig into your resources or even elevate their privileges. This method often goes unnoticed because everything looks like standard guest activity.
Dynamic Group Membership Exploits
As previously mentioned, Dynamic Groups automatically add users based on specific attributes, like an email address that contains “admin” within it. If those rules aren’t carefully defined, attackers just need to create an account that matches the criteria, and they’re instantly added to privileged groups.
Example: An attacker invites a guest user with an email like fakeadmin@something.com. If a dynamic group rule adds any user whose email contains "admin" and is a guest to the Admin group, the malicious guest is automatically granted elevated privileges.





Cross Tenant Ghost Guests Drive-by
Normally, when you invite a guest user to your tenant, they get an email and have to click on it like we are stuck in a decade old onboarding flow.
But if Cross-Tenant Access settings are properly configured between organizations, it is possible to quietly plant a guest into a tenant without them clicking anything at all.
Once configured for automatic redemption, you can simply create a guest user through Microsoft Graph, and the user will redeem automatically the first time they access a resource. No clicking, no consent screens, no invite emails needed.
From there, the guest can be dropped automatically into a dynamic group, depending on whether the tenant has any in place and if you can match the membership criteria (as covered earlier), or you can manually assign access yourself. Either way, the account is fully planted and ready for further use.
Install the Microsoft Graph PowerShell module if you haven’t already:
Install-Module Microsoft.Graph -Scope CurrentUserThen connect using the right scopes (assuming you’ve got the creds)
Connect-MgGraph -Scopes "User.Invite.All"Now the fun part, quietly plant a guest into the tenant:
$externalUserEmail = "adminsomething@example.com"
$displayName = "An absolutely legitimate user"
$redirectUrl = "https://myapps.microsoft.com"
New-MgInvitation `
-InvitedUserEmailAddress $externalUserEmail `
-InvitedUserDisplayName $displayName `
-InviteRedirectUrl $redirectUrlAlternatively, you can do the same thing using the AzureAD PS module:
Install the AzureAD module if you haven't already
Install-Module AzureAD -Scope CurrentUserConnect to your tenant:
Connect-AzureADInvite the guest user:
$externalUserEmail = "adminsomething@example.com"
$displayName = "An absolutely legitimate user"
$redirectUrl = "https://myapps.microsoft.com"
New-AzureADMSInvitation `
-InvitedUserEmailAddress $externalUserEmail `
-InvitedUserDisplayName $displayName `
-InviteRedirectUrl $redirectUrlThat’s it. The user now exists in the tenant and no one had to click a damn thing.
Bear in mind: this only applies when inviting guests through Graph API or PowerShell. While technically "User.Invite.All" is the minimal scope required for Graph operations, it usually demands admin consent and is not available to regular users. If you are attacking manually through the Azure Portal, portal permissions and tenant settings will determine if you can invite guests without needing Graph privileges.
Subscription Hijacking
Azure subscriptions can be transferred to different tenants, and attackers who obtain the right permissions can do exactly that, leaving you stuck with the bill while robbing you of visibility and control. It’s like someone taking your credit card and locking you out of your bank account while the charges keep piling up. Once attackers seize your subscription, they gain full control and can spin up resource intensive virtual machines for crypto mining, host malicious applications or launch phishing campaigns, all, of course, on your expense.
Persistence in O365
Guest accounts often have enough read permissions to locate internal group listings, membership details, or even various SharePoint sites, Teams channels, and OneDrive documents, especially if external sharing is liberally enabled. If an adversary needs a long term vantage point (for espionage, data exfiltration, or as a stepping stone to further compromise), a well placed guest can accomplish a lot more than you might expect.
Exploitation
While these attack paths can be executed manually through the Azure Portal or PowerShell, adversaries typically leverage automation tools to make their attacks faster and harder to detect. Thanks to Beau Bullock's GraphRunner, we can explore how it interacts directly with Microsoft Graph to execute these attacks and break down each function:
Invoke-InviteGuest: Automate the process of sending guest invitations, eliminating the need for manual entry in the Azure Portal. This allows for rapid creation of multiple guest accounts without immediate detection.
Get-DynamicGroups: Scan through the tenant’s dynamic groups to identify membership rules that can be exploited. For instance, a poorly configured rule that adds anyone with "admin" in the beginning of their user principal name.
Invoke-AddGroupMember: Adds malicious guest users to any group the attacker has permission to modify, thereby inheriting the group’s privileges. This can grant access to sensitive resources like SharePoint sites or administrative roles.
Get-UpdatableGroups: Discover which groups the compromised account can update, providing a roadmap for escalating privileges or broadening access within the tenant.
Example Attack Flow Using GraphRunner:
Invite Malicious Guest:
Invoke-InviteGuest -Email "attacker@something.com"2. Identify Vulnerable Dynamic Groups:
$groups = Get-DynamicGroups -Tokens $tokens | Where-Object { $_.MembershipRule -match "admin|support" }Add Guest to Vulnerable Groups:
foreach ($group in $groups) { Invoke-AddGroupMember -Tokens $tokens -groupId $group.Id -userId "<Enter OID here>" }Maintain Persistence:
Get-UpdatableGroups -Tokens $tokens | foreach { Invoke-AddGroupMember -groupId $_.Id -userId "<Enter OID here>" }This is what the final output should look like, obviously on bigger tenants, the list of updatable groups is going to be a lot longer

P.S.
If you need a quick way to get your guest account's OID, here's a Microsoft Graph command to run:
Get-MgUser -Filter "Mail eq '<Enter email here>'" | Select-Object IdPersistence
Once inside your Entra ID tenant, adversaries aim to maintain their foothold through several methods directly related to guest access:
Creating multiple guest accounts through different email providers to ensure backup access if some accounts are discovered and removed.
Adding their guest accounts to various groups with different permission levels, if one group membership is revoked, they still maintain access through others.
Exploiting dynamic group rules to automatically regain access, for example, if a rule adds users with certain email domains or attributes, they can create new accounts matching these criteria.
Using existing guest access to invite additional guests, creating a web of accounts that's harder to fully clean up.
Identifying and joining "forgotten" or rarely monitored groups that still have valuable permissions but aren't actively managed.
Detection and IOCs
Look for out of hours or strange IPs and suspicious domains.
Monitor new guests in admin groups and unexpected changes to dynamic rules.
Set alerts for directory changes or cost spikes that might indicate cryptomining.
Watch for bulk invites or rapid group modifications in Entra ID logs.
KQL Queries
AuditLogs
| where (OperationName == "Invite external user" or (OperationName == "Add user" and AdditionalDetails[0].value == "Microsoft Azure Graph Client Library 1.0"))
| extend UserUPN = TargetResources[0].userPrincipalNameThis query searches AuditLogs for events where external users were invited or new users were added via the Microsoft Azure Graph Client Library. It helps spot potentially suspicious additions, including those performed using automated tools.
MicrosoftGraphActivityLogs
| where UserAgent contains "PowerShell"
| where (RequestUri == "https://graph.microsoft.com/v1.0/invitations" or RequestUri == "https://graph.microsoft.com/v1.0/organization")This query identifies activities performed through Microsoft Graph API specifically by PowerShell, focusing on guest invitations or modifications to the organization’s configuration. Unusual use of PowerShell in this context could indicate automation by attackers.
AuditLogs
| where AdditionalDetails[0].value contains "PowerShell"
| where OperationName == "Add member to group"
| extend UserUPN = TargetResources[0].userPrincipalName, GroupID = TargetResources[1].idThis query captures instances where users are added to groups through PowerShell, which is often used by attackers to automate group membership modifications.
OfficeActivity
| where Operation == "Add member to group."This query checks Office Activity logs specifically for operations adding members to groups. Sudden or unexpected group additions might indicate an attacker’s attempt to escalate privileges.
MicrosoftGraphActivityLogs
| where UserAgent contains "PowerShell"
| where RequestUri has_all("https://graph.microsoft.com/v1.0/groups/","/members/$ref")
| extend GroupObjectId = tostring(extract(@"groups/(.*?)/members", 1, RequestUri))This query highlights automated attempts to add users to groups using Microsoft Graph API via PowerShell. Frequent or unexpected entries here could indicate malicious activity.
MicrosoftGraphActivityLogs
| where UserAgent contains "PowerShell"
| where RequestUri == "https://graph.microsoft.com/v1.0/groups" This query monitors for changes to dynamic group definitions, potentially identifying attackers altering group rules to gain persistent access or elevate privileges.
MicrosoftGraphActivityLogs
| where UserAgent contains "PowerShell"
| where RequestUri == "https://graph.microsoft.com/beta/roleManagement/directory/estimateAccess" or RequestUri == "https://graph.microsoft.com/v1.0/groups"
| project-reorder TimeGenerated, RequestUriThis query reveals activities aimed at discovering which groups can be modified by a specific account, often the first step for attackers planning privilege escalation or broader infiltration.
AuditLogs
| where OperationName == "Invite external user"
| where TimeGenerated > ago(1d)
| summarize count() by InitiatedBy, bin(TimeGenerated, 1h)
| where count_ > 10This query monitors the "AuditLogs" for the "Invite external user" operation within the last 24 hours, then counts invitations by initiator in one-hour bins. It flags any initiator who has invited more than ten external users in that timeframe to catch large-scale or potentially malicious invite bursts.
let privilegedRoles = dynamic(["Global Administrator","Application Administrator","Cloud Application Administrator"]);
AuditLogs
| where OperationName == "Add member to role"
| where Result == "success"
| where TargetResources has_any(privilegedRoles)
| where tostring(TargetResources[0].userPrincipalName) contains "#EXT#"This query filters the "AuditLogs" for successful role additions to certain privileged roles, then checks whether the user principal name assigned to the role is an external account (contains "#EXT#"). If an outside account is assigned a privileged role, it highlights a suspicious or unauthorized privilege grant.
AzureActivity
| where OperationNameValue has "microsoft.subscription/subscriptions/providers/roleassignments/write"
| where Properties contains "Owner"
| where Properties contains "#EXT#"This query searches "AzureActivity" for assignments of the Owner role, then looks for the "#EXT#" string in the event properties. This helps detect scenarios where a guest user is made a subscription Owner, which is risky because it grants broad administrative permissions and potential billing abuse.
let riskyAzureSignIns = (
AADSignInEventsBeta
| where Timestamp > ago(7d)
| where ErrorCode == 0
| where Application == "Azure Portal"
| where RiskLevelAggregated == 100 or RiskLevelDuringSignIn == 100
| project AccountObjectId, RiskySignInTimestamp = Timestamp
);
let resourceGroupCreation = (
CloudAppEvents
| where Timestamp > ago(7d)
| where Application == "Microsoft Azure"
| where ActionType == "Write ResourceGroup"
| project AccountObjectId, ResourceGroupCreation = Timestamp
);
riskyAzureSignIns
| join resourceGroupCreation on AccountObjectId
| where ResourceGroupCreation between (RiskySignInTimestamp .. (RiskySignInTimestamp + 12h))
This query collects sign-ins with a high risk score and joins them with resource group creation events in a 12-hour window. If the same account performed a risky sign-in and then created a resource group, it suggests that a compromised account is being used to stand up new resources under the radar.
let threshold = 3; let newGuestAccounts = ( CloudAppEvents
| where Timestamp > ago(7d)
| where ActionType == "Add user."
| where RawEventData.ResultStatus == "Success"
| where RawEventData has "guest" and RawEventData.ObjectId has "#EXT#"
| mv-expand Property = RawEventData.ModifiedProperties
| where Property.Name == "AccountEnabled" and Property.NewValue has "true"
| project newGuestAccountObjectId = tostring(RawEventData.Target[1].ID)
| distinct newGuestAccountObjectId ); CloudAppEvents
| where Timestamp > ago(7d)
| where isnotempty(toscalar(newGuestAccounts))
| where Application == "Microsoft Azure"
| where ActionType == "Validate Deployments"
| where RawEventData contains "createVm"
| where AccountObjectId in (newGuestAccounts)
| summarize VMCreationCount = count() by AccountObjectId
| where VMCreationCount > thresholdThis query identifies newly added guest accounts by analyzing "CloudAppEvents" for user creations, then checks if those same accounts have created more than a set threshold of virtual machines. This approach reveals guests who quickly provision multiple VMs, a behavior that often signals malicious intent.
let Roles = pack_array("Company Administrator"); let newGuestAccounts = ( CloudAppEvents
| where Timestamp > ago(7d)
| where ActionType == "Add user."
| where RawEventData.ResultStatus == "Success"
| where RawEventData has "guest" and RawEventData.ObjectId has "#EXT#"
| mv-expand Property = RawEventData.ModifiedProperties
| where Property.Name == "AccountEnabled" and Property.NewValue has "true" | project CreationTimestamp = Timestamp, AccountObjectId, AccountDisplayName, newGuestAccount = RawEventData.ObjectId, newGuestAccountObjectId = tostring(RawEventData.Target[1].ID), UserAgent ); let promotedAccounts = ( CloudAppEvents
| where Timestamp > ago(7d)
| where isnotempty(AccountObjectId)
| where ActionType == "Add member to role."
| where RawEventData.ResultStatus == "Success"
| where RawEventData has_any(Roles)
| where RawEventData.Actor has "User"
| project PromoteTimestamp = Timestamp, PromotedUserAccountObjectId = tostring(RawEventData.Target[1].ID) ); newGuestAccounts
| join promotedAccounts on $left.newGuestAccountObjectId == $right.PromotedUserAccountObjectId
| where PromoteTimestamp > CreationTimestamp
| project CreationTimestamp, PromoteTimestamp, PromotedUserAccountObjectId, newGuestAccount, newGuestAccountObjectIdThis query tracks the creation of external guest accounts alongside subsequent role assignments involving "Company Administrator." It compiles a list of new guests, checks whether they’ve been added to that specific role, and flags anyone promoted after creation to expose suspicious privilege escalation.
Mitigation
Restrict Guest Invitations to Admins
By limiting who can invite external users, you reduce the risk of unauthorized or malicious guest accounts being added.
Open your web browser and navigate to the Azure Portal. Sign in with your administrator account.
Once signed in, locate and click on Microsoft Entra ID from the left-hand menu.
In the Entra ID overview pane, select Users and then User settings.
Under User settings, find the Manage external collaboration settings section and click on it.
In the Manage External collaboration settings, locate the Guest invite settings. Adjust the permissions:
Set Guest invite restrictions to Only users assigned to specific admin roles can invite guest users.
Choose which admin roles are permitted to invite guests. Typically, roles like Global Administrator or User Administrator should have this privilege.
Regularly review your guest invitation policies and monitor for any unauthorized attempts to invite guests. Ensure that only trusted personnel have the necessary permissions to manage guest access.
Reassess Dynamic Group Rules
Overly broad or poorly defined group rules can unintentionally grant elevated privileges to external users.
Ensure that dynamic group rules are specific and not easily exploitable. Avoid using simple string matches like
-startsWith "admin"
Periodically review dynamic group definitions and memberships to ensure they align with current security policies and business needs.
Example of Auditing Dynamic Groups:
Get-AzureADMSGroup
| Where-Object {$_.GroupTypes -contains "DynamicMembership"}
| Select-Object DisplayName, MembershipRuleSecure Azure Subscriptions
Limiting who can manage subscriptions prevents adversaries from transferring control or incurring unauthorized costs.
Restrict the assignment of Subscription Owner roles to a minimal, trusted group of individuals.
Implement alerts for any attempts to change the directory (tenant) associated with a subscription.
Monitor for Automated Tooling
Automated tools can execute large scale, rapid attacks that are harder to detect manually.
Ensure that all Microsoft Entra B2B and Microsoft 365 activities are logged and forwarded to a secure SIEM solution like Microsoft Sentinel.
Create custom alerts for activities indicative of automation tools, such as bulk guest invites or rapid group membership changes.
Enforce MFA and Conditional Access
On a personal note, I really hate recommending the implementation of MFA, as this should be a default and mandatory requirement in Azure, and honestly, every Cloud provider.
Multi-Factor Authentication (MFA) and Conditional Access add layers of security, making it harder for compromised credentials to be exploited.
Require MFA for all privileged accounts to add an extra layer of security against compromised credentials.
Implement policies that restrict access based on factors like user location, device compliance, and risk level.
Edit: I started writing this blogpost a couple of months ago, but never got to finish it, and by the time I did, Microsoft had already rolled out Mandatory MFA to Azure tenants.
References:
https://trustedsec.com/blog/unwelcome-guest-abusing-azure-guest-access-to-dump-users-groups-and-more
https://www.invictus-ir.com/news/a-defenders-guide-to-graphrunner-part-ii https://www.praetorian.com/blog/azure-guest-users-and-insecure-defaults/



Comments