This is a story about how patience during a pentest pays off.
Actually, it’s more than that.
It’s really about a digital bank heist.
Sort of.
It’s kind of both. Let me explain.
A few years back, I was working on an engagement for a customer who handles financial transactions and transfers. This included an internal API with a dependency on a third-party banking API.
We’ll get to the banking API later in this story. It’s a fun twist that ultimately allowed me to transfer any amount of money from any account to another without being detected. It was the perfect digital bank heist where I could steal billions.
But first, I need to show you how I figured out about the dependency and stumbled upon the internal API.
That’s the real story.
If you regularly read my articles, you know I preach about using a systematic approach to hacking APIs. Your methodology will change as you learn and experience new things… but staying focused and following it is a recipe for success.
Or, at the very least, it ensures you test the right things at the right time.
That served me well on this engagement. Mainly because what I found was NOT expected.
That’s all thanks to developers embracing agile development, microservices, and API gateway redirection that exposed something no one considered.
I’ve had this customer for years. I’ve come to know their tech stack well. And it’s fair to say I may sometimes run a little fast and loose when it comes to what gets pentested during each engagement.
Over the years, the customer has moved from larger timeboxed annual pentesting to a form of continuous pentesting where we focus on areas where code has changed or been added every month. So the Statement of Work is a bit loose… with a trade-off being that the customer provides me with information on what areas developers have changed so I can focus my time more efficiently.
And herein lies the rub.
Developers lie.
Well, more to the point, they don’t always tell you everything.
They aren’t trying to be malicious. They just tend to forget to mention things that they think are trivial or don’t matter in the grand scope of things.
Move fast to get sh*t done. That’s their creed.
When you only focus on the fragility of new code, you can’t always see how this code may impact aging code unless you check. Which is more challenging to do in smaller incremental engagements.
And that was precisely what happened here.
So things change. The whole point of API pentesting is to look at the target objectively with a clear and unbiased view. While it’s important to trust what I’m being told, I must verify everything anyway.
This engagement reminded me why that’s so important.
In this case, it was how the API framework was configured and used.
The API servers were configured for years to only support a Content-Type
of ‘application/json
‘. This makes sense since it was a RESTful API that relied on data models structured in JSON.
In fact, we’ve had automated tests that verify that using any other Content-Type would return an HTTP status code of 415 (unsupported media type). And that has worked well.
Until it didn’t.
It turned out that the developers were working on an internal API to bridge between the main API and a third party that processes currency conversion and transfers. As that external API required an XML call-back service, a slight tweak was made to the API framework to allow for a Content-Type of ‘application/xml
‘ for the new service endpoint.
Or so they thought.
What happened when the devs allowed for application/xml
as a supported media type? They accidentally reconfigured the API server to support both JSON and XML for ALL endpoints.
Now, I know what you’re thinking… “why didn’t the developers tell me about this config change?”
In the retrospective, it was explained that they didn’t want me to test those endpoints yet as they weren’t expected to be done for a couple more sprints. They assumed the API gateway would protect them as they had yet to publish the new endpoints through it.
They never realized the change to the framework exposed all the other endpoints too.
So why didn’t the automated tests catch this?
Ahhh… that’s a great question. Let me sum it up by saying API gateways aren’t always the smartest. How it processes requests and reprocesses headers sometimes makes it work in weird ways.
In this case, it returned 415 codes to the test server because it processed the request differently internally than from an external client. So, the tests were passing because the API gateway failed the request… not the API server.
This one little config change to the API server to allow XML let me get a foothold on the API server thanks to XML Entity Injection (XXE).
Well, that and a poorly configured API gateway that didn’t filter that out for unauthenticated requests.
You’ll see why that matters in a minute.
This let me exfiltrate the API artifacts from the server and ultimately reverse engineer the compiled API into source code… allowing me to find out about the external banking API before I should have known.
Let me show you how I did that.
Whenever you see an endpoint that accepts a JSON payload, see if you can convert it to XML and resend it.
Tainting data in weird places should be part of your methodology. I’ve talked about this before.
Request headers like Content-Type are fair game.
Burp Suite is great for this, with some help from a free BApp extension called Content Type Converter.
Look for places where successful POST or PUT operations are being done and send them to the Repeater tool. Then right-click on the body of the request and select Extensions > Content Type Converter > Convert To XML. It will automatically convert the JSON payload into well-structured XML and change the Content-Type accordingly.
Now send the modified request to the server.
Did it succeed? If so, the endpoint accepts XML, and you can try to abuse that.
If it doesn’t work, try removing the Content-Type header. Some API frameworks will auto-detect the media type based on the payload. This technique has the added benefit that it can sometimes bypass content type filtering in WAF and API gateways.
Finally, if that doesn’t work, change the Content-Type to ‘application/xml;charset=UTF-8
‘. It’s a last-ditch effort… but you’d be surprised how many filters have weak blacklist filters that can be bypassed by adding the charset in.
Have an endpoint that accepts XML? Great. Let’s abuse it.
I’m not going to bore you with instructions on how to exploit XXE. This is already well documented in places like PortSwigger’s XXE module within the Web Security Academy. What I will do is explain my thinking and approach to it.
Whenever I conduct a pentest, one of my personal goals is to get access to API artifacts and ultimately the source code. It is far easier to perform code analysis and taint tracing when you have it.
Have no clue what I am talking about? Read this.
Anyways, the potential for an XXE vuln is perfect. Done right, I should be able to exfiltrate files.
In my case, I was able to start by adding a standard <DOCTYPE> to the payload and see if it can bring back basic files:
<!DOCTYPE danawuzhere [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
I put an external entity of &xxe;
into a field that was reflected in the response, and it leaked the passwd file.
Whoohoo!
But how could I use this to get the API artifacts?
Since I knew the API was written in Java using a special framework, I had a good idea of where to look for the files I wanted. Since they were compiled into JAR/WAR files, which are basically archives… I would have to download something more than text.
I’ll worry about that in a minute.
First, I needed to know the full path, including the filenames, of the API artifacts I wanted.
Lucky for me, since Java was in use, I could take advantage of a little-known issue in Java that lets you get directory listings using the file://
protocol if it ends with a forward slash.
So if I want to list all files in the /etc directory, I just need to pass in “file:///etc/
“.
With me? Awesome. So I just used my XXE payload several times to traverse the directories until I found the .jar file I needed.
So JAR and WAR files are just renamed ZIP archives. Trying to use normal text-based XXE to download them won’t work.
Remember, it is UTF-8, not a binary stream.
However, with a bit of patience and some luck, I found a workaround.
In this case, the API server was built upon a base image that included PHP binaries. I took advantage of that and used the built-in PHP filters to base64 encode the file before exfiltrating it.
The payload was adjusted accordingly:
<!DOCTYPE danawuzhere [ <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/path/to/jar/file"> ]>
And with that, I could download the API artifacts I needed.
Let’s recap.
So far, we’ve tainted a request to send the API server something it wasn’t expecting, which returned to us the compiled assets of the API we wanted. Now to get to the source code.
With the JAR file exfiltrated from the server and base64 decoded, I decompiled it back to a reasonable representation of the source code.
Tools like JD-GUI and JD-CLI can do this.
Once decompiled, running simple source code auditing tools like graudit lets us peek into potentially suspicious code. In my case, I have some custom auditing tools that parse out URL fragments from source code… leading me to discover the new code.
From there, I was able to hone in on the undocumented API endpoints, the expected models and schema, and the corresponding business logic within the API that used the external banking API.
I won’t bore you with the gory details, but the developers were right… they weren’t ready to release the code to this new functionality.
I can’t disclose what was found, but it is fair to say that both the third-party vendor and this customer had to review their threat models and make some changes.
More importantly… and what was more fun… was that because I immediately reported this vulnerability when I did and provided a reasonable PoC exploit for my findings, I was asked to participate in some testing with the third-party vendor on a safe test environment that wouldn’t impact real accounts.
I couldn’t help it. I just had to do it.
With a few keystrokes, I updated my exploit to handle significantly more rogue transactions and ultimately transferred 1 billion dollars to my test account.
It felt so good to send the screenshot of the account balance to the vendor. It was the perfect digital bank heist.
Unfortunately, it just wasn’t real money.
Damn my ethics. Why did I report this again?
In all seriousness, I hope this story resonates with you. And I hope you learned something.
Trust, but verify.
Taint all the things.
Don’t give up.
Be patient.
You’d be surprised what you might find.
Have you joined The API Hacker Inner Circle yet? It’s my FREE weekly newsletter where I share articles like this, along with pro tips, industry insights, and community news that I don’t tend to share publicly. If you haven’t, subscribe at https://apihacker.blog.
The post That time I broke into an API and became a billionaire appeared first on Dana Epp's Blog.
*** This is a Security Bloggers Network syndicated blog from Dana Epp's Blog authored by Dana Epp. Read the original post at: https://danaepp.com/that-time-i-broke-into-an-api-and-became-a-billionaire