CTF and other games

SHA2017 CTF – web400 write-up

I created several challenges for SHA2017 CTF. One of them was the web400 ‘A View of Holland‘ challenge, a web challenge featuring an image gallery with some nice images from Holland. I always try to create challenges which give a clear idea of what you should do. The challenge may be challenging, but it shouldn’t be a needle in a haystack. There were 2 solves of this challenge, so I succeeded in the challenging part and reading the write-up of ESPR I see that they followed the intended solution till the brute-forcing of the mt_rand seed.

This post will contain the write-up of the intended solution of this challenge.

The challenge is based on the work of George Argyros and Aggelos Kiayias (I Forgot Your Password: Randomness Attacks Against PHP Applications∗), but 5 years later it is still not a good idea to use mt_rand() for important functions.

There were several flaws in the application, each of them could bring you closer to the solution.

Getting the Source
When browsing the web-site you can notice that the full screen images are shown using a PHP script. It also uses the argument file which could mean that you could use it to open files.

And indeed, you can get the source from this and other files, because there is a typo in preg_match. Instead of using the declared variable $image_dir, it is using $imagedir which is empty, so you can view the source of all files in this path. The most interesting one would be Init.php.

This file will give you some hints. First of all a description to setup your own environment for testing, secondly a hint about php_combined_lcg(), but more about this later.

Token Generation
When browsing the web-site menu, you’ll notice some options: Register, Login, Upload and something about Tokens. Apparently you can create a token which you can use to login as a user to upload on his behave. And when trying to upload, you will get a message that only the ‘admin’ user can upload. So with this information we can conclude that we need an admin token to upload a file.

So, let’s have a look at how tokens are generated. When pushing on the button to create a token, the following javascript is executed:

It does a POST call to api.php with as arguments: action=create_token and user=CURRENT_USER. If a token is created it shows a message and refreshes the current tokens displayed. So let’s see if we can create a token for the ‘admin’ user.

Apparently, we needed to be authorized to create a token. For this we can simply use the cookie from our browser session.

So, we can indeed create tokens for the admin user. But there is no way to actually get the token. So let’s dig deeper in the source and look how tokens are created.

So basically the hash is created as SHA1(some mt_rand() calls). At first sight it looks like a clever solution to prevent an attack with php_mt_seed. But this also hints that the way to solve the challenge is to get the mt_rand seed value. So the attack you should do will look like:

  1. Create token as user
  2. Create token as admin
  3. Get the mt_rand seed for the user token
  4. Calculate the next token value, which should be the admin token

Getting the mtrand seed
There are several ways to get the token. One of them is to create a lookup table of the complete 32-bit seed space, like ESPR did in their write-up. An excellent write-up btw.

Another way would be to dig even deeper into the code. In this case, the PHP-source code.

From the source of Init.php we know exactly what PHP source is used, so you can clone the source and can find how mt_rand() is seeded.

So, to generate the seed, php uses:

  • Current time in seconds
  • Process ID
  • php_combined_lcg()

If there is a way to get the current time and php_combined_lcg(), we only need to brute-force the process ID to get the seed. Let’s first look into how php_combined_lcg() is created.

The php_combined_lcg() function works with a state (s1 and s2), which we will brute-force to get the php_combined_lcg() value used in the creation of the mt_rand() seed. For this to work we will need some values from php_combined_lcg(), luckily they are used in the uniqid() function if you set the more_entropy argument to true.

The uniqid() function returns an uniqid created from the current time in seconds, the microseconds and php_combine_lcg(). Good thing the application is also using uniqid to create uniq error tokens:

We can test this by calling an invalid action on the API.

So our attack now would be:

  1. Get three uniqid values (to brute-force the LCG state) and also for current time
  2. Create an user token
  3. Create an admin token
  4. Brute force the LCG state s1 and s2, calculate the php_combined_lcg value used in the creation of mt_rand seed
  5. Brute force the process ID in the creation of mt_rand seed
  6. Check the mt_rand for our user token
  7. Calculate the next token value for the admin token

Since php_combined_lcg() function is also used in other PHP functions (like session_start()), I added the hint to Init.php.

We create a session to the web environment and will do the following:

  1. Login at the web-site (seeds the LCG state and uses LCG1 and LCG2 in session_start() )
  2. Create an error (uses LCG3 in session_start() and LCG4 in uniqid() )
  3. Create an error (uses LCG5 in session_start() and LCG6 in uniqid() )
  4. Create an error (uses LCG7 in session_start() and LCG8 in uniqid() )
  5. Create user token (uses LCG9 in session_start() and LCG10 in mt_rand seed)
  6. Create admin token (uses LCG11 in session_start())

Brute-forcing the LCG state can return multiple solutions, that’s why 3 LCG values are used to brute-force, so we can rule out false positives. In our attack we will use LCG4, LCG6 and LCG8 to brute-force the state of s1 and s2 and calculate the value of LCG10.

In this code we try all possibilities of s1, and calculate s2 from LCG4. If it finds a matching s1 state it will print out the LCG10 value which is used in the seed. Since there could be multiple states which returns the same 3 values, the script tries all possibilities.

In 1 minute time we found the LCG value used in the seed of mt_rand. With this seed, the time in seconds and our own created token, we can brute-force the PID used and find the admin token.

And running this script with the brute-forced LCG value, our created user token and the current time (also from uniqid), we can find the admin token.

This python script will do the complete attack.

And running it will give us the admin token.

Getting the flag
With the admin token we can login as the admin user and upload a file. However the file is saved under a token name, so we can’t upload a web-shell directly. Let’s have a look at the upload source code.

The uploaded file is saved in a temporary directory with a unknown random token as name. We do however have the token created for the imagefile. With that token we can do the same trick as before to get the token for the temporary directory.

This script uploads a simple web-shell which we use to get the flag from config.ini (as creator I had some inside information). Let’s do a complete run with timing:

And we found the flag: flag{7e127c06569044ec7cdf2f5384598524} in only 4 minutes of brute-forcing.

Stop using mt_rand() or rand() functions for anything important. Even if you implement a creative solution as was shown in this challenge. Although we used the values from uniqid() in this solution, ESPR just created a look-up table with all possible values for the seed, so the challenge is also solvable without knowing any LCG values. Instead use the functions random_int() and random_bytes() for some randomness. There is also an implementation for PHP 5.x.