PHP 7.4 Type Juggling0:00
So first up, I want you to notice that we're running PHP 7.4.30, because I want to show you how it used to work in PHP 7.4 before the upgrade to 8. There are many sites still running PHP 7.4, and so as developers we need to be aware of how it worked, and it helps inform our decisions going forward in apps that we maintain and apps that we work on. So in a basic sense, type juggling is what PHP does when you give it a variable and it needs it in a different form. So PHP is a loosely typed language, so when you give it, say, a 10, a value of 10, well it's going to treat that as an integer, but if it needs to use it as a float or a string, it'll convert it into what it needs to convert it into.
it's going to treat that as an integer, but if it needs to use it as a float or a string, it'll convert it into what it needs to convert it into. So a good example of that is if I do something like this, $i equals 10, whoops, then I can do $i plus $i, which is the concatenation handler in php. It's turned it into a string of 1010, so it's joined those two numbers together. Now this works when you come to comparisons as well, when you're using the double equals. So if we do 10 double equals 10, it's going to come back as true, because although we have an integer on one side and a string on the other, PHP has realized that we cannot compare a string and an integer, and so it's converted the integer into a string, and essentially we've got 10 equals 10 is happening there, even though we're passing in one of the tens.
compare a string and an integer, and so it's converted the integer into a string, and essentially we've got 10 equals 10 is happening there, even though we're passing in one of the 10s as a string. Now the fun thing to note about PHP's integer handling, and I'll give you an example of that, is if I go 10 hello, what it's going to do to turn that string, the string 10 hello into an integer, is it's going to grab as many numbers as it possibly can from the start to form the integer, and then drop the rest. So if we hop that into our comparison here, 10 hello, PHP is going to type juggle the string into an integer, and it's going to come back as true. If we can convince PHP to translate one of the values into a different form, and manipulate
Exploiting Hash Comparisons1:51
string into an integer, and it's going to come back as true. If we can convince php to translate one of the values into a different form, and manipulate it and change it during that transition, then we can get it to give a result that's unexpected, a result that shouldn't be coming out. A great example of this is if we look at, say, a password hash, or a token, or any significantly long unguessable string, let's just make a quick one, md5(time()), it's not secure, so do not use this for actual random hash generation that you're going to use for something, but for this demonstration it's fine. Now here we have that string there, and let's just save it as a variable actually. Okay so now we have a key stored in there, and this is the key we're going to use, but
Now here we have that string there, and let's just save it as a variable actually. Okay so now we have a key stored in there, and this is the key we're going to use, but what if we have an application where we want to check to see if the key is being passed in and the key matches. But we don't know the key, we want to get true out of this equation, out of this comparison, but we don't know the key, we don't know the value. Well we can use type juggling, and we can cycle through all the different numbers and go 1, 2, 3, 4, etc, so on and so forth. We can do that as many times, and go through all the different numbers until eventually we're going to get to 42, and it's going to come out as true.
We can do that as many times, and go through all the different numbers until eventually we're going to get to 42, and it's going to come out as true. Because PHP has type juggled the string, the hash that we generated, the 42cf blah blah blah blah, into an integer representation of it, which comes out as 42. So to crack this key, to bypass this key when you're trying to break into this application, all you need to do is get the right starting number. And there's a good chance that if you start from 1 and work up, you're going to hit it within a couple of hundred requests. Eventually you'll get a hash string that you're comparing that has a lot of numbers at the start, but it's still going to take less time to guess those numbers than it is
PHP 8 Behavior Changes4:58
So hopefully that's given you an idea of how type juggling worked in PHP 7.4, where PHP was manipulating the types around and basically downgrading things to the simplest possible types, which allowed quite a wide range of control over the outputs that you would get. So I'm going to swap over to PHP 8, and we're going to run a couple of these tests again, and we should see different values coming out. Okay, so I'm now in the shell, and I've got PHP 8.1.10 running in here. And so let's do some of our type juggling from before and see what we get back in return. First up we have a basic integer vs string, and we're still going to get a true. Because what PHP 8 does is that rather than translating strings to integers, it translates
First up we have a basic integer vs string, and we're still going to get a true. Because what PHP 8 does is that rather than translating strings to integers, it translates integers to strings. And so integer 10 is loosely equal to string 10, because it turns integer 10 into a string. But if I did integer 10, 10hello, we're going to get false. Because integer 10 turns into string 10, and string 10 does not equal string 10hello. So now we have a difference in PHP 8, and this saves potential attacks where you can pass in integers and numerical values that cause it to type juggle strings and drop content out of strings. Now if you remember our simple hash comparison we had before, there we go, okay, so now we
out of strings. Now if you remember our simple hash comparison we had before, there we go, okay, so now we have one that has a number at the start. And so in php 7.4 we could do this, and it would give us a true, because it would reduce down this string, and it would grab the 65 as the integer. But instead, because it's turning the 65 into a string, as we talked about before, it's now going to come out as false. This is protecting that string. We also looked at some of the things we could do with 0, and we've got 0 equals as empty string is now giving us false rather than true.
We also looked at some of the things we could do with 0, and we've got 0 equals an empty string is now giving us false rather than true. And the same with 0 and a string like that is now going to give us false rather than true. And so a lot of these manipulations are now coming out as false rather than true, because php 8 is changing those integers into strings. However, what PHP 8 didn't do was remove the boolean comparison in here. So if I go true equals 1, we're going to get true. It's going to get true on 2. If I do true equals var, we're going to get true.
It's going to get true on 2. If I do true equals $var, we're going to get true. Because PHP, when it sees the boolean in the string, is going to look for the existence of a value, and it's going to turn that value, the non-boolean, into a boolean in some way or in the way that makes sense. But something to remember is that although PHP 8 fixes the problem when you have integers and strings being compared, it doesn't fix the problem when you've got booleans and other values being compared. So that's something to remember. Hopefully you've picked up what's going on here is all of these comparisons that I've
Using Strict Comparisons7:46
So that's something to remember. Hopefully you've picked up what's going on here is all of these comparisons that I've run have all been loose comparisons, and that's because we have the == signs. But what you want to do is use a === sign. So if we do 10 == 10 here, we're going to get true, because php is turning the integer 10 into a string 10. But what if we don't want it to come out true? We can do that. And forgive my console, it's putting the ligatures in there to make the triple === like that. I've actually got three equal signs here.
And forgive my console, it's putting the ligatures in there to make the triple equals like that. I've actually got === here. So there's two there and a third one there, and that's ===, which is a strict comparison must be equal to, and that includes the types. So this is going to return false, because 10 (integer) does not match '10' (string), and it doesn't do any type juggling. And so anytime you're doing any comparisons in your application, unless you specifically want a loose comparison, you really should be using strict. And you can do it also with not equals as well, you just add a third equals on there. So I've got there, !, then !==, which is a strict not equals.
Laravel JSON Attack Demo8:46
And you can do it also with not equals as well, you just add a third = on there. So I've got there, !, then !==, which is a strict not equals comparison. Okay, so that's enough talking in a console, let's look at an actual attack. So in our demo app, we have this POST endpoint /secret, which accepts a parameter of key. And the key is looking for a string, which is a pre-shared key, basically that string's on screen. So this should be a valid request, and it should get through as expected. And what we have here, we can see the application received the string, which is when we sent.
What if we were trying to hack into the application? Well, if you think about type juggling, we could type in the five, and say this was running PHP 7.4. And we put the five in there, we would hope that we could get the the string value that is comparing it to to be downgraded to an integer in order to match out five. So I'm going to send the request. But the problem I've got here is that because it's a formula encoded request, it's coming through as a string in the key value, which means that we can't use our type juggling on this value, because it's coming through as a string. So it's going to do a string comparison, the string of five does not equal the string of
on this value, because it's coming through as a string. So it's going to do a string comparison, the string of five does not equal the string of five f4d, etc. But luckily for us, Laravel makes this really, really easy, because Laravel accepts JSON post payloads, as well as form URL encoded payloads, and it will accept any sort of payload that it understands on all the requests, which means we don't have to do anything in the application, all we have to do is change the request we're sending to the application, and the application will do what we want. So I can go up in here and get rid of this and then go JSON. And then I'm going to turn this into a JSON payload.
So I can go up in here and get rid of this and then go JSON. And then I'm going to turn this into a JSON payload. So what we're doing there is we're sending a JSON payload with the key of key and the value of integer 5, because JSON is aware of the type of its values. And so now I'm going to send that to the server. So what we can see here is that the request has gone through and has received the key as an integer of 5. Now it still failed because the integer of 5 in PHP 8.1 does not equal the string five f4dc, etc. But we know from our previous tests that if this was running PHP 7.4 earlier, then it
five f4dc, etc. But we know from our previous tests that if this was running PHP 7.4 earlier, then it would have type juggled the hash down from five f4 blah blah blah down to five, and then it would have done an integer comparison of five to five and it would have let us in. So this would work on an application running PHP 7.4 and below, because it is vulnerable to type juggling on integers, but not on this version of PHP, this is running PHP 8.1. Because it's JSON, and we can pass strings, we can pass integers, we can also pass booleans as well. And so rather than passing in five, what I can do here is pass in true, like so. Now if I run the request now, what we can see here is that it received the boolean value
And so rather than passing in five, what I can do here is pass in true, like so. Now if I run the request now, what we can see here is that it received the boolean value of true, and then it passed because true is loosely equal to some value, some value that is there. And so by passing in true as the key to our application, we've completely bypassed the security, bypassed the check of this key, and then we're straight into what we want to get into. That is type juggling that works now, that works on PHP applications running on the latest version of PHP. And because of the way that Laravel accepts values as JSON payloads, you can use this
Fixing with hash_equals12:58
So what do we do about this? How do we solve this? Let's jump over to the code. Okay, so what we have here is the code that's actually doing the comparison. And as we would expect from what we were talking about before, the reason why this worked, why we're getting true, why we're getting a Boolean true is bypassing the key, is because we have a loose comparison in here. So all we have to do is add in the third equals. And then if I run it again, suddenly we have true is not exactly equal to that. And so this is what's called a strict comparison in PHP.
And then if I run it again, suddenly we have true is not exactly equal to that. And so this is what's called a strict comparison in php. And my recommendation is that any time you're doing any comparisons in your application, unless you have a specific need to type juggle, always use a strict comparison. Always go for the triple equals or the does not equals if the two equals are true. There are some cases where you do want a loose comparison, say when you're doing a Boolean check on something, or you know that it could be an integer or string and it's fine to be translated through, or maybe you're going from integer to a float. There's a bunch of different reasons why you might want a loose comparison, but you'll know about it when you're writing that comparison, you'll know it needs to be loose, or it'll
course of multiple hundreds, possibly thousands of requests, you can slowly build up the key and guess the key based on the timing, if you can measure it accurately enough, which is ridiculously scary. No, I'm just going to leave it there. It's ridiculously scary. And it's a level of timing and a level of attack that you often don't think about. But luckily for us, php has our backs and there is a really easy way around it. So rather than doing a strict comparison when you're comparing keys like this, when you're comparing strings that have to match and there is some security involved, instead we can use hash_equals, what it's called.
comparing strings that have to match and there is some security involved, instead we can use hash_equals, what it's called. And what hash_equals does is it provides a safe, a timing safe way to compare strings. So it takes the two strings and it compares them to make sure they exactly match. And regardless of if they match or not, it will always take the exact same amount of time. It will always compare them in a safe way, which means that you cannot measure even the minute differences in timing between the two strings as you add and remove things and as you guess different characters, because it's always going to take the same amount of time. So anytime you need to compare a key like this, in this sort of application where you
you guess different characters, because it's always going to take the same amount of time. So anytime you need to compare a key like this, in this sort of application where you have a security implication of what you're comparing, go for the hash_equals method. Put that in there. And it's going to give you the security you need. And there's no real downside because there's no dependencies, it's a default part of php. All you need is that method, put it in your comparison like that, and you're done.
