42

I've been practicing in security-related topics and I came upon this problem which I don't understand at all. You receive a form with one input named pass, and this is the code you need to bypass:

<?php
error_reporting(0);
session_save_path('/home/mawekl/sessions/');
session_start();
echo '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>';
echo 'This task is as hard to beat as the castle which you can see on the bottom.<br>';
echo '<pre>';
include('./ascii.txt');
echo '</pre><br>';

$_SESSION['admin_level']=1;

if (!isset($_POST['pass']) || md5($_POST['pass'])!='castle')
{
    echo '<b>Wrong or empty password.</b><br>';
    $_SESSION['admin_level']=0;
}

If it enters the final if statement, you lose (Need to make it so $_SESSION['admin_level'] stays at 1).

Any help is appreciated, thanks!

Clarification:

I can't edit the code I posted. It's a challenge. All I can do is send a password through an input whose name is "pass". Yes, I know md5 is supposed to return a 32-char long string. That's the challenge.

Bakuriu
  • 429
  • 4
  • 9
Tom
  • 880
  • 1
  • 7
  • 14
  • Comments are not for extended discussion; this conversation has been [moved to chat](http://chat.stackexchange.com/rooms/41095/discussion-on-question-by-tom-exploiting-md5-vulnerability-in-this-php-form). – Rory Alsop Jun 13 '16 at 09:17

7 Answers7

59

Try sending a HEAD request.

I'm assuming that with ascii.txt included, the output of the script is just over a nice number like 4096 bytes, a common output_buffering value. Once the script has written output_buffering bytes, it needs to flush the output buffer before continuing.

Normally this works fine and the script continues, but if the request type is HEAD, there's nowhere for the output to go and execution stops, hopefully in the middle of the "wrong password" message which means admin_level is never set back to 0.

If ascii.txt is a file you can control, you'll have to tweak the size so the numbers work out to exceed output_buffering while writing the "wrong password" message.

Here's a paper on this technique. It might not always be applicable based on the PHP version/config, but the last example in the paper is so similar to this problem that I'd expect it to be the intended solution.

gabedwrds
  • 665
  • 5
  • 6
  • @gabedwrds This was the most close answer I can see. But i tried it, not able to exploit. May be i am missing something .Using `PHP 5.3.10`. – Sravan Jun 12 '16 at 05:54
  • 1
    @baconface The idea is that with output buffering enabled, the first echo lines and ascii.txt get buffered and the first admin_level line runs normally. The "wrong password" message fills up the rest of the buffer, triggers a flush, and stops execution before the second admin_level line. – gabedwrds Jun 12 '16 at 05:57
  • @gabedwrds My bad. I misread parts of this answer. Up voted. – Bacon Brad Jun 12 '16 at 06:47
  • @Tom Just curious of the PHP version exploited there? – Sravan Jun 12 '16 at 08:24
  • 2
    @Sravan It's 5.5.9 according to the response header – Tom Jun 12 '16 at 08:45
27

The security flaw isn't in MD5 in this case. And the idea isn't to get if (!isset($_POST['pass']) || md5($_POST['pass'])!='castle') to evaluate in favor of the hacker. The vulnerability appears when you crash the script. The admin privileges are not conditional. No matter what the user gets their permission elevated by setting 1 to $_SESSION['admin_level']. PHP is procedural. So if you can error out the following IF statement the permission is not only set but it remains persistent. This is now available to the rest of the program as it is stored in the session. Even if the script crashes and errors out. So if the IF statement only exists during a login the attacker can visit all the other restricted scripts as an admin_level set to 1.

So timeline of events:

  • Attacker puts bad data or an overflow of data in the POST request
  • Script runs
  • Script gives admin_level of 1 which is stored in the session
  • Script bombs at evaluation
  • Attacker goes to another page/script that looks to see if $_SESSION['admin_level'] is 1

This could of been avoided if the script made the session value part of the IF statement:

if (!isset($_POST['pass']) || md5($_POST['pass'])!='castle')
{
    echo '<b>Wrong or empty password.</b><br>';
    $_SESSION['admin_level']=0;
} else {
    $_SESSION['admin_level']=1;
}

However there is still a lot more that should be done to secure this code.

Bacon Brad
  • 3,340
  • 19
  • 26
  • 2
    Always assume that a user has *no* rights until and unless they prove otherwise by correctly authenticating as a privileged user. The Bill of Rights does not apply to software access control. – Shadur Jun 13 '16 at 11:21
17

I've seen a similar challenge somewhere. Try to send something like 'QNKCDZO' as input. md5('QNKCDZO') is '0e830400451993494058024219903391' (note the 0e prefix in the hash indicating scientific notation of a number) and since the code uses the != operator (instead of the type sensitive !==) PHP (older versions only?) will actually cast it to a number before comparing. Obviously, both will then evaluate to 0 and you win.

Edit: ruakh is right. This doesn't quite work, because both the hash and the string 'castle' would need to look like a number to PHP for the cast to happen.

Younis Bensalah
  • 271
  • 1
  • 5
  • 4
    Hmm, I can't get this to work. On the version of PHP I have handy, something like `'0e3' == '0e4'` is true (because *both* strings are deemed numeric, so get converted to numbers), and something like `0e3 == 'castle'` is true (because the left-hand side is actually a number (not just a numeric string), so the right-hand side gets juggled into being a number as well), but something like `'0e3' == 'castle'` is false (because both sides are strings, and one side isn't a numeric string, so a string comparison happens). – ruakh Jun 12 '16 at 02:39
  • 16
    (The whole thing is still a pretty good argument against ever using PHP for anything ever at all, though.) – ruakh Jun 12 '16 at 02:40
  • 1
    Wait what? In php three equals four? – Adam Martin Jun 12 '16 at 09:59
  • 11
    @AdamMartin No, `0 times 10 to the power of 3` equals `0 times 10 to the power of 4`. `0e3` and `0e4`, as scientific notation, are both redundant ways of writing `0`. The problem comes because PHP can coerce *both sides* of the `==` operator to int, rather than just coercing one to match the other. – IMSoP Jun 12 '16 at 16:07
9

I haven't been able to test in PHP 5.5.9, and what I'm going to propose doesn't work on my PHP 5.6.22 (but I might have made some silly mistake).

The only workaround I can fathom is if MD5 returns something that is treated as a number. I've Googled and found something which seems germane.

LSerni
  • 22,521
  • 4
  • 51
  • 60
  • 1
    Thanks for your help. Having further researched the subject, I don't believe this will work sadly. It will work when both sides are treated as a number, but "castle" doesn't start with "0e" sadly :( – Tom Jun 12 '16 at 07:57
4

I see that you accepted gabedwrds solution (which is excellent, by the way), but I wanted to post one potential solution that I have used in the past when doing some interesting pentesting practice. (I am by no means an expert in that field, but I am sometimes a hobbiest.)

If you will notice, the script never sets any policy for handling a user abort. By default (unless configured otherwise in the PHP ini), if a user aborts their connection while a page is loading, the PHP script exits immediately and stops all processing. Because of this, if you time your connection just right, you could abort your connection between the $_SESSION['admin_level']=1; and the evaluation of the conditional, thus forcing the server to exit the script immediately, without finish the conditional evaluation that would cause admin_level to be reset to zero.

Of course, this is entirely dependent on the PHP configuration not having ignore_user_abort=1. So if that is set to continue even if a user aborts, the privileges will still be reset.

To increase your window of opportunity to use this exploit, you could increase the size of your pass payload/variable data. md5 has to hash in chunks, so by increasing the size of the pass data, you will cause the md5 function to spend more time computing the hash, thus giving a large gap of time that you have to abort the connection.

Spencer D
  • 770
  • 1
  • 5
  • 13
  • Have assumed that PHP would not flush the session variables back to the session storage (whatever mechanism is being used, e.g. file or database) if the script aborted. Does it really do it anyway? – Jules Jun 13 '16 at 21:42
  • That sounds like it requires more luck than skill, and I'm definitely not on the lucky side of things :) – Tom Jun 14 '16 at 06:17
  • 1
    @Tom haha Well, like I said, I do think gabedwrds' answer is a better solution, but exploiting similar time-sensitive windows is something I have had fun playing around with in the past, and I consider it to be a rather interesting exploit, so i felt it was worth sharing ;) However, I most definitely agree that this is probably not the intended solution to your challenge. – Spencer D Jun 14 '16 at 18:21
2

Similar to what others have said, PHP has a max memory size. If you put in POST data that was JUST big enough to use ALMOST all of PHP's memory up, then it could conceivably crash when it gets exactly to this line if it ran out of memory:

echo 'Wrong or empty password.'

This is built on three principles:

1) When sessions are enabled, PHP doesn't flush the output, instead storing it all in RAM until the page is done rendering.

2) PHP has a maximum RAM size allocated for each requests and, when reached, crashes your script.

3) When new lines are written, it uses more RAM, using up your remaining RAM.

Given the above facts, if you put more HTTP POST data in your HTTP request (ex: $_POST["data"]="largestring") then you could conceivably trigger an out of memory error before $_SESSION["admin_level"]=0 is run.

In a real-life scenario, this is highly unlikely because PHP also has a maximum POST body size, maximum header size, and whatever server container is hosting it (nginx, apache, etc) typically also has max limits. You would probably hit all of these max limits, but if configured poorly, its possible you could get $_SESSION["admin_level"]=1 effectively becoming an admin when going to any other pages on the website using this method.

Update: Another similar method would be to put a big password so that the md5() function fails while processing it or something, again causing it to crash because it runs out of memory. Depends on how md5() internally works. Its probably making a COPY of the $_POST["pass"] field, so if $_POST["pass"] was gigantic, then it could be very easy to cause this copy to be what uses up all the RAM and makes it crash.

2

Assuming you can modify the ./ascii.txt, I would put this text inside:

<?php

override_function('md5', '$a', 'return "castle";');

And that should work.