DedeCMS pm.php SQL 注入修复完整指南:CVE-2018-9134、参数化查询、密码哈希升级与应急响应
DedeCMS 会员中心 pm.php 的 CVE-2018-9134 注入至今仍是脱库主入口。本文从源码层讲清漏洞成因(intval 缺失),给出最小补丁、PDO 参数化查询、ModSecurity WAF 兜底三层修复,并扩展到全站注入点审计、bcrypt 密码升级、被脱库后的应急响应流程。
DedeCMS 会员中心 member/pm.php(站内私信功能)在 2018 年被披露存在 SQL 注入漏洞,CVE 编号 CVE-2018-9134。攻击者通过构造特殊的 id 参数能直接读取 dede_member 表里的密码哈希、邮箱、手机号等敏感字段。这个漏洞至今仍是 DedeCMS 站点被脱库的最常见入口之一。本文从源码层面讲清漏洞成因(intval 缺失导致字符串拼接进 SQL)、给出最小修复 patch 与几种替代方案(参数化查询、ORM 包装层)、扩展到全站 dede:sql 注入审计的方法论、被脱库后的应急响应、与现代 PHP 生态(PDO 预编译、Doctrine ORM)的迁移路径。
漏洞成因详解
原始漏洞代码
问题代码在 member/pm.php 的 read 分支:
else if ($dopost == 'read') {
$sql = "SELECT * FROM `#@__member_friends`
WHERE mid='{$cfg_ml->M_ID}' AND ftype!='-1'
ORDER BY addtime DESC LIMIT 20";
$friends = array();
$dsql->SetQuery($sql);
$dsql->Execute();
while ($row = $dsql->GetArray()) {
$friends[] = $row;
}
$row = $dsql->GetOne(
"SELECT * FROM `#@__member_pms`
WHERE id='$id' AND (fromid='{$cfg_ml->M_ID}' OR toid='{$cfg_ml->M_ID}')"
);
// ...
}关键问题在 WHERE id='$id' —— $id 直接来自 $_GET['id'] 或 $_POST['id'],未做任何过滤。SQL 字符串拼接的方式让攻击者可以构造:
?id=1' UNION SELECT pwd FROM dede_member WHERE mid=1 -- 实际拼出的 SQL:
SELECT * FROM dede_member_pms
WHERE id='1' UNION SELECT pwd FROM dede_member WHERE mid=1 -- '
AND (fromid='current_user' OR toid='current_user')UNION 查询返回了 dede_member 表里管理员(mid=1)的密码哈希。
为什么单引号包围还是有漏洞
很多新手会以为「id='$id'」的单引号已经把输入限制成字符串,攻击者注不进 SQL。但这是错觉:单引号本身可以被攻击者闭合。?id=1' 中的 ' 把 SQL 里的引号闭合了,后面的 UNION 就脱离了字符串上下文。
为什么 mid 字段拼接是安全的
仔细看 SQL 里同样有 mid='{$cfg_ml->M_ID}',但 M_ID 来自服务器端会话(已登录用户的 ID),不可被外部控制。这是关键差别:来自用户输入的字段必须过滤,来自服务器内部的可控变量不需要。
修复方案一:intval 强类型转换
最小补丁
原文给的修复方案是在拼 SQL 之前加 intval:
else if ($dopost == 'read') {
$sql = "SELECT * FROM `#@__member_friends`
WHERE mid='{$cfg_ml->M_ID}' AND ftype!='-1'
ORDER BY addtime DESC LIMIT 20";
$friends = array();
$dsql->SetQuery($sql);
$dsql->Execute();
while ($row = $dsql->GetArray()) {
$friends[] = $row;
}
/* 修复:$id 强转整数 */
$id = intval($id);
/* */
$row = $dsql->GetOne(
"SELECT * FROM `#@__member_pms`
WHERE id='$id' AND (fromid='{$cfg_ml->M_ID}' OR toid='{$cfg_ml->M_ID}')"
);
if (!is_array($row)) {
ShowMsg('对不起,你指定的消息不存在或你没权限查看!', '-1');
exit();
}
$dsql->ExecuteNoneQuery(
"UPDATE `#@__member_pms` SET hasview=1
WHERE id='$id' AND folder='inbox' AND toid='{$cfg_ml->M_ID}'"
);
$dsql->ExecuteNoneQuery(
"UPDATE `#@__member_pms` SET hasview=1
WHERE folder='outbox' AND toid='{$cfg_ml->M_ID}'"
);
include_once(dirname(__FILE__) . '/templets/pm-read.htm');
exit();
}intval 的工作原理
PHP 的 intval() 把任意字符串转成整数。intval("1") 是 1,intval("1' UNION SELECT...") 也是 1,因为 PHP 从字符串开头取数字直到第一个非数字字符为止。所有注入 payload 都会被截断为安全的整数。
这个方案的局限
- 只对整数 id 字段有效。如果字段是字符串(用户名、邮箱、UUID),intval 会变成 0。
- 同文件中其它 dopost 分支(write、send、del)可能有相似漏洞,需要逐一审计。
- 修复后下次 DedeCMS 升级 pm.php 可能覆盖你的改动,要做好补丁记录。
修复方案二:参数化查询(更彻底)
DedeCMS 的 dsql 类支持预编译
DedeCMS 自带的 $dsql 对象其实有 SetQuery 之外的预编译接口,但代码里几乎不用。改成参数化:
$id = intval($id); // 防御性双保险
$sql = "SELECT * FROM `#@__member_pms`
WHERE id = ?
AND (fromid = ? OR toid = ?)";
$pdo = new PDO($dsn, $user, $pass);
$stmt = $pdo->prepare(str_replace('#@__', $cfg_dbprefix, $sql));
$stmt->execute([$id, $cfg_ml->M_ID, $cfg_ml->M_ID]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);PDO 预编译彻底杜绝 SQL 注入,不依赖 intval 这种类型转换的偶然性。
用 escapestring 兜底
如果不引入 PDO,DedeCMS 自带 addslashes 等转义。但 addslashes 不安全(多字节字符集下能被绕过),改用 mysqli_real_escape_string:
$id = mysqli_real_escape_string($GLOBALS['link'], $id);
// 仍要确保字段类型,避免空字符串触发逻辑错误
if (!preg_match('/^\d+$/', $id)) { exit('Invalid ID'); }全站 SQL 注入审计方法论
定位所有用户输入入口
DedeCMS 通过 require_once common.inc.php 自动把 $_GET / $_POST 的所有值注册为全局变量。比如 ?id=123 会自动产生 $id=123。这种「自动全局化」让追踪输入入口变得困难——表面上 $id 看起来是局部变量,实际是用户输入。
命令行扫描所有可能涉及用户输入的代码:
# 找所有 SQL 字符串拼接的位置
cd /var/www/dedecms
grep -rn "WHERE.*=.*'\$" --include="*.php" .
grep -rn "INSERT.*\$" --include="*.php" .
grep -rn "UPDATE.*=.*\$" --include="*.php" .标记可疑文件
历史上 DedeCMS 漏洞集中爆出的目录:
- plus/ —— 几乎每个文件都曾出过漏洞,其中 recommend.php、carbuyaction.php、search.php、mytag_js.php、advancedsearch.php 是高频名单。
- member/ —— pm.php、resetpassword.php、edit_baseinfo.php、ajax_membergroup.php。
- include/ —— common.func.php 里的 GetCookie、PutCookie 函数有签名验证缺失。
- dede/ —— 后台理论上要鉴权,但部分接口的鉴权用 == 而不是 === 比较,可类型混淆绕过。
批量打补丁
对每个识别出的漏洞点应用「intval + addslashes + 严格类型校验」三件套:
// 整数字段
$id = intval($id);
if ($id <= 0) { ShowMsg('参数错误', '-1'); exit(); }
// 字符串字段
$keyword = addslashes(htmlspecialchars($keyword));
if (strlen($keyword) > 50) { ShowMsg('关键词过长', '-1'); exit(); }
// 枚举字段(性别、状态)
$gender = in_array($gender, ['male', 'female', 'other']) ? $gender : 'other';WAF 层面的防护
nginx 层 SQL 注入拦截
Application 层修复之外,可以在 nginx 加正则规则拦截 SQL 注入特征请求:
map $args $sql_injection {
default 0;
"~*(union[\s\+]+select|select[\s\+]+from|insert[\s\+]+into|update[\s\+]+set|drop[\s\+]+table|delete[\s\+]+from|or[\s\+]+1=1|sleep\(|benchmark\(|substr\(|concat\()" 1;
}
server {
if ($sql_injection = 1) {
return 403;
}
# ...
}这种规则有误报风险(合法 URL 中含有 select 关键词会被拦),生产部署前用日志模式(log 而不是 return 403)观察一段时间。
用 ModSecurity 与 OWASP CRS
更专业的做法是上 ModSecurity + OWASP Core Rule Set。OWASP CRS 包含几千条 SQL 注入、XSS、命令注入特征规则,覆盖度比手写正则强得多。安装:
sudo apt-get install libmodsecurity3 nginx-module-modsecurity
# 下载 OWASP CRS
git clone https://github.com/coreruleset/coreruleset /etc/nginx/modsec/
# 在 nginx.conf 启用云 WAF
阿里云、腾讯云、Cloudflare、AWS WAF 都提供 SQL 注入防护规则集。中小站点上云 WAF 是最省心的方案,每月几十到几百块。
密码哈希加固
即便修了 pm.php 的注入,DedeCMS 的密码哈希(MD5)本身已经不安全。MD5 单次哈希加 ec_salt 在 GPU 字典攻击下几小时就能破解。建议:
升级到 password_hash
PHP 5.5+ 提供 password_hash() 与 password_verify(),默认用 bcrypt。改造 DedeCMS 用户验证:
// 用户注册时
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
$dsql->ExecuteNoneQuery("INSERT INTO #@__member (userid, pwd) VALUES ('$userid', '$hash')");
// 用户登录时
$row = $dsql->GetOne("SELECT pwd FROM #@__member WHERE userid='$userid'");
if (password_verify($password, $row['pwd'])) {
// 登录成功
}cost 参数 12 让 bcrypt 在现代 GPU 上单次哈希耗时约 250ms,字典攻击成本陡增。
历史数据迁移
DedeCMS 库里已有几千上万个 MD5 密码不能直接转成 bcrypt(不知道明文)。处理:
- 登录时如果检测到 MD5 格式,校验通过后立刻用 bcrypt 重哈希一次写回。
- 对长期不登录的账号发邮件强制重置密码。
- 给所有账号默认开启二步验证(2FA)。
被脱库后的应急响应
检测被脱库的迹象
- nginx access.log 出现大量带 SQL 关键字的请求。
- MySQL 慢查询日志记录到长查询(attacker 可能在做盲注探测)。
- 用户反馈邮箱密码在其它站被试出(数据被打包出售)。
- HaveIBeenPwned 等数据泄漏数据库出现你站点。
应急动作
- 立刻把站点切到维护页,修复漏洞。
- 强制所有用户改密码(清空 dede_member.pwd 字段或下发邮件链接)。
- review 后台管理员账号的最近活动,重置 root 密码。
- review 数据库表是否被增删(dede_admin 表是否多了未授权账号)。
- 通知用户已发生泄漏(GDPR 与个保法要求 72 小时内通知)。
- 向网信办或公安备案上报(数据规模大的话)。
溯源调查
查 access.log 找首个成功注入的请求时间点:
grep -i "union\|select.*from\|sleep(" /var/log/nginx/access.log | head -50看 IP 地理位置(whois、ipinfo.io)、User-Agent 特征(sqlmap、Havij 等工具有特征 UA)、之后的访问行为模式。完整的攻击链通常是:探测注入 → 拿数据库结构 → 拿用户表 → 离线破解密码哈希 → 登录后台 → 上传 webshell。
迁移到现代 PHP 生态
用 ORM 替代手写 SQL
Doctrine ORM、Eloquent(Laravel 的 ORM)从设计上就是参数化查询,不可能写出 SQL 注入:
// Eloquent 写法
$pm = MemberPM::where('id', $id)
->where(function($q) use ($currentUserId) {
$q->where('fromid', $currentUserId)
->orWhere('toid', $currentUserId);
})
->first();$id 不管是什么内容,永远作为参数绑定到查询,不会被解释为 SQL。
用 Symfony Form 验证用户输入
Symfony Form 组件提供「类型 + 约束」声明式验证:
$builder
->add('id', IntegerType::class, [
'constraints' => [new Assert\Positive()]
]);Form 验证失败的请求根本到不了 controller,更不会触发 SQL 拼接。
升级到 DedeBIZ 或迁移到 Typecho
DedeBIZ 是 DedeCMS 的商业延续版,在原版基础上做了大量安全修复。Typecho 是国产轻量博客系统,代码质量与现代 PHP 实践都更优。如果你的站点不依赖 DedeCMS 特定功能(自定义模型、采集),迁移过去能彻底消除历史漏洞包袱。
常见故障
故障 1:修复后用户无法访问私信
intval 把非数字 id 转成 0,原本应该抛错的请求被当成「查询 id=0 的私信」返回空结果。改进:先校验是否为合法整数:
if (!is_numeric($id) || intval($id) <= 0) {
ShowMsg('无效的私信 ID', '-1');
exit();
}
$id = intval($id);故障 2:升级后补丁丢失
DedeCMS 升级会覆盖 member/pm.php。建议把所有补丁单独维护一份 patches/ 目录,每次升级后用 patch 命令重新应用:
diff -u original/pm.php patched/pm.php > patches/pm.php.patch
# 升级后
patch /var/www/dedecms/member/pm.php < patches/pm.php.patch故障 3:WAF 规则误拦合法请求
关键词如 select、union 在合法 URL 中也可能出现(比如 ?action=select&item=union_pay)。WAF 规则要精确到「带 SQL 语义的组合」而不是单关键词。先开 log-only 模式观察 1-2 周再启用拦截。
故障 4:参数化查询性能下降
PDO 预编译有缓存机制,第一次执行慢,后续快。如果你看到性能变差,检查是否每次都新建 PDO 连接(连接池缺失)。改用持久连接:
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_PERSISTENT => true]);故障 5:审计扫描出大量误报
grep 正则会把字符串字面量里的 SELECT 也算上。结合 AST 分析工具(PHP-Parser、psalm)能更精确:psalm 静态分析 DedeCMS 代码能找出真正的 SQL 注入点。
常见问题解答
修了 pm.php 是不是 DedeCMS 就安全了?
远远不够。pm.php 只是一个公开的漏洞点,DedeCMS 还有几十个未公开或低危的注入位置。完整安全靠「所有用户输入都过滤 + WAF 兜底 + 密码哈希升级 + 后台目录与权限分离」组合。
intval 修复对 base64 编码的 id 还有效吗?
有效。intval("MQ==" base64 解码前) 取数字部分得 0。但攻击者也可能用其它编码绕过。建议先检查格式(is_numeric 或正则)再 intval。
DedeCMS 官方还在修漏洞吗?
原版 DedeCMS 自 2020 年起官方维护停滞。新发现的漏洞官方不会修,社区有人维护非官方补丁但更新慢。建议主动升级到 DedeBIZ 或迁移到 Typecho/WordPress。
WAF 与代码层修复哪个更重要?
代码层是根本,WAF 是兜底。只用 WAF 不修代码:WAF 规则总有空子(编码绕过、时序攻击)。只修代码不用 WAF:未来出现新漏洞前没有兜底防线。两者都做最稳。
升级 PHP 到 8.x 后 DedeCMS 报错怎么办?
DedeCMS 5.7 在 PHP 8.x 下大量函数废弃错误。需要打 dedeCMS-php8 兼容补丁,或者降回 PHP 7.4。但 PHP 7.4 已经停止安全更新(2022 年起),长期不可持续。
能否直接禁用 pm.php?
能。如果你的会员中心不用站内私信功能,nginx 层直接 deny:
location = /member/pm.php { deny all; }这是最彻底的「关掉攻击面」做法。
如何检查站点是否已被注入?
看 dede_admin 表是否多了陌生账号;看 dede_member 表是否有 mid=1 的密码哈希被替换;用 wpscan 类工具扫描站点指纹;用 phpcheck 扫 webshell 特征。还可以对比 mysqldump 备份与现状的差异。
DedeCMS 有没有像 WordPress Wordfence 那样的安全插件?
没有官方维护的。社区有几个第三方安全插件但更新滞后。目前最务实的做法是 ModSecurity + 云 WAF + 自己审计代码。
升级补丁后怎么验证修复有效?
用 sqlmap 工具针对该 URL 测一次:sqlmap -u "https://example.com/member/pm.php?dopost=read&id=1" --cookie="PHPSESSID=xxx" --level=5 --risk=3。修复有效的话 sqlmap 报「not injectable」。
除了 pm.php 还有哪些 DedeCMS 已知 SQL 注入漏洞?
CVE 数据库搜「DedeCMS」能查到几十个。最常见的还有 plus/recommend.php、plus/carbuyaction.php、plus/search.php、include/dialog/select_soft.php。打全套补丁参考社区维护的 dedecms-vulnerability-patches 项目。
本文标题:《DedeCMS pm.php SQL 注入修复完整指南:CVE-2018-9134、参数化查询、密码哈希升级与应急响应》
本文链接:https://zhangwenbao.com/dedecms-membership-center-pm-php-injection-vulnerability-repair-method.html
版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0