33

I am working on the following war game from Defend The Web, which requires me to do a source code review to login as the user memtash. The code is on GitLab here.

Here is my methodology:

  1. Reset the password for user memtash which causes memtash’s password to be set to 0 (look at the password = 0 in the below snippet).

    $st = $this->db->prepare('UPDATE users SET `reset` = :reset,
    password = 0 WHERE uid = :uid LIMIT 1');
    
  2. During login, use the username memtash and find a value that the PHP sha1() function will output as 0 (or a long line of 0’s) to successfully login.

    if ($row && $row->password == sha1($pass)) {
    

How can I make the PHP function sha1() return zero, or a list of zeros? One approach is to cause the function to fail making it return false (which is 0), or find an input which when hashed outputs a string value of just zeros.

It will result in a successful login because in PHP these both evaluate to true:

0 == false

0 == 00000000000000000000000000000000

Does anyone have any suggestions on how to crash the hashing function, or where to get an input which when hashed outputs just zeros?

Even if this is not the correct solution, I think it is a good find!

1 Answer 1

67

Since this is a "learning challenge", I'll present the answer as a "guide" to finding the answer, instead of just straight up telling you what the solution is.

You were actually very close to finding the solution, to the point where you identified all the components necessary for exploitation. You seem to miss just the final step, so let's walk through it step by step.

Password Reset

When I perform a source code review or penetration test, I always look out for "weirdness". Code, that doesn't quite seem standard. In this case, what first tripped me up was the reset() function. Let's have a look at the rough structure here. I'll abbreviate it so we can see the relevant details.

// $username is fully in our control
private function reset($username) {
    $user = getUserForUsername($username)

    if($user) {
        $token = generateToken()
        $user->resetToken = $token
        $user->passwordHash = 0

        sendResetLink($user->email, $user->resetToken)
    }
}

Doesn't it seem weird, that the password is reset the moment we request a password reset, rather than once the email is confirmed? That means, as soon as we know a username (in our case memtash), we can reset that user's password hash to 0.

Login

So now, you've reached the point where you want to login. Let's look at that code now, which I will abbreviate again:

// $username and $password are fully under our control
private function login($username, $password) {
    $user = getUserForUsername($username)

    if($user && $user->passwordHash == sha1($password)) {
        win();
    }
}

This is a remarkably small function, and you are right in suspecting that the sha1() function should be manipulated somehow.

Your intuition of making it either return false or 00000000000000000000000000000000 is on the right track - painfully close, in fact. But let's have a look at the official documentation:

Return Values

Returns the sha1 hash as a string.

That means that the sha1() function will always return a hash as a string, and never false.

But what about an all-zero hash? In theory, you could find an input that hashes to all-zeroes. In practice, none have been found (or at least, made public).

So that is no good. However, there is something else we can exploit: Type Juggling

Type Juggling

Type Juggling is a bug feature in PHP, which automatically treats types as other types, supposedly to "aid" developers in doing what they "meant" to do, but most often introducing bugs, that are very hard to find unless you know where to look.

PHP has two different operators to compare for equality, == and ===. For the sake of readability, I will call the former "weak comparison" and the latter "strict comparison".

When performing a strict comparison, PHP first checks if the types of both variables are the same, before actually checking for equality. That means something like <int> === <string> will always be false, because integers are not strings, regardless of their values.

When performing weak comparison, PHP will perform Type Juggling, and bizarrely, will try to cast everything to an integer, if possible.

For some of these, this almost makes sense. For example, 0 == "0" should evaluate to true, which it does. But PHP's "string to integer" conversion rules are also very strange. For example, the string "123abc" evaluates to the integer 123, at least in PHP 5.

What does all of this mean? We need to find a string, which - when converted to an integer by PHP - would evaluate to 0.

Exponential Notation

Luckily, PHP supports exponential notation, such as 1.23e4 to represent 1.23 * 10^4 (1.23 * 10*10*10*10), or 12300. So what would 0e4 be? 0 * 10^4, which is 0.

In fact, regardless of which number we place after the exponent, it will always evaluate to 0. So "0e29823758752983759283750" would also be 0.

Now we're getting closer to the answer. We need to find a SHA-1 hash, which has the following format: 1

  • 1st Digit: 0
  • 2nd Digit: e
  • 3rd to 40th Digit: [0-9]

You have several ways of finding inputs that generate such hashes. You can either write a script to brute-force them, or you can just google for them. One such input would be 10932435112. This would actually be remarkably fast to brute force. You can see that the result of sha1("10932435112") is "0e07766915004133176347055865026311692244" - which fits perfectly.

And indeed, if you write var_dump(0 == sha1("10932435112")), the result will be true. Magical! Hence why these are called "magic hashes".

A Recap

This may have gone a bit fast, so I will try to recap each step a bit more succinctly:

First, we notice that we can reset the hash of the password to 0 when we call the reset function.

Next, we realize that the relevant code, that checks for successful login is $hash == sha1($input).

Because $hash has been set to 0, we need to find a SHA-1 hash, which evaluates to 0.

Because of the weak comparison, PHP will try to convert string inputs (such as the SHA-1 hash) to integers. Furthermore, PHP will honor "exponential notation" when evaluating integers.

We know that, in exponential notation, 0 times any number is 0. Therefore, we need to find a hash, which begins with 0e followed by only digits.

This an input, which generates such a hash, can either be brute-forced, or looked up. When looking it up, you will find that sha1("10932435112") == "0e07766915004133176347055865026311692244"

When comparing 0 == "0e07766915004133176347055865026311692244", you will find that all PHP versions evalue this to true.


1: In PHP 5, it's even easier, since any "non-digit" after the exponent is simpy ignored, so "0e1foobar..." would also evaluate to 0. But by using only digits, the exploit works in PHP 7 and beyond as well.

12
  • 3
    So I hope this little write-up was useful to you. I felt like I wasn't spoiling too much of the challenge, since you have basically already solved it and were just missing the final step. In the future, whenever you see if(0 == hash($input)), you can be virtually certain that that part is vulnerable. Feb 5, 2023 at 2:30
  • 8
    In PHP, like Perl and nearly all other computer languages back to Fortran, 10e4 is 10x(10^4) =10x(10x10x10x10) = 100,000 and 0e4 is 0x(10^4) = 0. The difference might be clearer with 2e4 which is 2x(10x10x10x10) = 20,000 not 2x2x2x2 = 16. Feb 5, 2023 at 7:15
  • 7
    I was thinking, is exponential notation even needed there? If as you said, 123abc turns into 123, something like 0abc or even just abc should compare equal to zero. On a quick test with onlinephp.io, they do just that, up to PHP 7.4. But in PHP 8.x intval("0abc") does give a zero, but 0 == "0abc" is false. So yeah, you do seem to need that trick with the exponential in recent versions.
    – ilkkachu
    Feb 5, 2023 at 13:59
  • @ilkkachu In PHP 5, you are absolutely correct. In PHP 7 and onwards, it sadly is. Feb 5, 2023 at 15:34
  • 8
    I just want to note that Exponential notation does not work mathematically like you wrote - 7e4 means 7 * 10^4. Your example of 10e4 is 10 * 10^4 = 100,000. It doesn't change the result of 0e with arbitrary digits being 0
    – user49822
    Feb 5, 2023 at 17:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .