Vvveb CMS 任意文件上传导致RCE | CVE-2026-6257原理分析&研究

0x0 背景介绍

NocoBase的流程HTTP请求插件和自定义请求动作插件会在用户提供的URL上发起服务器端HTTP请求,且没有任何SSRF防护。经认证的用户可访问内部网络服务、云元数据端点以及本地主机。

Vvveb是一套开源的PHP内容管理系统,用于构建网站、博客或电商平台。

受影响版本中,media.php文件的rename()函数在检测到受限扩展名(如.php、.htaccess)时,仅设置$success=false但缺少return语句,导致验证失败后仍继续执行重命名操作。攻击者可利用此逻辑缺陷先将文本文件重命名为.htaccess注入Apache指令,再将另一文件重命名为.php从而执行任意操作系统命令。

修复版本中通过在扩展名检测逻辑后添加return语句并提前设置响应类型为json,使验证失败时立即返回错误响应并终止函数执行。


0x1 环境搭建(Ubuntu24)

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.phpMedia 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/400success=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


免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。

相关推荐
测绘第一深情2 小时前
租用GPU云服务器进行深度学习(AutoDL,超保姆级,适用新手)
数据结构·人工智能·经验分享·python·深度学习·算法·计算机视觉
weixin_537217063 小时前
数学讲义资源合集
经验分享
HelloGitHub3 小时前
《HelloGitHub》第 121 期
开源·github
lularible3 小时前
PTP协议精讲(3.8):硬件时间戳详解——纳秒级精度的魔法
网络·网络协议·开源·嵌入式·ptp
奇逍科技圈3 小时前
开源赋能与 BC 一体化:深度解析中企销订货系统源码如何重构批发零售增长引擎
后端·架构·开源·零售
呱牛do it3 小时前
企业级需求管理工具(关注后续开源)
开源
a1117763 小时前
Web3D 在线3D模型骨骼动画编辑器(开源 Reze Studio)
前端·3d·开源·html
我不是懒洋洋3 小时前
【数据结构】二叉树链式结构的实现(二叉树的遍历、使用二叉树的基本方法、二叉树的创建和销毁)
c语言·数据结构·c++·经验分享·算法·链表·visual studio