Holiday Hack 2025: Spare Key
Introduction
Spare Key
Difficulty:❅❅❅❅❅Barry the Goose is next to Grace by the frozen lake with another cranberry-pi:
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:
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 versionss- 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 timesig- 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 tableThis 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 tableThis 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-nameyou 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 | lesswill 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 versionss=b- Blob service accesssrt=co- Container and object level accesssp=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
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.