下载项目最新发行版
代码审计
漏洞定位
漏洞点在后台的模块市场,这里也是一个小技巧一般在这种引入外部模块的时候常常存在风险
先抓个包看看源码位置
查看哪里调用了upload
在app/admin/controller/Module.php中
对于代码进行查看,这里的上传检查了token因此只能时后台上传,并且在后续再次调用了一次upload方法并且带有file和token参数因此跟进查看
public function upload(): void
{
AdminLog::setTitle(__('Upload install module'));
//对于$file和token进行检测
$file = $this->request->get("file/s", '');
$token = $this->request->get("token/s", '');
if (!$file) $this->error(__('Parameter error'));
if (!$token) $this->error(__('Please login to the official website account first'));
$info = [];
try {
//存在upload方法
$info = Manage::instance()->upload($token, $file);
} catch (Exception $e) {
$this->error(__($e->getMessage()), $e->getData(), $e->getCode());
} catch (Throwable $e) {
$this->error(__($e->getMessage()));
}
$this->success('', [
'info' => $info
]);
}
跟进到\app\admin\library\module中
分析代码
public function upload(string $token, string $file): array
{
$file = Filesystem::fsFit(root_path() . 'public' . $file);
if (!is_file($file)) {
// 包未找到
throw new Exception('Zip file not found');
}
//文件移动
$copyTo = $this->installDir . 'uploadTemp' . date('YmdHis') . '.zip';
copy($file, $copyTo);
// 解压
$copyToDir = Filesystem::unzip($copyTo);
$copyToDir .= DIRECTORY_SEPARATOR;
// 删除zip
@unlink($file);
@unlink($copyTo);
// 读取ini
$info = Server::getIni($copyToDir);
if (empty($info['uid'])) {
Filesystem::delDir($copyToDir);
// 基本配置不完整
throw new Exception('Basic configuration of the Module is incomplete');
}
// 安装预检 - 系统版本号要求、已安装模块的互斥和依赖检测
try {
Server::installPreCheck([
'uid' => $info['uid'],
'sysVersion' => Config::get('buildadmin.version'),
'nuxtVersion' => Server::getNuxtVersion(),
'ba-user-token' => $token,
'installed' => Server::getInstalledIds($this->installDir),
'server' => 1,
]);
} catch (Throwable $e) {
Filesystem::delDir($copyToDir);
throw $e;
}
//获取ini中的uid
$this->uid = $info['uid'];
//把原本解压的文件目录改名为modules/ + info.ini中的uid/
$this->modulesDir = $this->installDir . $info['uid'] . DIRECTORY_SEPARATOR;
$upgrade = false;
if (is_dir($this->modulesDir)) {
$oldInfo = $this->getInfo();
if ($oldInfo && !empty($oldInfo['uid'])) {
$versions = explode('.', $oldInfo['version']);
if (isset($versions[2])) {
$versions[2]++;
}
$nextVersion = implode('.', $versions);
$upgrade = Version::compare($nextVersion, $info['version']);
if (!$upgrade) {
Filesystem::delDir($copyToDir);
// 模块已经存在
throw new Exception('Module already exists');
}
}
if (!Filesystem::dirIsEmpty($this->modulesDir) && !$upgrade) {
Filesystem::delDir($copyToDir);
// 模块目录被占
throw new Exception('The directory required by the module is occupied');
}
}
$newInfo = ['state' => self::WAIT_INSTALL];
if ($upgrade) {
$newInfo['update'] = 1;
// 清理旧版本代码
Filesystem::delDir($this->modulesDir);
}
// 放置新模块
rename($copyToDir, $this->modulesDir);
// 检查新包是否完整
$this->checkPackage();
// 设置为待安装状态
$this->setInfo($newInfo);
return $info;
}
其中的关键是从getIni函数中获取了一个uid,并且将解压缩文件的文件名改为了对应的uid,因此变量可能存在可控的风险,尝试断点分析。在app\admin\controller\Module.php下断点
但上传时出现报错,显示无法直接上传,搜索后发现是token报错,但利用bp抓包可以成功上传。利用F12查看网络发现是存在Batoken但debug时无法读取到因此抛出报错。为完成测试先注册一个官网账户。
注册登录后再次上传即可进入upload断点,并且跟进到getIni,并分析源码
public static function getIni(string $dir): array
{
//获取路径下的一个info.ini文件
$infoFile = $dir . 'info.ini';
$info = [];
//判断是否存在这个文件
if (is_file($infoFile)) {
$info = parse_ini_file($infoFile, true, INI_SCANNER_TYPED) ?: [];
if (!$info) return [];
}
return $info;
}
我们本次zip内只含有一个phpinfo的代码,这里无法正常进入,return一个空的$info导致抛出基本配置不完整的错误,尝试手动添加一个info.ini文件并且对应内容
info.ini伪造
根据代码中的内容进行info.ini的伪造
public function checkPackage(): bool
{
if (!is_dir($this->modulesDir)) {
throw new Exception('Module package file does not exist');
}
$info = $this->getInfo();
$infoKeys = ['uid', 'title', 'intro', 'author', 'version', 'state'];
foreach ($infoKeys as $value) {
if (!array_key_exists($value, $info)) {
Filesystem::delDir($this->modulesDir);
throw new Exception('Basic configuration of the Module is incomplete');
}
}
return true;
}
将info.ini与恶意文件一起压缩,再次上传,成功获取到ini中的信息
并且我们出入的uid和$this->installDir进行了拼接
继续往下跟进拼接后的语句会通过rename函数进行重命名这样就可以是的文件路径可知
PHP中的rename()函数可以用于移动文件夹
文件上传Getshell
既然我们可以通过uid的拼接来控制命名,那能否利用拼接来完成目录穿越导致任意文件上传呢
构造uid如下
uid=/../public/test
成功拼接uid
成功getshell
总结
对于一个网站来说在模板导入是一个危险的操作,常常伴随着文件上传的风险,因此测试时可以在此测试是否存在文件上传漏洞。
但文件上传往往需要知道路径,如在本次代码中的路径是通过date('YmdHis') 生成,并且上传的路径与是否解析有关,PHP代码中常常限制了解析代码的路径,正常上传的路径可能存在文件无法解析的问题。在该代码中也存在着相同的问题,但本次代码中存在uid的拼接问题,并且有rename函数导致文件可以直接移动,因此导致了文件上传漏洞的产生。