I am a believer in always trying to produce a decent proof of concept (PoC) exploit. They help to demonstrate the impact of security vulnerabilities found. I’ve talked about this before.
A good exploit helps security triage, product managers, and developers understand how a vulnerability could be leveraged to cause harm. That could be to the users, their data, the underlying resources, and/or the business itself. It also helps level-set and clearly articulates the criticality of a vulnerability.
I’m always looking for new ways to demonstrate this. I am a HUGE fan of using Python scripts to do it. However, I recognize that not everyone understands the programming language. When demonstrating vulnerabilities in APIs, I usually craft minimalistic cURL one-liners. I’ve even written about how to use cURL to write exploits before.
If an attack kill chain requires multiple steps (and the more critical vulns usually do), one-liners aren’t easy to produce. We fall back to using bash scripts or Python to demonstrate the PoC exploit.
It got me thinking. Is there a more visual way we could demonstrate an exploit kill chain?
Postman introduces “Flows”
One of the more recent features in Postman is something called Flows. It’s a visual programming interface that allows you to arrange and connect APIs without writing code.
I’ve never used Flows before. Postman sells the idea of the feature as a visual tool for building API-driven applications for the API-first world. They say we can use flows to chain requests, handle data, and create real-world workflows in our Postman workspace.
It sounds like the perfect environment to design and run our exploits visually.
In this article, I am going to bring you along as I give Postman Flows a try. Does it live up to the hype and allow me to design and run my exploits visually within Postman? Are the Flows easy to understand and follow? Can I share them with security triage and other developers as part of my vulnerability reporting process?
Let’s find out.
The goal for today isn’t about demonstrating the wickedest exploit I can write. But I do need it to showcase a few things. These include multiple requests, data transforms (i.e., for payload injection, etc.), conditional checking, and visual “flow” down a kill chain.
I have the perfect vulnerability in mind for this.
OWASP’s Completely Ridiculous API (crAPI) has a vulnerability in the password reset process that allows for account takeover (ATO). It abuses a weakness that allows an attacker to bruteforce the OTP that is emailed to an account during reset. On success, the attacker can change the password of a victim’s account and take it over.
Understanding the attack kill chain
So our exploit will need to work like this:
- We start the password reset process by POSTing to
/identity/api/auth/forget-password. This will set the target account into reset mode and generate a new OTP that is emailed to the victim.
- This OTP is a 4-digit string value ranging from 0000 to 9999.
- We will need to iterate through every possible OTP and send it to
/identity/api/auth/v2/check-otpto see if it’s valid for the account we are taking over. Because this endpoint has no rate limiting, we can hammer on the API server until we find the right one.
- Once we find it, we must stop iterating through all potential 10,000 OTPs. We then need to let the attacker know we found the OTP and reset the victim’s password.
Seems easy enough.
Now to figure out how Postman Flows works.
The Basics of Postman Flows
Let me give you the barebones guide to what I’ve had to learn to make my first exploit in Flows.
When you create your first flow (by hitting “+” in the Flows tab), you will be presented with an empty, infinite canvas with a Start block.
You can zoom in and out of the canvas using a pinch gesture on your trackpad. You can pan around the canvas by left-clicking on it and moving it in the direction you want to go.
If you right-click on the canvas, you can choose an object (commonly called a block) to drop onto it.
Outside of the Start block that every Flow canvas has, there are five types of blocks that you can use:
- Information blocks – These include variables, templates, and structured data. You can define data types (i.e., number, string, bool, etc.) or more complex objects like Records, Lists, Date, and even regex objects.
- Decision blocks – These include If blocks for decision branching and Evaluate blocks, which allow you to work and compare variables using a proprietary language called Flows Query Language (FQL).
- Repeating blocks – These allow you to Repeat a section a fixed number of times or loop through an array of elements using a For syntax. This is VERY rudimentary looping and quite cumbersome (more on that later).
- Action blocks – These allow you to Send Requests to an API endpoint and Delay for a period of time.
- Output blocks – These allow you to Log to the console or Output a value to the Flow canvas. When using the Output block, it can render many types of data, including strings, charts, tables, videos, images, and raw JSON.
For a full list of possible blocks you can use in a Flow, you can read the docs here.
Connections enable block interactions. By dragging a connection from one block’s output to another’s input, you can either trigger the second block or transfer information to it for execution.
Flows Query Language (FQL)
The Flows Query Language (FQL) is used to parse and transform JSON data to get the fields and structure you want. You find these in Evaluate blocks and are helpful to transform data.
I found this very helpful when having to produce the unique OTPs. I even used the built-in PostBot AI to help me figure it out. I’ll show you how in a minute.
Running a Flow is as simple as hitting the Run/Play button in the middle bottom of the canvas. When running, you will see dots moving between blocks to tell you where the current Flow is.
More interesting is that you can click on any block to see the results from that “step” in the flow. You can even click on the outputs and compare them to see how it’s going. Very helpful when debugging a Flow.
Designing our First Exploit in Flows
OK, now that we understand the building blocks of a Flow, let’s go about building our exploit.
Annotating the Canvas
Being a visual medium, the canvas is a great way to describe the exploit and help set up the attack. Postman includes a way to drop text right on the canvas, including turning the content into H1 or H2 headers or styling it with bold, italic, or strikethrough.
You can also set up variables, which I initially thought would help define what inputs are needed to run the exploit, like setting the URL of the target API server and the victim account we want to take over. It looked something like this:
Not a bad way to start describing an exploit using a Flow.
I would later find out this would bite me.
My first failure at designing an exploit Flow
I am going to let you in on a little secret. My first attempt at building this flow failed miserably. Some of it was my fault for not understanding the capabilities of Postman Flows. Some of it was because of missed expectations on how things currently work.
I was lucky enough to get on a Zoom call with an engineer from the Postman Flows team, who walked me through my Flow and pointed out why things weren’t working.
The good news is that everything that tricked me up is now understood by the Postman team and has been internalized, and many of the issues have already been reported and are being fixed.
Here are just a few things that tripped me up.
Variables don’t work the way you think
- You can’t use variables in blocks that are being re-used as part of conditional loops. As such, trying to define and describe variables at the start of the flow is useless. The solution is to set variables up in a Postman Environment and assign that environment to the Flow.
Loops can’t be broken out of
- Repeat loops cannot be broken out of. I originally designed the Flow to iterate 10,000 times to allow me to send requests to the
check-otpendpoint repeatedly, assuming I could break out of the loop once I got a successful response. Nope. It will run through all 10,000 iterations every time it runs. The solution is to drop the Repeat block and do a conditional If block, connecting it back to the entry point to the sequence to force it to loop over itself.
You can’t use variables for temporary storage
- You can’t store temporary variables as part of re-entrant loops. Similar to the first point, storing variables like this can’t be recalled holding the last value stored. I had to really re-jig the connectors to make sure the iterator I was using to track the current OTP value wasn’t lost between loop iterations.
Watch your Connectors closely
- Connectors matter. It’s important to understand that when sending the output from one block into another, things won’t progress until all inputs are received. This quickly becomes cumbersome if you end up looping back to the beginning of a grouped sequence and never get to the block in question until much later. In my case, I had to add an additional If block to absorb the results from a Send Request block to ensure I could keep the OTP number in sync and output it correctly at the end of the Flow.
Anyway, this post isn’t about complaints about limitations I found in Flows but how I successfully designed an exploit with it. So, let’s go ahead and show you how I built the working exploit.
Designing our Working Exploit in Flows
For those who like TL;DR, here is what my working flow ended up looking like:
Don’t worry if it’s too hard to read. I am going to break down each section.
Set up your Postman Environment
Learning from the engineers of Flow that variables can’t be re-used in Send Request blocks required me to build a special Postman Environment, which I called Flows Env. I configured the baseURL, targetEmail, and newPass variables there.
Sending a Password Reset Request
So the first request we need to send is to the
forget-password endpoint. By right-clicking on the canvas and selecting the Send Request block, we can choose the collection we have for the crAPI service and browse to find the appropriate endpoint we want to call.
When the right endpoint is selected, Flows will render the block with the expected inputs. Because I used the same naming convention as the variables I already had for the Postman collection, they immediately mapped correctly.
Now you can wire the success of this call to the next block. I also wired the failure to a Log block so I could see any failure in the console.
TIP: As the Flow gets more “busy”, you may find it hard to trace all the connectors on the canvas. You can right-click on any block and set the color of it, which will also colorize the output connections of each block. This can be immensely helpful for debugging later. I’ll make this one blue.
Once the forget-password endpoint is successfully called, the account is in a reset mode, and a temporary OTP is assigned. Since this value can range from “0000” to “9999”, we need to track each iteration as we loop through each possible candidate.
In the first entrance to this sequence, we manually set a number input of -1 into a block variable called
otpNum. This way, when the Evaluate block does its work via FQL to increment the input by 1, the first output of this block is actually 0, which is the lower boundary of the OTP numbers we want to check.
The output of that Evaluate block then goes into an If block, where we check the upper boundary. As long as otpNum is less than 10,000, we will continue on. Otherwise, we bail out of the Flow and report we didn’t find a valid OTP.
When we continue on, we need to transform the numeric otpNum into a 4-digit string, padded with zeros at the beginning, as required. I found a neat trick when working with this Evaluate block that helped me with this.
To the right of the block is a purple icon that, when clicked, loads up the Postbot AI assistant, prompting to help generate FQL for you.
I tried this prompt:
The resulting FQL the AI generated was pretty close. It showed me FQL commands like
$string() that I didn’t know about and what arguments you can pass into them. Unfortunately, the
$pad() function actually pads at the end.
I opened a case with Postman support, but they couldn’t help me. They told me to go check for solutions in the community, but I was determined to figure this out right now. So on a whim, based on previous experience in other languages, I tried using -4 instead of 4. And sure enough… that worked to prepend the padding instead of adding it to the end.
Checking OTP and Resetting the Password
So we are almost done. All that’s left is to send the current OTP attempt to the
check-otp endpoint and see if it’s valid. If it is, we can bail out of the sequence as the password will be reset. If not, we need to return to the block where we increment
otpNum and try again.
This is where color coding blocks are helpful. If you look closely at the If block where I am checking the HTTP status code of the request, if we get a failure, we go all the way back to the first Evaluate block. Tracing that yellow line is much nicer when zoomed out to determine where it’s going. That’s a monster to track without the color coding since the line is under other blocks.
And before you say anything… yes, I reported that as a feature enhancement to consider in the future… let us move/redraw the lines to organize the connectors better so they are easier to follow.
Using our Exploit
We’re done with the design. With everything linked up, by hitting the Run button, we can watch in the console as Postman Flows goes to town trying to take over an account. After several minutes of bruteforcing, we succeed and reset the password for the victim’s account on the crAPI server.
Fun fact, even though the entire exploit only took a few minutes to run, it took far longer for the console log messages to all print out.
It’s time to package this up so I can include it in a vulnerability report.
Sh*t. You can’t export Flows
Hmm… Houston, we have a problem.
While I designed this nice visual exploit, I have no way to export, encrypt it, and then give it to someone in security triage or to a developer I may be working with.
It’s in my personal Postman workspace. And this isn’t something I can easily share externally. If you work in a team and have the professional version, you could use team roles to light up access to the Flow in a private workspace. But then, EVERYONE with Flows Editor access can see it, which is probably not what you want.
So this is a non-starter for me. If I can’t export the Flows and Flows environment so I can digitally sign and encrypt it to safeguard it from getting into the wrong hands, I really can’t use this.
As pretty as this visual exploit is with Flows, it’s more important that we can get these in the right hands of the right people safely.
Postman Flows has a lot of potential. It was fun (and frustrating) to learn a new way to design an API exploit using tools we use in the industry each day.
Is it practical for someone on the #redteam responsible for offensive security engineering? Not really. As pretty as it is, I don’t think security triage or developers working on fixing a vulnerability can genuinely benefit from looking at an exploit designed in Postman Flow.
At least, not yet.
Once improvements are made in the block layout and design, like moving connector arrows so they are easier to trace across the canvas and re-using variables so we can better describe required inputs, I think the visual side will be addressed. This will make it interesting to help both developers and non-developers visualize an attack chain for any given API vulnerability.
But until Flows can be privately exported, encrypted, and shared, it’s not practical to be able to submit to security triage or attach to the case in whatever defect tracking system the vendor uses.
I’ll stick to writing exploits in Python or shell scripts. At least for now.
I want to thank the folks over at Postman for being so open with my questions and feedback. Without their assistance, I am unsure if I would have ever gotten an exploit working in Flows.
One last thing…
The API Hacker Inner Circle continues to grow. 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 yet, join us by subscribing at https://apihacker.blog.