Jade CTF 2022 - ++game
++game is a web challenge that was part of the 2022 Jade CTF. In order to get the flag, we had to beat an impossible game.
Overview
After registering, the game greets us with our current score and a button to increase it. Each time we click the button, the score is increased by 1. To protect itself from automated requests, a valid reCAPTCHA token is required to increase the score.
The backend
As part of the challenge, we were given the source code of the backend. The most interesting part is in /api/scr/update_score.php
:
<?php
error_reporting(0);
include "/var/www/db/secret.php";
$database=array();
$database=unserialize(file_get_contents('/var/www/db/database.bin'));
if(isset($_GET["username"]) && isset($_GET["next_level"]) && isset($_GET["signature"])){
$username=$_GET["username"];
$next_level=$_GET["next_level"];
$signature=$_GET["signature"];
$concatenated=$username.$next_level.$secret;
$computed=sha1($concatenated);
if($signature===$computed){
$database[$username]['score']=$next_level;
file_put_contents('/var/www/db/database.bin', serialize($database));
http_response_code(200);
}
else{
http_response_code(404);
}
}
else{
http_response_code(404);
}
The backend uses a simple signature scheme to verify that the request is valid. The signature is computed by concatenating the username, the next level and the secret and then hashing it with SHA1. The secret is stored in /var/www/db/secret.php
and is not available to us.
The frontend
The frontend is responsible for signing our request and requesting the backend to increase our score:
if(isset($_POST['inc'])){
$recaptcha = $_POST['g-recaptcha-response'];
$secret_key = '<<REDACTED>>';
$url = 'https://www.google.com/recaptcha/api/siteverify?secret='.$secret_key.'&response='.$recaptcha;
$response = file_get_contents($url);
$response = json_decode($response);
if ($response->success===false) {
die('Captcha failed!');
}
$current=$_SESSION['score'];
$new_score=$current+1;
$concat=$username.$new_score.$secret;
$signature=sha1($concat);
$var="http://api/update_score.php?username={$username}&next_level={$new_score}&signature={$signature}";
$head=sprintf("API: %s",$var);
header($head);
$status=fetch($var);
}
Instead of making a request to the backend directly, the frontend redirects our browser to the backend. This means that we’re able to both inspect and modify any request that is sent to the backend.
Exploitation
If we were able to deduce the secret, we could compute valid signatures for any username and next level. However, the secret is not available to us. We might be tempted to try to brute force the secret from a given username, score and signature.
There is however a much easier way to win the game. Since the signature scheme involves directly concatenating the username and the next level,
the same signature will be computed for schlingel
with score 10001
and schlingel1000
with score 1
.
To obtain the winning signature, we first register the user schlingel922337203685477580
and capture the signed request to the backend:
http:103.20.235.21:9000/api/update_score.php?
username=schlingel922337203685477580
&next_level=5
&signature=46f328d22834f1d65426cabbcfefb9cb1e3a4981
We then register the innocent user schlingel
and reuse the signature from the previous request:
http://103.20.235.21:9000/api/update_score.php?
username=schlingel
&next_level=9223372036854775805
&signature=46f328d22834f1d65426cabbcfefb9cb1e3a4981
After that, we need to manually solve two reCAPTCHA challenges to increase our score to 9223372036854775807
and obtain the flag.