247CTF Web CTF Writeups
7 min read

247CTF Web CTF Writeups

247CTF is an amazing platform that provides CTF challenges that are available 24/7, with categories ranging from web, to binary exploitation, and from networking to cryptography.

It’s personally one of my favourite platforms, and it is extremely entertaining / educational. In this post I will be going through three of the easier web challenges with in depth explanations of how each one is exploited / why it is possible.

I am hoping this brief post will help get more players involved with the platform, since I think it is massively underrated, and people just need to take that initial step into the world of CTFs.


This challenge was fairly simple, and was mainly to introduce users to how basic flask applications work, but also makes the player look into how the flask cookies are encoded and stored.

We start off with the following source code (I have commented it to help make it more understandable):

import os # Import the OS module
from flask import Flask, request, session # Import the required parts of the flask module
from flag import flag # Import the custom module used to stored the flag, so that user's can't read the flag from the source code

app = Flask(__name__) # Define "app" as a flask app
app.config['SECRET_KEY'] = os.urandom(24) # Make the session secret key extremely hard to guess / crack*

def secret_key_to_int(s): # Define the secret_key_to_int function and expect one parameter
        secret_key = int(s) # Try to convert the "s" parameter to an integer
    except ValueError:
        secret_key = 0 # If error is hit then set the secret key to 0
    return secret_key # Return the secret key

@app.route("/flag") # Define what to do when /flag is accessed
def index():
    secret_key = secret_key_to_int(request.args['secret_key']) if 'secret_key' in request.args else None
    session['flag'] = flag # Set the flag in the session cookie
    if secret_key == app.config['SECRET_KEY']: # If you can guess the secret key then return the flag
      return session['flag']
      return "Incorrect secret key!" # Return string if secret key is wrong

@app.route('/') # Define what to do when the / route is accessed
def source():
    return """


""" % open(__file__).read() # Return this source code

if __name__ == "__main__":
    app.run() # Run the app

It may seem complex, but this is where you need to take absolutely everything into account, and study what each part does. about 98% of this code is just a red herring.

It starts the app in the app.run() bit, this will basically just run the webapp and wait for our request to arrive. Nothing much happens when we hit "/", except the source code is displayed to us, so we can move on from that bit, and study the other endpoint (/flag).

def index():
    secret_key = secret_key_to_int(request.args['secret_key']) if 'secret_key' in request.args else None
    session['flag'] = flag
    if secret_key == app.config['SECRET_KEY']:
      return session['flag']
      return "Incorrect secret key!"

So when this is accessed, set the secret_key to be the secret key converted to an int with the custom function if the request argument is set, else do nothing. This is part of the red herring, we can ignore it and move past to the next line.

The flag we imported at the start of the script is then set into our session cookie, in the name pair called "flag". This is the important part.

We know that session cookies are set in our local browser, meaning that we now have the encoded version of the flag stored in our local browser (the value of the flag was put in our session cookie here: session['flag'] = flag).

Let's look in the dev tools and see if we can find it (to access dev tools simply press F12, or access the web console from the small menu in the top left).

Bingo, in the tab called "storage" we have our cookies for this webpage (make sure you hit the "/flag" endpoint before checking for the cookies, since you need to trigger the flag being stored in the cookie).

The specific cookie we are interested in is called "session", and it seems to have the distinguishable base64 encoded {" start (eyJ). You don't need to know this, it is just something I recognised after seeing a lot of b64 encoded json payloads in other CTFs.

Since we recognised that all the characters in the cookie seem to range from a-z,A-Z,0-9 alongside "+" and "/", with a possibility of "=" at the end for padding (this isn't always the case), we know that it is indeed base64.

We can simply copy and paste this session cookie into a base64 decoder online (https://www.base64decode.org/), or we can use the base64 binary on our linux machine:

chiv@Dungeon:~$ echo "eyJmbGFnIjp7IiBiIjoiTWpRM1ExUkdlMlJoT0RBM09UVm1PR0UxWTJGaU1tVXdNemRrTnpNNE5UZ3dOMkk1WVRreGZRPT0ifX0.XlRvbg.sBNAS_F_IUh0zIXDE5eiZf6q9Sw" | base64 -d
{"flag":{" b":"WW91IGdvdHRhIHRyeSB0aGUgY2hhbGxlbmdlIHlvdXJzZWxmIDpQ"}}base64: invalid input

For clarification, the reason the binary couldn't interpret the part after the "." in the cookie is that the part following the "." is the signature, which is not base64 encoded (hence the "_", underscores are not used in B64 encoded strings).

Great, so we can see the name pair value for the flag, and the flag seems to be encoded once again, in the same format as the first string, leading me to believe it is base64.

We just need to base64 decode the inner string, and we obtain the flag.


Right, a PHP loose comparison challenge. Let's get to it I guess. We start off with the following source code, extremely simple (commented to make it more understandable):

  require_once('flag.php'); # Import the flag value
  $password_hash = "0e902564435691274142490923013038"; # Set the hash of the password we will compare our own to
  $salt = "f789bbc328a3d1a3"; # Set the salt for the inputted password (this will be appended to our password and then md5 hashed)
  if(isset($_GET['password']) && md5($salt . $_GET['password']) == $password_hash){ # Compare the md5 of the salt string + our inputted password to the password hash stored at "$password_hash"
    echo $flag; # If true is returned the give the flag
  echo highlight_file(__FILE__, true); # Echo the source

The whole vulnerability for this challenge comes into play when there is a loose comparison (==) in use over a strict comparison (===).

When a loose comparison is used (==), only the value is checked, whereas, with a strict comparison (===), both the type and the value are checked, so if a string and an integer are compared, it will return false (bare in mind an empty response means False):

php > echo "22" == 22;
php > echo "22" === 22;
php >

Cool, so we know how it is vulnerable, now let's take a look at that hash: 0e902564435691274142490923013038. It definitely doesn't look like a normal hash, the only letter in the whole md5 is the "e" near the start.

After some research on PHP type juggling, we come across an amazing pdf by OWASP that happens to talk about our exact case (see references). Specifically this part:

TRUE: "0e12345" == "0e54321"
TRUE: "0e12345" <= "1"
TRUE: "0e12345" == "0"
TRUE: "0xF"     == "15"

Cool, so we can see from the first example that anything that starts with "0e", followed strictly by only digits will return True. So we need to find a string, that when combined with the salt, and md5 hashed, it returns "0e" + 30 digits.

For this I made a very basic python script that runs through all combinations of length 1-10 until it finds a string that fits our requirements.

from itertools import product
import hashlib # Import the modules
for x in range(0, 10): # Iterate through the lengths
        for combo in product("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", repeat=x): # For each combination in this alphabet with this length
                result = hashlib.md5("f789bbc328a3d1a3" + ''.join(combo)).hexdigest() # Generate the result
                if result.startswith("0e") and result[2:].isdigit(): # Check if result matches our requirements
                        print "f789bbc328a3d1a3" + ''.join(combo) # If it does print out the string
                        pass #If not pass

Which outputs:

chiv@Dungeon:~$ python hash.py
f789bbc328a3d1a3abr1R hashes to: 0e918704785736777877278345014851

Cool, so it seems to have found a valid string f789bbc328a3d1a3abr1R, this is our salt (f789bbc328a3d1a3) appended to our randomly generated string (abr1R), and then md5 hashed.

Let's try it on the website. Nice, by removing the salt from our input, we can pass the password GET parameter and obtain the flag.


Finally, we have another PHP challenge, this concept is very simple, but at the same time quite interesting.

We start of with the following source code (commented by myself to help make it more understandable):

  $fp = fopen("/tmp/flag.txt", "r"); # Open the flag file and read it's contents
  if($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['include']) && strlen($_GET['include']) <= 10) { # If the request method is GET and the include GET parameter is set and the length of the value of said GET param is under or equal to 10 then include the file
  fclose($fp); # Close the flag.txt file
  echo highlight_file(__FILE__, true); # Echo this sourcecode

For this challenge it is fairly obvious what we need to do, find a file path that we can include which takes up less than 11 characters, which holds the value of the flag.txt file.

One of my favourite things about Linux is the idea that "everything is a file". I won't go into too much detail in this, but the name explains pretty well what is meant by this, in Linux, everything is stored as a file somehow. For example /proc is sometimes referred to as a process information pseudo-file system. It doesn't contain 'real' files but runtime system information.

Knowing this, and knowing that the contents of flag.txt are stored in a file somewhere on the server, we can look into where abouts the contents are stored in the raw linux filesystem.

I came accross this PHP bug while looking into how the file descriptors work: https://bugs.php.net/bug.php?id=53465

In the report, they seem to try and access the contents of the file via /dev/fd/<descriptor>.

If we can figure out the descriptor, then we can get the contents of the flag, we can check the length of the path to see how far we can test:

chiv@Dungeon:~$ echo -ne "/dev/fd/1" | wc -c
chiv@Dungeon:~$ echo -ne "/dev/fd/99" | wc -c

Nice so we can test up to descriptor 99, a simple bash for i in $(seq 0 99) loop combined with curl should do the trick.

for i in $(seq 0 99); do echo; echo "Testing fd $i"; curl -s https://CUSTOMSUBDOMAIN.247ctf.com/?include=/dev/fd/$i | grep 247; done

Bingo, we get fd 10 holds the contents of the flag.


I hope this clarified some concepts regarding Linux file systems, PHP type juggling and flask web applications. If anyone has any questions feel free to contact me at https://twitter.com/SecGus.