0x0 背景介绍
NocoBase的流程HTTP请求插件和自定义请求动作插件会在用户提供的URL上发起服务器端HTTP请求,且没有任何SSRF防护。经认证的用户可访问内部网络服务、云元数据端点以及本地主机。
Vvveb是一套开源的PHP内容管理系统,用于构建网站、博客或电商平台。
受影响版本中,media.php文件的rename()函数在检测到受限扩展名(如.php、.htaccess)时,仅设置$success=false但缺少return语句,导致验证失败后仍继续执行重命名操作。攻击者可利用此逻辑缺陷先将文本文件重命名为.htaccess注入Apache指令,再将另一文件重命名为.php从而执行任意操作系统命令。
修复版本中通过在扩展名检测逻辑后添加return语句并提前设置响应类型为json,使验证失败时立即返回错误响应并终止函数执行。
0x1 环境搭建(Ubuntu24)
-
1.1-Ubuntu24+Docker搭建配置
bash
#!/bin/bash
# 若任何命令失败则退出
set -e
echo "[*] 阶段1/4:检查并安装基础依赖..."
if ! command -v docker &> /dev/null || ! command -v git &> /dev/null || ! command -v curl &> /dev/null; then
echo "[+] 正在安装 docker.io git curl ..."
sudo apt update && sudo apt install -y docker.io git curl
fi
sudo systemctl enable --now docker 2>/dev/null || true
echo "[*] 阶段2/4:创建工作目录并克隆 Vvveb 仓库(含子模块)..."
WORKDIR="$HOME/vvveb-lab-git"
mkdir -p "$WORKDIR" && cd "$WORKDIR"
if [ -d Vvveb ]; then
echo "[!] 目标目录 Vvveb 已存在,将进行清理..."
rm -rf Vvveb
fi
git clone --recurse-submodules https://github.com/givanz/Vvveb.git
cd Vvveb
# 切换到稳定版本 1.0.8
git checkout 1.0.8
# 确保子模块匹配标签版本
git submodule update --init --recursive
echo "[+] Vvveb 1.0.8 项目已准备完毕(含完整主题)。"
echo "[*] 阶段3/4:生成 docker-compose.yml 配置文件..."
cat > docker-compose.yml <<'EOF'
version: '3.8'
services:
web:
image: php:8.1-apache
container_name: vvveb_web
volumes:
- .:/var/www/html
ports:
- "8080:80"
depends_on:
- db
networks:
- vvveb_net
command: >
sh -c "apt-get update &&
apt-get install -y libzip-dev &&
docker-php-ext-install pdo_mysql gettext mysqli zip &&
a2enmod rewrite &&
sed -i 's|DocumentRoot /var/www/html|DocumentRoot /var/www/html/public|' /etc/apache2/sites-available/000-default.conf &&
sed -i 's|AllowOverride None|AllowOverride All|g' /etc/apache2/conf-enabled/docker-php.conf &&
chown -R www-data:www-data /var/www/html &&
apache2-foreground"
db:
image: mysql:5.7
container_name: vvveb_db
command: --default-authentication-plugin=mysql_native_password --sql-mode=""
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: vvveb_db
MYSQL_USER: vvveb_user
MYSQL_PASSWORD: vvveb_pass
volumes:
- db_data:/var/lib/mysql
networks:
- vvveb_net
volumes:
db_data:
networks:
vvveb_net:
EOF
echo "[*] 阶段4/4:启动 Docker 容器(首次需拉取镜像并编译扩展,约1分钟)..."
docker compose down -v 2>/dev/null || true
docker compose up -d
echo ""
echo "=============================================="
echo " Vvveb 1.0.8 环境部署完成!"
echo " - 访问地址: http://localhost:8080"
echo " - 安装向导将自动启动"
echo " - 数据库信息(安装时填写):"
echo " 引擎:MySQLi"
echo " 主机:db"
echo " 库名:vvveb_db"
echo " 用户:vvveb_user"
echo " 密码:vvveb_pass"
echo " - 管理员账号在安装时自行设置"
echo " - 后台地址: http://localhost:8080/admin/"
echo ""
echo " - 如有问题,可查看日志: docker logs vvveb_web"
echo "=============================================="
0x2 漏洞复现
2.1-脚本验证
bash
https://github.com/Kai-One001/cve-/blob/main/Vvveb_CMS_RCE_CVE-2026-6257.py

- 脚本思路是👇:
bash
1) 用管理员账号登录后台
2) 打开媒体管理器(媒体/资源管理相关页面,内部会调用`admin/index.php?module=media/media`)
3) 上传一个普通文本文件(例如a.txt)到media/根目录
4) 在媒体列表中对a.txt执行重命名,改成.htaccess
5) 再上传另一个文本文件(例如 shell.txt)
6) 访问 https://<host>/media/shell.txt?cmd=whoami(示例)观察命令执行结果
2.2-手动复现
2.2.1-先上传txt文件内容为.htacces

2.2.2-对文件进行重命名

2.2.3-再次上传一句话
理论思路是:上传一句话txt文件,重命名为php文件配合之前重命名的.htacces进行利用,但是这里偷懒了

2.2.4-验证执行
因为.htacces有 将txt当作php执行,所以可以直接验证

2.2.5-主机验证

2.3-复现流量特征 (PCAP)
- 上传接口

- 重命名文件

- RCE验证成功

0x3 漏洞原理分析
3.1-架构与模块定位
Vvveb CMS 的媒体管理并不是一个"单独的上传脚本",而是典型的Admin****控制器 + Trait 复用逻辑 结构:控制器负责路由入口与视图变量,而真正的上传/删除/重命名等危险文件操作被放进system/traits/media.php的Media trait 里复用
这类设计的预期安全边界通常是:
markdown
1. 入口层(Admin 控制器)保证 已登录、CSRF 校验、权限;
2. 逻辑层(Trait)保证 文件名净化、扩展名/MIME 拦截,并且在拦截命中时 立即中断危险操作;
3. 物理层(文件系统/Apache)按配置决定是否会解释执行。

3.2 锁定关键路径:从"媒体重命名"一路追到文件系统 rename()
漏洞描述中提到"媒体管理功能",而Vvveb的后台模块通常是 admin[自定义]/index.php?module=... 的路由。先确认媒体模块的入口是否把upload/rename作为可调用action暴露出来。
技巧:
bash
• 直接在代码里搜索 module=media/media,可以看到多处控制器引用;
• 最核心的是 admin/controller/media/media.php ,里面会构造 $controllerPath。
查到一个媒体模块控制器 并不直接处理重命名,但它做了一件关键事:调用 setMediaEndpoints(),把 upload/rename 等 endpoint 拼成 URL暴露给前端。
php
// admin/controller/media/media.php
useVvveb\System\Traits\Media asMediaTrait;
// ...
function index(){
$adminPath = \Vvveb\adminPath();
$controllerPath = $adminPath .'index.php?module=media/media';
$this->setMediaEndpoints($controllerPath);
$this->view->mediaContentUrl ="$controllerPath&action=mediaContent";
}
// ...
-
入口可以明确了:所有重命名都落到同一个动作
action=rename,而动作实现就在同一个trait里的rename()函数(问了下AI命名逻辑)。 -
控制器类
Media明确use MediaTrait;真正的action实现并不在控制器文件里,而在trait中 -
接下来要找的就是:它是否真的在执行前阻断了危险扩展名。
3.3-边界缺失:好消息~有拦截 坏消息!没拦住
顺着setMediaEndpoints()往下看,找下endpoint ,检查是否存在upload()与 rename()
kotlin
// system/traits/media.php
protected function setMediaEndpoints($controllerPath) {
$this->view->uploadUrl = "$controllerPath&action=upload";
$this->view->renameUrl = "$controllerPath&action=rename";
//xxxx
}
- 先注意到一个deny列表,说明代码是做限制了:
php
// system/traits/media.php
public $uploadDenyExtensions = ['php', 'svg', 'js', 'exe'];
- 然后再看主角
rename(),关键片段如下:
php
// system/traits/media.php
function rename() {
$file = sanitizeFileName($this->request->post['file']);
$newname = sanitizeFileName($this->request->post['newname'] ?? '');
// ...
$currentFile = $dirMedia . DS . $file;
if ($newname) {
$targetFile = dirname($currentFile) . DS . $newname;
}
$extension = strtolower(substr($targetFile, strrpos($targetFile, '.') + 1));
if (isset($this->uploadDenyExtensions) && in_array($extension, $this->uploadDenyExtensions)) {
$message .= __('File type not allowed!');
$success = false;
}
// 关键:即便命中 deny,这里也没有 return / die / output
if (rename($currentFile, $targetFile)) {
$message = ['success' => true, 'message' => __('File renamed!')];
} else {
$message = ['success' => false, 'message' => __('Error renaming file!')];
}
$this->response->output($message);
}
-
预期设计:命中deny列表(例如目标扩展名是
php)时,应当 立即中断 ,并返回失败响应(比如403/400或success=false)。 -
实际实现:命中deny后只是给
$success=false,但没有任何 控制流中断 ,随后无条件进入rename($currentFile, $targetFile)。 -
结果:扩展名拦截从安全策略变成提示,只要底层文件系统允许重命名,后台就会给出 重命名成功的JSON。
-
更绝的是:
rename()函数中$message/$success甚至没有初始化(相比upload()有明确初始化),这意味着"以为自己在维护一个success/message 状态机",但真正返回给客户端的,最终会被rename()的结果覆盖
重命名PHP后缀时,提示失败,但已完成(不过按理说可以直接执行PHP了,但是我一直403...人麻木了)

3.4-推导最大危害:从"可写文件"升级到"可执行代码"
目前来看,重命名绕过.php的理论已经成立,然后就再往下翻翻如何RCE
第一段:上传阶段的安全边界确实存在
php
//system/traits/media.php
if (isset($this->uploadDenyExtensions) && in_array($extension, $this->uploadDenyExtensions)) {
$message .= __('File type not allowed!');
$success = false;
}
......
if (isset($this->uploadDenyMime) && in_array($mimeType, $this->uploadDenyMime)) {
$message .= __('File type not allowed!');
$success = false;
}
......
if ($success) {
if ($overwrite) {
$destination = $this->dirMedia . $path . DS . $fileName;
} else {
while (file_exists($destination = $this->dirMedia . $path . DS . $fileName) && ($i++ < 5)) {
$fileName = rand(0, 10000) . '-' . $origFilename;
}
}
if (@move_uploaded_file($files['tmp_name'][$count], $destination)) {
if (isset($this->request->post['onlyFilename'])) {
$return = $fileName;
} else {
$return = $destination;
}
$message = __('File uploaded successfully!');
} else {
$destination = $this->dirMedia . $path . DS;
$success = false;
if (! is_writable($destination)) {
$message = sprintf(__('%s not writable!'), $destination);
} else {
$message = __('Error moving uploaded file!');
}
}
}
upload()会检查扩展名与 MIME:所以攻击者不能直接上传.php。
第二段:重命名阶段把上传边界"拆掉"一旦攻击者能把已上传文件重命名,媒体目录就可能出现可执行脚本。
但还差一个现实条件:服务器是否会把该目录下的.php执行?
bash
1、在很多很多部署里,/media/ 本就位于 Web 根目录,且 PHP 解析默认对 .php 生效;这时 仅凭重命名 即可 RCE。•
2、如果运维曾做过限制,攻击者仍可通过写入 .htaccess
这里还有一个关键事实:Vvveb的deny列表里 没有htaccess。这意味着:
css
1、上传 a.txt(内容为 Apache 指令)是允许的;
2、重命名为 .htaccess 也很可能允许; 最终媒体目录的行为被攻击者"重配置",从纯静态资源目录变成可执行载荷的容器。
3.5 链路总结(调用链 + 注入点/爆发点标注)
完整链路(以后台媒体模块为入口):
perl
/xxx/index.php?module=media/media
-> xxx/controller/media/media.php::index()
-> system/traits/media.php::setMediaEndpoints()`(暴露 `action=upload` / `action=rename`)
-> [注入点] `system/traits/media.php::upload()`(允许上传文本载荷为 `.txt`)
-> [逻辑缺陷] `system/traits/media.php::rename()`(deny 命中不 return,仍执行 `rename()`)
-> [爆发点] Web 访问 `public/media/<payload>.php`(或受 `.htaccess` 影响的扩展)触发解释执行
-> RCE:以 Web 进程用户执行系统命令
0x4 修复建议
1、升级最新版本:将组件升级安全版本,v1.0.8以上版本
javascript
https://github.com/givanz/Vvveb
2、临时防护措施:
-
服务器隔离:禁止public/media/目录下解析PHP,明确禁止.htaccess 生效:对媒体目录AllowOverride None,并阻止上传/写入以点开头的敏感文件名
-
防火墙 / WAF:拦截/审计后台接口,且newname/newfile 以.php、.phtml、.phar、.htaccess、.user.ini结尾
-
拦截上传问:拦截上传内容包含Apache指令关键字:AddType、SetHandler、php_value、php_flag
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。