Challenge Description
An organization seems to possess knowledge of the true nature of pumpkins. Can you find out what they honestly know and uncover this centuries-long secret once and for all?
In this challenge, we are presented with a web app, that presents us with scary and non-scary facts about pumpkins. We are also given the source code for the challenge making it a white box challenge.
By extracting the source code given and by running the docker within it we are presented with the following web app:

By interacting with the web application and looking at the network being used on the backend, we can have a deeper understanding of the inner workings of the challenge.
The web application is simple, we can select see Spooky, Not So Spooky, or Secret Facts, if we select one we are presented with facts about that specific category. But in the secret facts category we are presented with the following:

If we take a look at the requests being made we can see that the application is interacting with the server backend by making a POST request to an API:

The payload used on the POST request is the following:
{
"type":"secrets"
}
By taking a look at the source code given we can see how the data passed through the POST request is handled. In the source code below, we can see that first if we pass the value secrets
as the type in our payload and if the origin of our request isn’t from localhost we are blocked from accessing the secret facts:
if ($jsondata['type'] === 'secrets' && $_SERVER['REMOTE_ADDR'] !== '127.0.0.1')
{
return $router->jsonify(['message' => 'Currently this type can be only accessed through localhost!']);
}
If we pass the previous check the server will then check in what category we are interested in bypassing our type through a switch case:
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!'
]);
}
One problem with this approach is that although the comparison made on the first check to see what category and IP we sent is made with strict comparisons (=== or !==) the comparisons used on the switch case are loose comparisons (== or !=) as seen in the documentation:
switch (PHP 4, PHP 5, PHP 7, PHP 8) The switch statement is similar to a series of IF statements on the same expression. In many occasions, you may want to compare the same variable (or expression) with many different values, and execute a different piece of code depending on which value it equals to. This is exactly what the switch statement is for.
Note: Note that switch/case does loose comparison.
This lack of strict comparison leads to the check being vulnerable to type juggling attacks.
Type Juggling (also known as Type Confusion) vulnerabilities are a class of vulnerability wherein an object is initialized or accessed as the incorrect type, allowing an attacker to potentially bypass authentication or undermine the type safety of an application, possibly leading to arbitrary code execution.
In this specific case, the version of PHP being used is crucial because of changes made to the way PHP compares values before and after PHP8.
By taking a look at the docker file provided with the source code we can see each version we are dealing with:
# Install PHP dependencies
RUN apk add --no-cache --update php8 php8-fpm php8-mysqli php8-json
As we can see the version being used is PHP 8. Now if we look at the comparison table provided by the phpandmysql documentation we can see what values would produce the result we pretend:
true | false | 1 | 0 | -1 | ‘1’ | ‘0’ | ‘-1’ | null | array [] | ‘string’ | '' | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
true | true | false | true | false | true | true | false | true | false | false | true | false |
false | false | true | false | true | false | false | true | false | true | true | false | true |
1 | true | false | true | false | false | true | false | false | false | false | false | false |
0 | false | true | false | true | false | false | true | false | true | false | false | false |
-1 | true | false | false | false | true | false | false | true | false | false | false | false |
‘1’ | true | false | true | false | false | true | false | false | false | false | false | false |
‘0’ | false | true | false | true | false | false | true | false | false | false | false | false |
‘-1’ | true | false | false | false | true | false | false | true | false | false | false | false |
null | false | true | false | true | false | false | false | false | true | true | false | true |
array [] | false | true | false | false | false | false | false | false | true | true | false | false |
‘string’ | true | false | false | false | false | false | false | false | false | false | true | false |
'' | false | true | false | false | false | false | false | false | true | false | false | true |
As we can see if we are able to send the value true
as the type, this will enable us to bypass the first check while still enabling us to enter the first switch case because in this case, the comparison would be "secret" == true
which is true, as seen in the previous table.
To retrieve the flag, we just need to make a POST request with the crafted payload:
$ curl --silent -d '{"type":true}' -H "Content-Type: application/json" -X POST http://161.35.33.243:32223/api/getfacts | jq '.facts[0].fact'
"HTB{sw1tch_stat3m3nts_4r3_vuln3r4bl3!!!}"