Sunday, March 3, 2013

Unauthorized Access: Bypassing PHP strcmp()


While playing Codegate CTF 2013 this weekend, I had the opportunity to complete Web 200 which was very interesting. So, let get our hands dirty.

The main page asks you to provide a valid One-Time-Password in order to log in:



A valid password can be provided by selecting the "OTP issue" option, we can see the source code (provided during the challenge) below:


        include("./otp_util.php");
     
        echo "your ID : ".$_SERVER["REMOTE_ADDR"]."";
        echo "your password : " .make_otp($_SERVER["REMOTE_ADDR"])."";
     
        $time = 20 - (time() - ((int)(time()/20))*20);
        echo "you can login with this password for $time secs.";


A temporary password is calculated based on my external IP (208.54.39.160) which will last 20 seconds or less, below the result:


So, then I clicked on "Login" option (see first image above) and below POST data was sent:

id=208.54.39.160&ps=69b9a663b7cafaca2d96c6d1baf653832f9d929b

Which gave me access to the web site (line 6 in the code below):

But we cannot reach line 9 (see code below) in order to get the flag since the IP in the "id" parameter was different. Let's analyze the script that handles the Login Form (login_ok.php):


1. $flag = file_get_contents($flag_file);
        
2. if (isset($_POST["id"]) && isset($_POST["ps"])) {
3.    $password = make_otp($_POST["id"]);
4.    sleep(3); // do not bruteforce
5.    if (strcmp($password, $_POST["ps"]) == 0) {
6.       echo "welcome, ".$_POST["id"]
8.       if ($_POST["id"] == "127.0.0.1") {
9.           echo "Flag:".$flag
          }       
      } else {
                        echo "alert('login failed..')";
      }       
   }    


Test case 1: Spoofing Client IP Address:

So, the first thing that came to my mind in order to get the flag (line 9) was to send "127.0.0.1" in the "id" parameter, so, let's analyze the function make_otp() which calculates the password:


       $flag_file = "flag.txt";

        function make_otp($user) {
                // acccess for 20secs.
                $time = (int)(time()/20);
                $seed = md5(file_get_contents($flag_file)).md5($_SERVER['HTTP_USER_AGENT']);
                $password = sha1($time.$user.$seed);
                return $password;
        }

As we can see in the code above, the function make_otp receives the "id" parameter in the $user variable and is used to calculate the password, so, by following this approach, we will not be able to pass line 5 since we need a password  for  the IP 127.0.0.1, and we can only request passwords based on our external IP via "OTP Issue" option as explained above, so, how can we get one? What if we try to find a vulnerability in the code related to "OTP Issue" option? 

So, since "OTP Issue" is reading the IP based on the environment variable "REMOTE_ADDR"  we could try to spoof our external IP address as if we were connecting from 127.0.0.1, but unfortunately it is not a good option, although spoofing could be possible, it is only an one way communication so we would not get a response from the Server, so at this point, we need to discard this approach.

Test case 2: Bruteforcing the password

By looking at the make_otp() function shown above, the only data we do not know in the password calculation process, is the content of $flag_file (obviously), so, assuming that the content of that file is less than 4-5  characters and therefore have a chance to bruteforce the MD5 hash, we only would have 20 seconds to guess it, and due to the sleep(3) command (see line 4 above), we could only guess 6 passwords before the password expires and therefore we definitely drop bruteforcing approach off the table.

Test case 3: Bypassing strcmp() function

After analyzing the two cases described above I started "googling" for "strcmp php vulnerabilities" but did not find anything, then, by looking at PHP documentation and realized this function has only three possible return values:

int strcmp ( string $str1 , string $str2 )
Returns
< 0 if str1 is less than str2; > 0 if str1 is greater than str2, and 0 if they are equal.


Obviously, we need to find a way to force strcmp to return 0 and be able to bypass line 5 (see above) without even knowing the password, so, I started wondering what would be the return value if there is an error during the comparison? So, I prepare a quick test comparing  str1 with an Array (or an Object) instead of another string:


$fields = array(
    'id' => '127.0.0.1',
    'ps' => 'bar'
);
$a="danux";
 if (strcmp($a,$fields) == 0){
        echo " This is zero!!";
 }
 else{
       echo "This is not zero";
}


And got below warning from PHP:

PHP Warning:  strcmp() expects parameter 2 to be string, array given in ...

But guess what?Voila! it also returns the string "This is zero!!" In other words, it returns 0 as if both values were equal.

So, the last but not least step is to send an Array in the "ps" POST parameter so that we can bypass line 5, after some research and help from my friend Joe B. I learned I can send an array this way:

id=127.0.0.1&amp;ps[]=a

Notice that instead of sending "&ps=a", I also send the square brackets [] in the parameter name which will send an array object!! Also, notice that I am sending "id=127.0.0.1" so that I can get to the line 9.
And after sending this POST request...


Conclusion:

I tested this vulnerability with my local version of PHP/5.3.2-1ubuntu4.17, I do not know the version running in the CTF Server but should be similar.
After this exercise, I would suggest you all make sure you are not using strcmp() to compare values coming from end users (via POST/GET/Cookies/Headers, etc), this also reminds me the importance of not only validate parameter values BUT also parameter names as described in on of my previous blogs here.

Hope you enjoy it.

Thanks to CODEGATE Team to prepare those interesting challenges!

14 comments:

  1. very very nice! I wonder if this still works if the strcmp is used like strcmp($password, $_POST["ps"]) === 0
    with three equal signs (strict comparison in php). I'm kinda lazy to test it right now and I hope it does not work, but if it does, then PHP is full of shit.

    ReplyDelete
  2. This is because on error strcmp() returns NULL, which when using the type-unsafe equality comparison operator == evaluates to 0, as will any string and any number value >0<1.

    To avoid this hideous security hole developers should use the type-safe equality comparison operator with stcmp, e.g.:

    strcmp($str1, $str2) === 0

    ReplyDelete
  3. Very interesting post.

    I've noticed the behavior of strcmp() is difference between PHP 5.2 and PHP 5.3+.

    In PHP 5.2, strcmp('Array', array()) returns 0 (not NULL) because arguments of this function should be converted to String (see also: http://jp2.php.net/manual/en/language.types.string.php#language.types.string.casting).

    But, since PHP 5.3, strcmp('...', array()) returns NULL as you say.

    I can't find some descriptions about this changes yet, but here's the trigger commit:
    https://github.com/php/php-src/commit/58a673a9094bd26453e2b910b87ae45800ecc88c#L11L326
    Oh by removing convert_to_string_ex(), strcmp() returns NULL in case of its arguments are an array().

    It's very interesting thing too, isn't it?

    ReplyDelete
  4. Cool stuff guys, thanks for sharing, honestly I did not even know about the "===" operand.

    ReplyDelete
  5. Did you try using the X-Forwarded-For HTTP header to "spoof" ip address? If this is included in the request, it often winds up being used as remote address instead of the actual TCP endpoint, which is assumed to belong to a http proxy.

    ReplyDelete
  6. I do not understand why use "strcmp()" instead of "===". The first is for comparison, the second - for equality test. I think for password hashes it is not necessary to compare, e.g., to know whether the result is lesser or greater than.

    ReplyDelete
  7. On PHP 5.2.13 and 5.2.17, I can't get strcmp to return zero when comparing the string "danux" to the array ('id' => '127.0.0.1', 'ps' => 'bar'). It always returns 1 or -1, depending on the order.

    ReplyDelete
  8. Hi,

    My team solved that challenge in same way, but I am wondering how exactly do you spoof $_SERVER['REMOTE_ADDR']?
    I know you can spoof some headers, but not REMOTE_ADDR (as it is not a header).

    ReplyDelete
  9. Do you know what answer you'd get if you post a bug ticket in the PHP tracker, for a security issue that possibly affects tens of thousands of sites? "Thank you for your report, but this is not a bug." Every wrong, misguided, insane or simply meaningless behavior in PHP is dismissed as "works as intended" by the dev team.

    ReplyDelete
  10. Hi Anonymous, I did not spoof my IP, as explained in the blog that was not an option. You could spoof your IP address by creating a socket from scratch and changing the source IP in the packet so that Apache sets REMOTE_ADDR env variable to 127.0.0.1 and therefore PHP get the same information, this scenario work one way since no response will be received from the web app.

    ReplyDelete
  11. I think that since PHP is open source we need to use it "on our own risk" :-(

    Still I am wondering why not disable "==" operand in strcmp command and just allow "==="? compatibility/legacy issues? I am sure, there must be a valid reason.

    ReplyDelete
  12. This blog is really informative i really had fun reading it.

    ReplyDelete
  13. PHP is best and world popular programming language. Today all user want PHP web application development because it is easy to manage and so cheap also. It is fast to develop and user friendly also.

    ReplyDelete
  14. The lack of knowledge of a few commenters around here, either make me laugh or cry. The ones that think lacking knowledge of how a function works is a language "security" bug; should either stop pretending to be "security people", or stop commenting until much later when they have more knowledge.

    ReplyDelete

Note: Only a member of this blog may post a comment.