Fetch the Flag CTF 2022 writeup: Juggalo Central
November 10, 2022
0 mins readThanks for playing Fetch with us! Congrats to the thousands of players who joined us forFetch the Flag CTF. And a huge thanks to the Snykers that built, tested, and wrote up the challenges!
If you were at Snyk’s 2022 Fetch the Flag and weren’t able to finish the Juggalo Central challenge, you’ve come to the right place. Let’s walk through the solution together!
Walkthrough
First off, there is a link to a website that contains the usual login form with two fields username and password. There’s also a file with .php
extension. Let's take a look at the website first.
The website
As mentioned before, the page contains two input fields (and an image of a clown). I first checked the page source, .css file, and even the image. Nothing that I can make use of. One thing I noticed was that there were two different error messages: one when I use a standard username like “admin”, and when I use some random string like “qweasdzxc”.
The code
Inside of index.php
, I saw three functions: getFlag
, searchUserByName
, and validateCredentails
.
The first function, getFlag
, is making a call to a database table called flag
and returns the very first row found. I did some math and came to the conclusion that the key I am looking for is stored in this table.
The second function, searchUserByName
, is trying to find a user based on the input variable called $username
. If there are any results, the function is going to return them. One thing I’ve noticed here is the line:
1 return $stmt->fetchAll(PDO::FETCH_ASSOC);
That means that not only one row would be returned by this function (as I expect users to have unique usernames), but if there is more than one user with the same username, it will return all of them. Later, I found out that it’s not useful for us because of what is happening in the next function. Sometimes that happens in a CTF.
The third function, validateCredentails
, is reading the username and password provided in the body of the POST
request and calling searchUserByName
to see if is there a user with that username. Here, I also saw why the error message was different for a username with random characters. If the user provided a non-existing username, the return value of the function is: "Juggalo not found!". While for any existing username with an incorrect password, the error message was: "Invalid credalo".
Getting a valid username
Now we are going to find some valid credentials. When I tried something as usual as “admin”, I saw "Invalid credalo" error message. Now with code analysis from the previous step, I can tell that there is a user with a username field value of “admin”.
The first guess
When we do have a valid username, let’s see how to hack the password. The error message is stating that we don’t have access to this user is happening because of the following line of code:
1 if ($results[0]['password'] != substr(md5($username . $password), 0, 20)) {
On the left side of the condition, a user’s password is retrieved from the database. On the right side of the condition are the first 20 characters of the md5 hash containing two variables provided by the user: username
and password
.
My first thought was that because of this condition, any valid password is a 20 characters string. Long story short, it was wrong.
The second guess
After thinking about the challenge for a while, another idea came to my mind. This line is using the !=
comparison operator. So I headed to the official PHP documentation to check how comparison operators work in PHP. One line in the documentation that I couldn’t understand was right next to the !=
operator, saying:
1true if $a is not equal to $b after type juggling.
Wait. What is “type juggling”? After reading that, I opened the challenge web page again. That clown was still there, watching me.
The hack
Turns out, there is a thing in PHP called “type juggling”. After a read on this topic and running some PHP tests online, I came up with the following condition
1 if ("0" == "0e12345")
This, surprisingly, returned “true”. This happens because PHP converts both operands to numeric values. Now in order to find the right password for the “admin” user we just need to generate an md5 hash that meets two conditions: starts from “0e” and contains only numbers after that (or at least 18 characters after, because of substr
usage).
The script
Here is a Python script that can find the right hash:
1import hashlib
2
3target = '0e'
4candidate = 0;
5while True:
6 plaintext = 'admin'+target+str(candidate)
7 hash = hashlib.md5(plaintext.encode('ascii')).hexdigest()
8 # Hash starts with “0e”
9 if hash[:2] == target:
10 # Hash contains only one letter (“e”) in first twenty characters
11 # So it can be considered as a number by PHP
12 if sum(c.isalpha() for c in hash[:20]) == 1:
13 print('username and password:' + plaintext);
14 break
15 candidate = candidate + 1
After a minute I was able to log into the Juggalo Central web page with credentials from this script.
More Fetch the Flag solutions
Want to learn how we found all the other flags? Check out our Fetch the Flag solutions page to see how we did it.