0x0 背景介绍
Smart Slider 3是一款流行的WordPress动态滑块和页面构建插件。
在3.5.1.33及之前版本中,插件的actionExportAll 函数在处理导出逻辑时未对用户输入的文件路径进行严格过滤。具有订阅者(Subscriber)及以上权限的攻击者可以利用该漏洞读取服务器上的任意文件内容(如 wp-config.php),导致数据库凭据等敏感信息泄露。
0x1 环境搭建(Ubuntu24)
- 老朋友·inshtall.sh
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进行访问控制,仅允许后台已登录且满足权限的会话访问
-
权限最小化:站点全部权限最小化,设置强密码避免盗刷。
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。