0x0 背景介绍
在 SeaCMS 13.3 及更早版本中,js/player/dmplayer/dmku/class/mysqli.class.php文件中的 显示_弹幕列表() 函数因未对 page和 limit 参数进行类型校验,导致远程 SQL 注入漏洞。攻击者可利用该漏洞读取数据库敏感信息,若 MySQL 配置不当(如 secure_file_priv = ''),还可写入Webshell 实现远程代码执行。
0x1 环境搭建
1、Ubuntu24+Docker搭建配置
- 保存
install.sh,并赋予执行权限chmod +x install.sh
bash
#!/bin/bash
# SeaCMS v13.3 一键部署脚本
set -e
PROJECT_DIR="SeaCMS-sql"
ZIP_NAME="SeaCMS_V13.3_install.zip"
URL="https://www.seacms.net/download/%E5%AE%89%E8%A3%85%E5%8C%85/SeaCMS_V13.3_install.zip"
echo "[+] 创建项目目录..."
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
echo "[+] 下载安装包..."
if [ ! -f "$ZIP_NAME" ]; then
wget -O "$ZIP_NAME" "$URL"
fi
echo "[+] 在宿主机解压并标准化路径(解决 #Uxxxx 问题)..."
rm -rf sea_cms_clean 2>/dev/null || true
unzip -o "$ZIP_NAME" -d sea_cms_clean
UPLOAD_DIR=$(find sea_cms_clean -name "Upload" -type d | head -n1)
if [ -z "$UPLOAD_DIR" ]; then
echo "❌ 未找到 Upload 目录!"
exit 1
fi
echo " -> 找到 Upload 目录: $UPLOAD_DIR"
rm -rf ./webroot 2>/dev/null || true
cp -r "$UPLOAD_DIR" ./webroot
echo "[+] 生成 Dockerfile..."
cat > Dockerfile << 'EOF'
FROM php:7.4-apache
RUN apt-get update && apt-get install -y \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
libzip-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd mysqli pdo_mysql zip \
&& echo "allow_url_fopen = On" >> /usr/local/etc/php/conf.d/custom.ini \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
COPY webroot/ /var/www/html/
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 775 /var/www/html \
&& chmod 777 /var/www/html
EOF
echo "[+] 生成 docker-compose.yml..."
cat > docker-compose.yml << 'EOF'
version: '3'
services:
web:
build: .
ports:
- "8080:80"
volumes:
- webroot:/var/www/html
depends_on:
- db
db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: seacms
command: --secure-file-priv=""
volumes:
- webroot:/var/www/html
volumes:
webroot:
EOF
echo "[+] 构建并启动服务..."
docker compose down -v >/dev/null 2>&1 || true
docker compose build --no-cache
docker compose up -d
echo ""
echo " 部署成功!"
echo " 访问安装页: http://localhost:8080/"
echo " 数据库地址: db"
echo " 用户: root / 密码: rootpass / 库名: seacms"
echo ""
echo " 启用弹幕后,可测试漏洞:"
2、 环境配置:
-
1、默认安装即可,数据库地址写
db,海洋CMS特性就是强制后台必须修改(这个dedecms可以学习),脚本实现了,下面这一步告知你新地址

-
2、后台登录后,开启弹幕功能

-
3、这一步我不太确认-添加随意一个影片

-
3.1、我这一步想的是手动发送弹幕让数据库有数据可查询,但是没有成功

-
4、进入容器插入数据
bash
#进入数据库
docker exec -it seacms-sql-db-1 mysql -u root -prootpass seacms
#插入弹幕
INSERT INTO sea_danmaku_list(id, type, text, color, size, videotime, ip, time, user)
VALUES('1', 'right', 'test_for_shell', '#ffffff', '24', 1.000, '127.0.0.1', UNIX_TIMESTAMP(), 'hacker');
#查询弹幕记录-id就是对应的影片
SELECT * FROM sea_danmaku_list;

0x2 漏洞复现
1、YML检测
一直陷入一个迷茫区域,如果扫描是创建一个shell,方便去直接利用, 但是现在大家安全设备已经很成熟,可能简单一句话上去就被杀掉了,也可能在IDS设备中,CVE本身不是危急等级, 然而shell的落地,会让安全人员快速关注到
bash
https://github.com/Kai-One001/cve-/blob/main/SeaCMS_SQLi_CVE-2025-15002.yml
- 痛腚思非,
YML尽量不实现恶意的(本次是创建一个txt,嘿嘿嘿)


2、手动复现步骤
- 未授权接口查询弹幕列表

SQL写入恶意文件

- 文件利用成功

3、复现流量特征 (PCAP)
- 可以看到明显注入语句

0x3 漏洞原理分析
1、漏洞文件与函数
- 公开描述中可以直接定位到
јѕ/рlауеr/dmрlауеr/dmku/сlаѕѕ/mуѕԛli.class.php文件,查询弹幕能找到具体方法
php
public static function 显示_弹幕列表()
{
try {
global $_config;
$page = 1;
if (isset($_GET['page'])) {
$page = $_GET['page'];
}
$limit = $_GET['limit'];
$conn = @new mysqli($_config['数据库']['地址'], $_config['数据库']['用户名'], $_config['数据库']['密码'], $_config['数据库']['名称'], $_config['数据库']['端口']);
$conn->set_charset('utf8');
$sql = "select count(*) from sea_danmaku_list ORDER BY time DESC";
$res = $conn->query($sql);
$length = $res->fetch_row();
$count = $length[0];
$index = ($page - 1) * $limit;
$stmt = self::$sql->prepare("SELECT * FROM sea_danmaku_list ORDER BY time DESC limit $index,$limit");
if($stmt->execute() == false) {
throw new Exception($stmt->error_list);
}
$data = self::fetchAll($stmt->get_result());
$stmt->close();
return $data;
} catch (PDOException $e) {
showmessage(-1, '数据库错误:' . $e->getMessage());
}
}
- 直接能敏感发现sql语句,虽然使用了
prepare(),但$index和$limit在prepare前已直接拼接到SQL字符串中,导致预编译完全失效,等同于普通query(),无法防御SQL 注入。 - 定义的"安全函数"
php
function secsql($str)
{
if (empty($str)) return false;
$str = htmlspecialchars($str);
$str = str_ireplace('/', "", $str);
$str = str_ireplace('[', "", $str);
$str = str_ireplace(']', "", $str);
$str = str_ireplace('>', "", $str);
$str = str_ireplace('<', "", $str);
$str = str_ireplace('?', "", $str);
$str = str_ireplace('&', "", $str);
$str = str_ireplace('|', "", $str);
$str = str_ireplace('{', "", $str);
$str = str_ireplace('}', "", $str);
$str = str_ireplace('%', "", $str);
$str = str_ireplace('=', "", $str);
$str = str_ireplace(':', "", $str);
$str = str_ireplace(';', "", $str);
$str = str_ireplace('*', "", $str);
$str = str_ireplace('@', "", $str);
$str = str_ireplace('--', "", $str);
$str = str_ireplace('//', "", $str);
$str = str_ireplace('\\', "", $str);
$str = str_ireplace('#', "", $str);
$str = str_ireplace('FROM', "FRO-", $str);
$str = str_ireplace('SELECT', "SELEC-", $str);
$str = str_ireplace('SLEEP', "slee-", $str);
$str = str_ireplace('union', "unio-", $str);
$str = str_ireplace('sea_', "sea-", $str);
$str = str_ireplace('null', "nul-", $str);
$str = str_ireplace('hex', "he-", $str);
$str = str_ireplace('file_', "fil-", $str);
$str = str_ireplace('updatexml', "update-", $str);
$str = str_ireplace('extractvalue', "extract-", $str);
$str = str_ireplace('benchmark', "bench-", $str);
$str = str_ireplace('load_file', "-", $str);
$str = str_ireplace('outfile', "out-", $str);
$str = str_ireplace('ascii', "asc-", $str);
$str = str_ireplace('char', "cha-", $str);
$str = str_ireplace('chr', "ch-", $str);
$str = str_ireplace('substr', "sub-", $str);
$str = str_ireplace('substring', "sub-", $str);
$str = str_ireplace('script', "scr-i", $str);
$str = str_ireplace('frame', "fra-", $str);
$str = str_ireplace('information_schema', "info-", $str);
$str = str_ireplace('exp', "ex-", $str);
$str = str_ireplace('information_schema', "infor-", $str);
$str = str_ireplace('GeometryCollection', "Geomet-", $str);
$str = str_ireplace('polygon', "poly-", $str);
$str = str_ireplace('multipoint', "multi-", $str);
$str = str_ireplace('multilinestring', "multi-", $str);
$str = str_ireplace('linestring', "lines-", $str);
$str = str_ireplace('multipolygon', "multi-", $str);
$str = str_ireplace('base64', "bas-", $str);
return $str;
}
-
secsql():典型的黑名单式字符串替换(替换union、select、sleep等关键字),本身就很脆弱,容易被大小写/编码/拼接绕过。 -
关键问题:在
显示_弹幕列表()中,对$_GET['page']和$_GET['limit']也没有调用secsql()或任何类型强制/白名单校验,完全无用。
2、漏洞入口
- 向上一级可以查看到
inde.php文件
php
error_reporting(0);
require_once('init.php');
require_once('class/danmu.class.php');
require_once('../admin/data.php');
if($yzm['danmuon']!='on'){echo '弹幕系统已关闭!';die;}
........
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
if ($_GET['ac'] == "report") {
$text = $_GET['text'];
sql::举报_弹幕($text);
showmessage(-3, '举报成功!感谢您为守护弹幕作出了贡献');
} else if ($_GET['ac'] == "dm" or $_GET['ac'] == "get") {
$id = $_GET['id'] ?: showmessage(-1, null);
$data = $d->弹幕池($id) ?: showmessage(23, []);
showmessage(23, $data);
} else if ($_GET['ac'] == "list") {
$data = $d->弹幕列表() ?: showmessage(0, []);
showmessage(0, $data);
} else if ($_GET['ac'] == "reportlist") {
$data = $d->举报列表() ?: showmessage(0, []);
showmessage(0, $data);
} else if ($_GET['ac'] == "del") {
$id = intval($_GET['id']) ?: succeedmsg(-1, null);
$type = $_GET['type'] ?: succeedmsg(-1, null);
$data = $d->删除弹幕($id) ?: succeedmsg(0, []);
succeedmsg(23, true);
} else if ($_GET['ac'] == "so") {
$key = $_GET['key'] ?: showmessage(0, null);
$data = $d->搜索弹幕($key) ?: showmessage(0, []);
showmessage(0, $data);
}
}
- 只检查是否开启弹幕,未作鉴定
- 关键参数,
ac、page、limit
| 参数 | 说明 | 来源 |
|---|---|---|
| ac | 动作类型 | $_GET['ac'] |
| page | 页码 | $_GET['page'](默认为 1) |
| limit | 每页数量 | $_GET['limit'](无默认值) |
3、业务逻辑层
- 入口文件中有引用
require_once('class/danmu.class.php');一个文件,我们查看是否有配置
php
public function 弹幕列表()
{
$data = sql::显示_弹幕列表();
//print_r($data);
if (empty($data)) return null;
$arr = [];
foreach ($data as $k => $v) {
// 请不要随意调换下列数组赋值顺序
$arr[$k][] = (string) $v['id']; //弹幕id
$arr[$k][] = (float) $v['videotime']; //弹幕出现时间(s)
$arr[$k][] = (string) $v['type']; //弹幕样式
$arr[$k][] = (string) $v['color']; //字体的颜色
$arr[$k][] = (string) $v['cid']; //现在是弹幕id,以后可能是发送者id了
$arr[$k][] = (string) $v['text']; //弹幕文本
$arr[$k][] = (string) $v['ip']; //弹幕ip
//$arr[$k][] = (string)$v['time']; //弹幕系统时间
$arr[$k][] = $date = date('m-d H:i', $v['time']); //弹幕系统时间
$arr[$k][] = (string) $v['size']; //弹幕系统大小
$arr[$k][] = (string) $v['user']; //弹幕用户
}
return $arr;
}
- 封装数据格式,不涉及任何输入过滤
- 调用链:
index.php → danmu::弹幕列表() → sql::显示_弹幕列表()
4、利用细节
关键语句如下,目的则是想要尝试注入进去:
php
- $index = ($page - 1) * $limit;
-
- "SELECT * FROM sea_danmaku_list ORDER BY time DESC limit $index,$limit"
小知识:
1. PHP对字符串参与算术运算时会做"数字前缀取值"
-
$page = "1abc" → 在 ($page - 1)中按数字 1 处理 -
$limit = "10 union select ..."→ 在乘法时会按数字 10 处理计算$index -
但
$limit自身在拼接中仍是原始字符串"10 union select ...",不会被自动截断
2. MySQL LIMIT 语法允许表达式
-
例如:
LIMIT 0, 10 UNION SELECT ...是合法的整个查询的一部分。 -
也可以写:
LIMIT 0, 10 PROCEDURE ANALYSE(...)等,这里逗号后是表达式,不要求是"纯数字字面量"
3.因此攻击面在第二个参数 $limit
-
$index虽然计算时会把$limit当数字使用,但SQL里使用的是原始字符串$limit -
攻击者只要构造一个以数字开头的字符串即可同时:
-
通过
PHP运算(不报错) -
又在
SQL中注入后续恶意片段。 -
用户请求:
?ac=list&page=1&limit=10 union select ... -
拼出的 SQL:
sql
SELECT * FROM sea_danmaku_list ORDER BY time DESC limit 0,10 union select ...
这就导致基于 LIMIT 子句的 SQL 注入。
4. INTO OUTFILE 原理
- 前提:
root权限、数据库配置文件中secure_file_priv=''满足,知道数据库的绝对路径 - 当执行
SELECT 'hello' INTO OUTFILE '/tmp/test.txt';MySQL会:把字符串'hello'的字节表示直接写入文件 - 然而查询配置文件
config.inc.php也没有发现有限制,这导致可以写入任意目录

0x4 修复建议
修复方案
- 升级到最新版本?:我看官方好像也就最新在
13了(14版本的更新日志没有发现处理这个功能),不清楚后续是否还继续更新,毕竟是免费开源的,多一些理解:海洋CMS地址 - 开发不是强项,只能给一些思路吧
bash
1、强制类型转换 + 数值范围校验
场景:所有涉及 page/limit 的 SQL 查询(显示_弹幕列表()/显示_举报列表())
代码:js/player/dmplayer/dmku/class/mysqli.class.php
//修复1:强制转换并校验 page
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) $page = 1; // 最小值校验
if ($page > 10000) $page = 10000; // 防DoS(最大10000页)
//修复2:强制转换并校验 limit(默认20,最大100)
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
if ($limit < 1) $limit = 1;
if ($limit > 100) $limit = 100; // 防数据量过大
$index = ($page - 1) * $limit; // 此时 index 已为安全整数
//修复3:安全拼接 SQL(仅使用整数变量)
$sql = "SELECT * FROM sea_danmaku_list ORDER BY time DESC LIMIT $index,$limit";
$stmt = self::$sql->prepare($sql); // 无需额外转整数
2、同步修复 显示_举报列表()
// 修复逻辑完全一致,仅替换函数名
3、安全加固:统一参数验证函数 + 预处理语句
// 文件顶部添加(所有函数共用)
function safe_int($param, $default = 1, $min = 1, $max = 10000) {
if (!isset($param) || !is_numeric($param)) return $default;
$val = (int)$param;
return max($min, min($val, $max));
}
- 临时防护措施:
Nginx WAF 规则 :拦截含SQL关键字的limit参数
添加访问控制: :防止未授权数据泄露
关闭/限制INTO OUTFILE功能 :若无业务需求,改为·NULL,或者自定义目录
项目审计:此项还有其它漏洞,建议审计代码
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。