Introduction

Owner

Difficulty:
Help Goose James near the park discover the accidentally leaked SAS token in a public JavaScript file and determine what Azure Storage resource it exposes and what permissions it grants.

James the Goose is in the park with a cranberry-pi:

image-20251107165044188
James

James

The Neighborhood HOA uses Azure for their IT infrastructure.

The Neighborhood network admins use RBAC for access control.

Your task is to audit their RBAC configuration to ensure they’re following security best practices.

They claim all elevated access uses PIM, but you need to verify there are no permanently assigned Owner roles.

The terminal opens up a terminal with two panes. The top pane has instructions, and the bottom a place to interact:

image-20251107165515749

Solution

Background

Azure Role-Based Access Control (RBAC) determines what actions users can perform on Azure resources. The Owner role is the most privileged, granting full access including the ability to assign roles to others.

Azure organizes RBAC in a hierarchy:

  • Management Group → Subscription → Resource Group → Resource

Permissions assigned at higher levels inherit downward. Owner at the subscription level means full control over everything in that subscription.

Privileged Identity Management (PIM) is a security best practice that provides just-in-time privileged access. Instead of permanent Owner assignments, users activate the role when needed for a limited time. This reduces the attack surface since compromised accounts don’t have standing privileges.

#1

Let’s learn some more Azure CLI, the –query parameter with JMESPath syntax! $ az account list --query "[].name" Here, [] loops through each item, .name grabs the name field

I learned here that az has a jq-like syntax for processing the JSON output!

neighbor@55d1228527fd:~$ az account list --query "[].name"
[
  "theneighborhood-sub",
  "theneighborhood-sub-2",
  "theneighborhood-sub-3",
  "theneighborhood-sub-4"
]

There are four accounts.

#2

You can do some more advanced queries using conditional filtering with custom output. $ az account list --query "[?state=='Enabled'].{Name:name, ID:id}" Cool! 😎 [?condition] filters what you want, {custom:fields} makes clean output ✨

This query shows I can add filtering (though the same four come back, so they all must be enabled) and control the output:

neighbor@55d1228527fd:~$ az account list --query "[?state=='Enabled'].{Name:name, ID:id}"
[
  {
    "ID": "2b0942f3-9bca-484b-a508-abdae2db5e64",
    "Name": "theneighborhood-sub"
  },
  {
    "ID": "4d9dbf2a-90b4-4d40-a97f-dc51f3c3d46e",
    "Name": "theneighborhood-sub-2"
  },
  {
    "ID": "065cc24a-077e-40b9-b666-2f4dd9f3a617",
    "Name": "theneighborhood-sub-3"
  },
  {
    "ID": "681c0111-ca84-47b2-808d-d8be2325b380",
    "Name": "theneighborhood-sub-4"
  }
]

#3

Let’s take a look at the Owner’s of the first listed subscription 🔍. Pass in the first subscription id. Try: az role assignment list --scope "/subscriptions/{ID of first Subscription}" --query [?roleDefinition=='Owner']

It wants me to get info on the owner of the theneighborhood-sub account:

neighbor@55d1228527fd:~$ az role assignment list --scope "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64" --query [?roleDefinition=='Owner']
[
  {
    "condition": "null",
    "conditionVersion": "null",
    "createdBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
    "createdOn": "2025-09-10T15:45:12.439266+00:00",
    "delegatedManagedIdentityResourceId": "null",
    "description": "null",
    "id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/providers/Microsoft.Authorization/roleAssignments/b1c69caa-a4d6-449a-a090-efacb23b55f3",
    "name": "b1c69caa-a4d6-449a-a090-efacb23b55f3",
    "principalId": "2b5c7aed-2728-4e63-b657-98f759cc0936",
    "principalName": "PIM-Owners",
    "principalType": "Group",
    "roleDefinitionId": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
    "roleDefinitionName": "Owner",
    "scope": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64",
    "type": "Microsoft.Authorization/roleAssignments",
    "updatedBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
    "updatedOn": "2025-09-10T15:45:12.439266+00:00"
  }
]

The principalName is PIM-Owners. This is good security practice because members must explicitly activate their Owner role rather than having it permanently. When no activations are present (as the challenge states), no one currently has Owner access.

#4

Ok 🤔 — there is a group present for the Owners permission; however, we’ve been assured this is a 🔐 PIM enabled group. Currently, no PIM activations are present. 🚨 Let’s run the previous command against the other subscriptions to see what we come up with.

It’s saying that there are no I’ll try for the other three account ids:

neighbor@55d1228527fd:~$ az role assignment list --scope "/subscriptions/4d9dbf2a-90b4-4d40-a97f-dc51f3c3d46e" --query [?roleDefinition=='Owner']
[
  {
    "condition": "null",
    "conditionVersion": "null",
    "createdBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
    "createdOn": "2025-09-10T15:45:12.439266+00:00",
    "delegatedManagedIdentityResourceId": "null",
    "description": "null",
    "id": "/subscriptions/4d9dbf2a-90b4-4d40-a97f-dc51f3c3d46e/providers/Microsoft.Authorization/roleAssignments/b1c69caa-a4d6-449a-a090-efacb23b55f3",
    "name": "b1c69caa-a4d6-449a-a090-efacb23b55f3",
    "principalId": "2b5c7aed-2728-4e63-b657-98f759cc0936",
    "principalName": "PIM-Owners",
    "principalType": "Group",
    "roleDefinitionId": "/subscriptions/4d9dbf2a-90b4-4d40-a97f-dc51f3c3d46e/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
    "roleDefinitionName": "Owner",
    "scope": "/subscriptions/4d9dbf2a-90b4-4d40-a97f-dc51f3c3d46e",
    "type": "Microsoft.Authorization/roleAssignments",
    "updatedBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
    "updatedOn": "2025-09-10T15:45:12.439266+00:00"
  }
]
neighbor@55d1228527fd:~$ az role assignment list --scope "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617" --query [?roleDefinition=='Owner']
[                 
  {                               
    "condition": "null",                                                 
    "conditionVersion": "null",                                          
    "createdBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
    "createdOn": "2025-09-10T15:45:12.439266+00:00",
    "delegatedManagedIdentityResourceId": "null",
    "description": "null",
    "id": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617/providers/Microsoft.Authorization/roleAssignments/b1c69caa-a4d6-449a-a090-efacb23b55
f3",
    "name": "b1c69caa-a4d6-449a-a090-efacb23b55f3",
    "principalId": "2b5c7aed-2728-4e63-b657-98f759cc0936",
    "principalName": "PIM-Owners",
    "principalType": "Group",
    "roleDefinitionId": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a
75c-2fe8c4bcb635",
    "roleDefinitionName": "Owner",
    "scope": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617",
    "type": "Microsoft.Authorization/roleAssignments",
    "updatedBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
    "updatedOn": "2025-09-10T15:45:12.439266+00:00"
  },
  {
    "condition": "null",
    "conditionVersion": "null",
    "createdBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
    "createdOn": "2025-09-10T16:58:16.317381+00:00",
    "delegatedManagedIdentityResourceId": "null",
    "description": "null",
    "id": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617/providers/Microsoft.Authorization/roleAssignments/6b452f58-6872-4064-ae9b-78742e8d987e",
    "name": "6b452f58-6872-4064-ae9b-78742e8d987e",
    "principalId": "6b982f2f-78a0-44a8-b915-79240b2b4796",
    "principalName": "IT Admins",
    "principalType": "Group",
    "roleDefinitionId": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
    "roleDefinitionName": "Owner",
    "scope": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617",
    "type": "Microsoft.Authorization/roleAssignments",
    "updatedBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
    "updatedOn": "2025-09-10T16:58:16.317381+00:00"
  }
]
neighbor@55d1228527fd:~$ az role assignment list --scope "/subscriptions/681c0111-ca84-47b2-808d-d8be2325b380" --query [?roleDefinition=='Owner']
[
  {
    "condition": "null",
    "conditionVersion": "null",
    "createdBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
    "createdOn": "2025-09-10T15:45:12.439266+00:00",
    "delegatedManagedIdentityResourceId": "null",
    "description": "null",
    "id": "/subscriptions/681c0111-ca84-47b2-808d-d8be2325b380/providers/Microsoft.Authorization/roleAssignments/b1c69caa-a4d6-449a-a090-efacb23b55f3",
    "name": "b1c69caa-a4d6-449a-a090-efacb23b55f3",
    "principalId": "2b5c7aed-2728-4e63-b657-98f759cc0936",
    "principalName": "PIM-Owners",
    "principalType": "Group",
    "roleDefinitionId": "/subscriptions/681c0111-ca84-47b2-808d-d8be2325b380/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
    "roleDefinitionName": "Owner",
    "scope": "/subscriptions/681c0111-ca84-47b2-808d-d8be2325b380",
    "type": "Microsoft.Authorization/roleAssignments",
    "updatedBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
    "updatedOn": "2025-09-10T15:45:12.439266+00:00"
  }
]

The third subscription (theneighborhood-sub-3) has two Owner assignments: the expected PIM-Owners group, but also an “IT Admins” group. This second group is a permanent assignment, not PIM-protected, which violates the principle of least privilege.

#5

Looks like you are on to something here! 🕵️ We were assured that only the 🔐 PIM group was present for each subscription. 🔎 Let’s figure out the membership of that group. Hint: use the az ad member list command. Pass the group id instead of the name. Remember: | less lets you scroll through long output

I’ll look at the group for members:

neighbor@55d1228527fd:~$ az ad group member list --group 6b982f2f-78a0-44a8-b915-79240b2b4796
[
  {
    "@odata.type": "#microsoft.graph.group",
    "classification": null,
    "createdDateTime": "2025-09-10T16:54:24Z",
    "creationOptions": [],
    "deletedDateTime": null,
    "description": null,
    "displayName": "Subscription Admins",
    "expirationDateTime": null,
    "groupTypes": [],
    "id": "631ebd3f-39f9-4492-a780-aef2aec8c94e",
    "isAssignableToRole": null,
    "mail": null,
    "mailEnabled": false,
    "mailNickname": "15a80d1d-5",
    "membershipRule": null,
    "membershipRuleProcessingState": null,
    "onPremisesDomainName": null,
    "onPremisesLastSyncDateTime": null,
    "onPremisesNetBiosName": null,
    "onPremisesProvisioningErrors": [],
    "onPremisesSamAccountName": null,
    "onPremisesSecurityIdentifier": null,
    "onPremisesSyncEnabled": null,
    "preferredDataLocation": null,
    "preferredLanguage": null,
    "proxyAddresses": [],
    "renewedDateTime": "2025-09-10T16:54:24Z",
    "resourceBehaviorOptions": [],
    "resourceProvisioningOptions": [],
    "securityEnabled": true,
    "securityIdentifier": "S-1-12-1-1662958911-1150433785-4071522471-1321846958",
    "serviceProvisioningErrors": [],
    "theme": null,
    "uniqueName": null,
    "visibility": null
  }
]

I can get just the displayName, the id, and the type for each user in the group:

neighbor@55d1228527fd:~$ az ad group member list --group 6b982f2f-78a0-44a8-b915-79240b2b4796 | jq '.[] | [.displayName, .id, ."@odata.type"]'
[
  "Subscription Admins",
  "631ebd3f-39f9-4492-a780-aef2aec8c94e",
  "#microsoft.graph.group"
]

It’s a nested group! Nested groups are a security auditing challenge because permissions aren’t immediately visible. An attacker who compromises a member of a nested group gains the parent group’s permissions, and auditors might miss these indirect access paths.

#6

Well 😤, that’s annoying. Looks like we have a nested group! Let’s run the command one more time against this group.

That group has one user:

neighbor@55d1228527fd:~$ az ad group member list --group 631ebd3f-39f9-4492-a780-aef2aec8c94e | jq '.[] | [.displayName, .id, ."@odata.type"]'
[
  "Firewall Frank",
  "b8613dd2-5e33-4d77-91fb-b4f2338c19c9",
  "#microsoft.graph.user"
]

Outro

On completing question 6, the banner says:

elevated access instead of permanent assignments. Permanent Owner roles create persistent attack paths and violate least-privilege principles.

Challenge Complete! To finish, type: finish

Running finish completes the challenge.

Token Exposure

Congratulations! You have completed the Token Exposure challenge!

James is validated:

James

James

You found the permanent assignments! CLUCK! See, I’m not crazy - the security really WAS misconfigured. Now maybe I can finally get some peace and quiet…