++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 game interface

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.

The game, showing us the flag