July 16, 2026
Recreating the Capital One Breach in My Own Azure Lab
A few weeks ago I decided to stop just reading about the 2019 Capital One breach and actually rebuild it. Not against Capital One…

By Sanidhya Kafle
4 min read
A few weeks ago I decided to stop just reading about the 2019 Capital One breach and actually rebuild it. Not against Capital One, obviously, and not against anyone else's system either. I spun up my own isolated environment on Azure for Students, populated it with fake customer data, and set out to see whether the exact chain of mistakes that led to one of the largest financial data breaches in history was still something a single Kali box could reproduce end to end.
Short answer: yes. It took about six curl commands and one Python script.
Why This Breach Specifically
The Capital One incident always stuck with me because it wasn't the result of some exotic zero-day. A former AWS employee found a misconfigured web application firewall that could be tricked into forwarding requests wherever she wanted, pointed it at the internal metadata service every cloud instance exposes, grabbed a set of temporary credentials that had no business being that powerful, and walked out with roughly 106 million customer records. Three ordinary weaknesses, lined up in exactly the wrong order.
That pattern, a webapp that forwards user input server-side, a metadata endpoint sitting quietly at 169.254.169.254, and a role assignment with more privilege than anyone bothered to trim, is not unique to AWS or to 2019. Azure has the same metadata address, the same identity model, and the same temptation to hand out broad storage roles because it's faster than scoping things properly. So I built the Azure version of that mistake on purpose.
Setting Up the Vulnerable Lab
I created a single resource group and kept everything inside it so cleanup would be trivial:
- A Linux VM (
vulnerable-webapp) running Ubuntu, given a public IP and a system-assigned managed identity - Two storage accounts loaded with synthetic JSON banking records, nothing real, all names and account numbers invented
- A network security group allowing inbound HTTP on port 80 from anywhere, mirroring the loose perimeter in the original case
- A role assignment giving the VM's managed identity Storage Blob Data Contributor on one of those storage accounts, which is far more access than a public-facing app should ever hold
On top of that VM I deployed a small Flask app with one deliberately dangerous route, /fetch. It takes a url query parameter, adds a Metadata: true header, and requests whatever URL it's given. No validation, no allow-list, nothing stopping it from reaching internal addresses. That one function is the entire vulnerability.
Finding the Opening
From my attacker machine I started the way any recon phase starts, quietly and from the outside. A WHOIS lookup on the target IP came back registered to Microsoft, which told me immediately I was looking at an Azure-hosted target and that the metadata service would be sitting at its usual address. A HEAD request and a WhatWeb scan confirmed a Werkzeug development server running on Python, and a quick dirb scan turned up /fetch, returning a 400 error when called without parameters, exactly the kind of endpoint that wants a URL and does something with it server-side.
Confirming the SSRF
Supplying a normal external URL to /fetch returned the full body of that page, proxied straight back to me. That confirmed the server itself was making the outbound request, not my browser. Pointing the same parameter at 127.0.0.1 returned content from the box's own local web server, which meant the request was executing from inside the VM. At that point the only question left was how far inside I could reach.
Reaching the Metadata Service and Stealing a Token
I pointed /fetch at 169.254.169.254 with the metadata query string Azure expects, and the response leaked the VM's resource group, location, OS profile, and admin username. That alone would already be useful reconnaissance, but the real prize was one request away.
Hitting the identity endpoint under that same metadata path returned a live OAuth2 Bearer token scoped to storage.azure.com, tied directly to the VM's managed identity and its Storage Blob Data Contributor role.
That token is functionally identical to a stolen AWS access key in the original breach. It doesn't matter that I never touched a login page or cracked a password. The web app did the work of authenticating for me.
Walking Out With the Data
With that token in hand, a single authenticated curl request against the storage account pulled down bank-records.json directly, bypassing the web application entirely. I automated the whole chain, SSRF, metadata request, token extraction, container enumeration, and download, into one Python script so it could be replayed cleanly. Running it end to end recovered eighteen synthetic customer banking records in a matter of seconds.
I didn't stop at exfiltration. From inside the compromised VM I ran an internal nmap sweep across the local subnet just to see what else was reachable, added a backdoor user to /etc/passwd as a basic persistence mechanism, and then cleared bash history and truncated the auth log to simulate the anti-forensics steps a real intruder would take. Every one of these steps existed purely to demonstrate how naturally post-exploitation follows once that first token is stolen.
The Part That Actually Matters
None of this required any advanced exploit development. The entire chain rests on three plain design choices: an endpoint that fetches arbitrary URLs, a metadata service that answers without question, and a role assignment that was scoped by convenience instead of necessity. Fix any single one of those, allow-list the destinations /fetch can reach, require the extra header protections that block naive SSRF from hitting the metadata service, or scope the managed identity down to read-only access on one container, and the entire chain collapses at that point. You don't need to fix all three, you just need to break one link.
That's really the takeaway I keep coming back to. The Capital One breach didn't need a genius attacker. It needed three tired defaults left unchanged, and I was able to line up the same three defaults in an afternoon.