Post

Spare Key

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.

Spare Key

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.

Hints

Spare Key

This terminal has built-in hints!

Solution

1
2
3
4
5
🎄 Welcome to the Spare Key! 🎄
You're connected to a read-only Azure CLI session in "The Neighborhood" tenant.
Your mission: Someone left a spare key out in the open. Find WHERE it is.
Connecting you now... ❄️
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1
2
3
4
5
6
7
8
9
10
11
12
Let's start by listing all resource groups
$ az group list -o table
This will show all resource groups in a readable table format.
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
neighbor@c69f6c28686f:~$ 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
1
2
3
4
5
6
7
8
9
10
11
12
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.
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
neighbor@c69f6c28686f:~$ 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
1
2
3
4
5
6
7
8
9
10
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
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
neighbor@c69f6c28686f:~$ az storage blob service-properties show --account-name neighborhoodhoa --auth-mode login
{
  "enabled": true,
  "errorDocument404Path": "404.html",
  "indexDocument": "index.html"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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.
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
neighbor@c69f6c28686f:~$ 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"
    }
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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!
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
neighbor@c69f6c28686f:~$ az storage blob list --account-name neighborhoodhoa --auth-mode login --container-name '$web' 
[
  {
    "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"
      }
    }
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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 💻.
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
neighbor@c69f6c28686f:~$ az storage blob download --account-name neighborhoodhoa --auth-mode login --container-name '$web' --name 'iac/terraform.tfvars' --file /dev/stdout
# 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%2BQxw
8o2i9QMQc%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"
}
1
2
3
4
5
6
7
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
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
neighbor@c69f6c28686f:~$ finish
Completing challenge...
1
2
🎉 Challenge complete! 🎉
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Dissecting the attack

graph TD
    subgraph Recon [1. Cloud Enumeration]
        A["List Storage Accounts
        (az storage account list)"]
        B["List Blobs in '$web'
        (Find iac/terraform.tfvars)"]
    end

    subgraph Analysis [2. Sensitive Data Discovery]
        C["Download Configuration
        (Retrieve tfvars file)"]
        D["Identify Secrets
        (Found migration_sas_token)"]
    end

    subgraph Exploitation [3. Credential Access]
        E["CWE-798: Hardcoded Credential
        (Long-lived SAS Token)"]
        F["Persistent Access
        (Valid until 2100)"]
    end

    %% Flow Connections
    A -->|Identify public container| B
    B -->|Interesting File Name| C
    C -->|Static Analysis| D
    D -->|Extract Token| E
    E -->|Bypass Auth| F

    %% Styling
    style A fill:#7f1d1d,stroke:#ef4444,stroke-width:2px,color:#fff
    style B fill:#9a3412,stroke:#f97316,stroke-width:2px,color:#fff
    style C fill:#7f1d1d,stroke:#ef4444,stroke-width:2px,color:#fff
    style D fill:#9a3412,stroke:#f97316,stroke-width:2px,color:#fff
    style E fill:#7f1d1d,stroke:#ef4444,stroke-width:2px,color:#fff
    style F fill:#7f1d1d,stroke:#ef4444,stroke-width:2px,color:#fff
PhaseVulnerability (CWE)Mitigation
1. Discovery
CWE-219
Storage of File with Sensitive Data Under Web Root
(IaC Files in Public CDN)
Deployment Hygiene
(Exclude Config Files from Artifacts)
2. Credentials
CWE-798
Use of Hard-coded Credentials
(Static SAS Token in tfvars)
Secret Management
(Use Key Vault References)

Fixing the Data Exposure (CWE-219)

Vulnerability: The $web container in Azure Storage is typically used to host static websites (publicly accessible). The deployment process mistakenly uploaded the iac/terraform.tfvars file, a configuration file meant for infrastructure provisioning, into this public web root. This made the sensitive variables readable by anyone who could guess the URL.
Fix: configure the deployment pipeline to Exclude Sensitivity Configuration Files from the upload artifacts. Ensure that .tfvars, .env, and .git directories are listed in .gitignore or the deployment script’s exclusion list.
Secure Deployment Script (Concept):

1
2
3
4
5
6
# FIX: Exclude infrastructure config when uploading web assets
az storage blob upload-batch \
  --source ./src/frontend \
  --destination '$web' \
  --pattern "*" \
  --exclude "iac/*" "*.tfvars"

Impact: Prevents internal configuration artifacts from being served to the public internet.

Fixing the Hardcoded Credential (CWE-798)

Vulnerability: The terraform.tfvars file contained a hardcoded migration_sas_token that was valid until the year 2100. Unlike the other keys in the file (which correctly used @Microsoft.KeyVault references), this token was pasted directly in cleartext.
Fix: Eliminate hardcoded secrets by using Secret Management Services (like Azure Key Vault). The application or infrastructure script should fetch the credential dynamically at runtime using a Managed Identity.
Vulnerable Code (terraform.tfvars):

1
2
# FLAW: Hardcoded secret
migration_sas_token = "?sv=2022-11-02&ss=b&srt=co&sp=rwdlaciytfx&se=2100-01-01..."

Secure Code:

1
2
# FIX: Use Key Vault Reference (like the adjacent keys)
migration_sas_token = "@Microsoft.KeyVault(SecretUri=https://kv-prod.vault.azure.net/secrets/migration-sas/)"

Impact: Even if the file is exposed, the attacker sees only a pointer to the Key Vault. Without the proper Managed Identity permissions to read that vault, the pointer is useless.

This post is licensed under CC BY 4.0 by the author.