Introduction

Spare Key

Difficulty:
Help Goose Barry near the pond identify which identity has been granted excessive Owner permissions at the subscription level, violating the principle of least privilege.

Barry the Goose is next to Grace by the frozen lake with another cranberry-pi:

image-20251107154456994
Barry

Barry

You want me to say what exactly? Do I really look like someone who says MOOO?

The Neighborhood HOA hosts a static website on Azure Storage.

An admin accidentally uploaded an infrastructure config file that contains a long-lived SAS token.

Use Azure CLI to find the leak and report exactly where it lives.

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

image-20251107154539342

Solution

Background

A Shared Access Signature (SAS) token is a URI query string that grants limited access to Azure Storage resources. Instead of sharing account keys, you can create SAS tokens with specific permissions and expiration times. A SAS token looks like:

sv=2023-11-03&ss=bfqt&srt=sco&sp=rwdlacup&se=2025-12-31T23:59:59Z&sig=abc123...

The token encodes:

  • sv - Storage service version
  • ss - Services (b=blob, f=file, q=queue, t=table)
  • srt - Resource types (s=service, c=container, o=object)
  • sp - Permissions (r=read, w=write, d=delete, l=list, a=add, c=create, etc.)
  • se - Expiry time
  • sig - Cryptographic signature

The danger with SAS tokens is that they’re bearer tokens, so anyone who has the token string can use it. Long-lived tokens (like one expiring in 2100) are particularly dangerous because they provide persistent access even if other credentials are rotated.

#1

Let’s start by listing all resource groups $ az group list -o table This will show all resource groups in a readable table format.

This command lists the resource groups in the current account:

neighbor@3b06e76a5204:~$ az group list -o table
Name                 Location    ProvisioningState
-------------------  ----------  -------------------
rg-the-neighborhood  eastus      Succeeded
rg-hoa-maintenance   eastus      Succeeded
rg-hoa-clubhouse     eastus      Succeeded
rg-hoa-security      eastus      Succeeded
rg-hoa-landscaping   eastus      Succeeded

There are five resource groups.

#2

Now let’s find storage accounts in the neighborhood resource group 📦 $ az storage account list --resource-group rg-the-neighborhood -o table This shows what storage accounts exist and their types.

I’m interested in the rg-the-neighborhood resource group, so I’ll look at it’s accounts:

neighbor@3b06e76a5204:~$ az storage account list --resource-group rg-the-neighborhood -o table
Name             Kind         Location    ResourceGroup        ProvisioningState
---------------  -----------  ----------  -------------------  -------------------
neighborhoodhoa  StorageV2    eastus      rg-the-neighborhood  Succeeded
hoamaintenance   StorageV2    eastus      rg-hoa-maintenance   Succeeded
hoaclubhouse     StorageV2    eastus      rg-hoa-clubhouse     Succeeded
hoasecurity      BlobStorage  eastus      rg-hoa-security      Succeeded
hoalandscaping   StorageV2    eastus      rg-hoa-landscaping   Succeeded

There are five accounts.

#3

Someone mentioned there was a website in here. maybe a static website? try:$ az storage blob service-properties show --account-name <insert_account_name> --auth-mode login

Azure Storage accounts can host static websites directly, serving HTML/CSS/JS without needing a separate web server. When enabled, Azure creates a special container named $web where website files are stored. The content is served at a URL like https://<account>.z13.web.core.windows.net/.

I can check for each account above, but it’s only the first that returns having static website hosting enabled:

neighbor@3b06e76a5204:~$ az storage blob service-properties show --account-name neighborhoodhoa --auth-mode login
{
  "enabled": true,
  "errorDocument404Path": "404.html",
  "indexDocument": "index.html"
}

The indexDocument and errorDocument404Path confirm this is configured as a static website.

#4

Let’s see what 📦 containers exist in the storage account 💡 Hint: You will need to use az storage container list We want to list the container and its public access levels.

I’ll use the az storage container list command just like in the previous terminal:

neighbor@3b06e76a5204:~$ az storage container list --account-name neighborhoodhoa --auth-mode login
[
  {
    "name": "$web",
    "properties": {
      "lastModified": "2025-09-20T10:30:00Z",
      "publicAccess": null
    }
  },
  {
    "name": "public",
    "properties": {
      "lastModified": "2025-09-15T14:20:00Z",
      "publicAccess": "Blob"
    }
  }
]

There’s the special $web container for static website content, and a “public” container. The $web container contents are served publicly via the static website URL even though publicAccess shows null. That setting only controls direct blob API access, not the website endpoint.

#5

Examine what files are in the static website container 💡 hint: when using --container-name you might need '<name>' Look 👀 for any files that shouldn’t be publicly accessible!

The public container doesn’t have anything too suspicious:

neighbor@3b06e76a5204:~$ az storage blob list --container-name 'public' --account-name neighborhoodhoa --auth-mode login
[
  {
    "name": "hoa-calendar.json",
    "properties": {
      "contentLength": 256,
      "contentType": "application/json",
      "metadata": {
        "type": "calendar"
      }
    }
  },
  {
    "name": "forms/request-guidelines.txt",
    "properties": {
      "contentLength": 128,
      "contentType": "text/plain",
      "metadata": {
        "type": "instructions"
      }
    }
  }
]

$web has an interesting file, iac/terraform.tfvars. Terraform is an infrastructure-as-code tool, and .tfvars files contain variable values for deployments. These often include sensitive configuration like connection strings, API keys, or tokens:

neighbor@3b06e76a5204:~$ az storage blob list --container-name '$web' --account-name neighborhoodhoa --auth-mode login
[
  {
    "name": "index.html",
    "properties": {
      "contentLength": 512,
      "contentType": "text/html",
      "metadata": {
        "source": "hoa-website"
      }
    }
  },
  {
    "name": "about.html",
    "properties": {
      "contentLength": 384,
      "contentType": "text/html",
      "metadata": {
        "source": "hoa-website"
      }
    }
  },
  {
    "name": "iac/terraform.tfvars",
    "properties": {
      "contentLength": 1024,
      "contentType": "text/plain",
      "metadata": {
        "WARNING": "LEAKED_SECRETS"
      }
    }
  }
]

#6

Take a look at the files here, what stands out? Try examining a suspect file 🕵️: 💡 hint: --file /dev/stdout | less will print to your terminal 💻.

I’ll download the terraform file:

neighbor@3b06e76a5204:~$ az storage blob download -c '$web' --name iac/terraform.tfvars --file /dev/stdout --account-name neighborhoodhoa --auth-mode login
# Terraform Variables for HOA Website Deployment
# Application: Neighborhood HOA Service Request Portal  
# Environment: Production
# Last Updated: 2025-09-20
# DO NOT COMMIT TO PUBLIC REPOS

# === Application Configuration ===
app_name = "hoa-service-portal"
app_version = "2.1.4"
environment = "production"

# === Database Configuration ===
database_server = "sql-neighborhoodhoa.database.windows.net"
database_name = "hoa_requests"
database_username = "hoa_app_user"
# Using Key Vault reference for security
database_password_vault_ref = "@Microsoft.KeyVault(SecretUri=https://kv-neighborhoodhoa-prod.vault.azure.net/secrets/db-password/)"

# === Storage Configuration for File Uploads ===
storage_account = "neighborhoodhoa"
uploads_container = "resident-uploads"
documents_container = "hoa-documents"

# TEMPORARY: Direct storage access for migration script
# WARNING: Remove after data migration to new storage account
# This SAS token provides full access - HIGHLY SENSITIVE!
migration_sas_token = "sv=2023-11-03&ss=b&srt=co&sp=rlacwdx&se=2100-01-01T00:00:00Z&spr=https&sig=1djO1Q%2Bv0wIh7mYi3n%2F7r1d%2F9u9H%2F5%2BQxw8o2i9QMQc%3D"

# === Email Service Configuration ===
# Using Key Vault for sensitive email credentials
sendgrid_api_key_vault_ref = "@Microsoft.KeyVault(SecretUri=https://kv-neighborhoodhoa-prod.vault.azure.net/secrets/sendgrid-key/)"
from_email = "noreply@theneighborhood.com" 
admin_email = "admin@theneighborhood.com"

# === Application Settings ===
session_timeout_minutes = 60
max_file_upload_mb = 10
allowed_file_types = ["pdf", "jpg", "jpeg", "png", "doc", "docx"]

# === Feature Flags ===
enable_online_payments = true
enable_maintenance_requests = true
enable_document_portal = false
enable_resident_directory = true

# === API Keys (Key Vault References) ===
maps_api_key_vault_ref = "@Microsoft.KeyVault(SecretUri=https://kv-neighborhoodhoa-prod.vault.azure.net/secrets/maps-api-key/)"
weather_api_key_vault_ref = "@Microsoft.KeyVault(SecretUri=https://kv-neighborhoodhoa-prod.vault.azure.net/secrets/weather-api-key/)"

# === Notification Settings (Key Vault References) ===
sms_service_vault_ref = "@Microsoft.KeyVault(SecretUri=https://kv-neighborhoodhoa-prod.vault.azure.net/secrets/sms-credentials/)"
notification_webhook_vault_ref = "@Microsoft.KeyVault(SecretUri=https://kv-neighborhoodhoa-prod.vault.azure.net/secrets/slack-webhook/)"

# === Deployment Configuration ===
deploy_static_files_to_cdn = true
cdn_profile = "hoa-cdn-prod"
cache_duration_hours = 24

# Backup schedule
backup_frequency = "daily"
backup_retention_days = 30
{
  "downloaded": true,
  "file": "/dev/stdout"
}

The critical leak here is the migration_sas_token. Breaking it down:

  • sv=2023-11-03 - API version
  • ss=b - Blob service access
  • srt=co - Container and object level access
  • sp=rlacwdx - Read, list, add, create, write, delete, execute permissions (nearly full access)
  • se=2100-01-01 - Expires in 2100 (effectively never)
  • sig=... - The cryptographic signature

This token grants almost complete control over blob storage and doesn’t expire for 75+ years. Anyone who finds this token can read, modify, or delete data in the storage account.

Outro

On completing question 6, the banner says:

You found the leak! A migration_sas_token within /iac/terraform.tfvars exposed a long-lived SAS token (expires 2100-01-01) 🔑 ⚠️ Accidentally uploading config files to $web can leak secrets. 🔐

Challenge Complete! To finish, type: finish

Running finish completes the challenge.

Too Powerfil to Fail

Congratulations! You have completed the Too Powerful to Fail challenge!

Odd that it’s Too Powerful to Fail, and not Spare Key. Perhaps that’s an older name for the challenge?

Barry offers a complement in a grumpy goose-like manner:

Barry

Barry

There it is. A SAS token with read-write-delete permissions, publicly accessible. At least someone around here knows how to do a proper security audit.