如果你手头还维护着一套 DedeCMS 站点,这几年你多半已经养成了一个习惯:隔一阵就去翻一遍官方公告,看看有没有新的安全更新。CNVD-2018-0109 这个编号,在老 DedeCMS 圈子里基本是绕不开的一个存在——前台任意用户密码修改漏洞。它厉害的地方在于:攻击者不需要进入后台,也不需要拿到数据库,只需要知道目标会员的用户名,就有机会直接在前台把这个账号的密码改掉。如果站长的管理员账号恰好也能从前台登录,那后果就不只是会员数据被篡改,而是整站权限都可能拱手让人。
我在前前后后处理这个漏洞的过程中,见过不少因为修复不及时被刷成垃圾站、被挂上跳转马、甚至整站被删的例子,也踩过一些修复后功能反而异常的坑。这篇文章会把整个修复流程、代码层面的处理思路、以及实际排错过程中的记录完整整理出来,给还在维护老 DedeCMS 的朋友做参考,尤其适合那些代码已经改过很多次、没法直接替换官方整包的站点。
1. 漏洞影响与成因分析
1.1 这个漏洞到底会造成什么后果
先把这个漏洞的影响范围说清楚。CNVD-2018-0109 属于典型的前台逻辑漏洞,主要影响 DedeCMS 5.7 SP1 及之前的较老版本。这类版本在大量老站里非常常见,很多站点从上线到现在就没升过级,会员系统、投稿功能都还在运行,正好处于漏洞射程以内。
漏洞的现实危害可以这么理解:一套会员系统相当于一栋楼里的门禁,正常流程是住户通过钥匙或门禁卡进出,忘记密码时可以找物业重新登记。而这个漏洞相当于门禁系统里多了一个隐蔽的后门——外人只要知道某个住户的房号,就能直接把这个住户的门锁密码换掉。用在网站上就是:攻击者只要知道目标用户名,或者能猜到用户的 ID,就有机会提交一个精心构造的请求,把该用户的密码重置成自己指定的值。
这里有一个很多人会忽略的细节:DedeCMS 很多站点的管理员账号,是在前台会员表里同样存在的。也就是说管理员既能进后台,也能走前台会员入口。一旦这类账号被改掉密码,攻击者再顺着后台登录入口进去,整站就彻底失守了。所以这个漏洞影响的绝对不止普通会员,站长账号一样在风险范围内。
1.2 密码重置流程为什么会被绕过
要理解漏洞成因,得先看 DedeCMS 正常的密码找回流程。常规情况下,用户在前台点击“忘记密码”,提交自己的用户名或邮箱,系统生成一个临时链接发到邮箱里,用户点击链接进入设置新密码的页面,输入新密码完成重置。这个流程本身是没问题的,问题出在“操作者身份”的校验不够严格。
漏洞主要集中在 member 目录下的密码重置相关文件里。整套流程在执行过程中,有一部分请求参数是直接从前台提交上来的,比如用户 ID、操作类型、临时凭证等。如果系统在某个环节没有对这类参数做严格校验,攻击者就有机会通过替换参数值,绕过邮箱验证这个关键环节,直接跳到修改密码的步骤。再加上早期版本中临时凭证的生成规则相对固定,一旦被分析出规律,整个重置流程就基本等于裸奔了。
从数据处理层面深挖,问题通常出在两个方面:一是校验顺序不对,某些关键判断放在密码更新之后执行,等于先操作后验证;二是对输入参数的过滤不足,用户 ID 这类敏感值没有做强制类型转换和归属校验。修复时必须同时处理这两点,否则只堵住其中一个入口,另一个方向仍然可能被绕过。
2. 动手修复前,先把准备工作做扎实
2.1 确认站点代码版本和漏洞命中情况
这里得先说一句:修复之前,必须确认你手头 DedeCMS 的具体版本和文件结构。同一套修复代码在不同版本上表现差异很大,直接照搬网上的片段,轻则功能异常,重则把会员系统改废掉。
确认版本有几种常用方式:
| 方式 | 操作路径 | 说明 |
|---|---|---|
| 后台版本显示 | 后台首页或“系统”菜单下的版本信息 | 最直观,能看到 V57_GBK 之类的版本标识 |
| 版本常量文件 | 查看 /include/version.inc.php | 里面定义了 DEDEVERSION,能精确到具体修订版本 |
| 安装信息文件 | 查看 /data/admin/ver.txt | 部分版本安装时会写入版本号 |
| 源码特征对比 | grep “resetpassword” 等关键词 | 通过 member 目录下文件内容辅助判断版本区间 |
在确认版本的同时,顺手看一下 member 目录下的文件列表。重点检查 resetpassword.php 是否存在、文件大小、最后修改时间。如果这个文件的修改时间非常老,而且一直没有备份记录,那基本可以判定漏洞处于未修复状态,需要立即处理。
2.2 备份、环境盘点与本地复现环境搭建
接下来做备份。很多站长觉得“改个文件而已,不用备份”,但实际操作中,改坏一个函数导致整站白屏的情况太常见了。我处理过的案例里,至少有三分之一是在修复过程中引入了新的问题。所以备份这一环无论如何不能跳。
备份分三层:
- 全站代码备份:最简单的做法是把网站根目录完整打包下载,压缩后在本地留存一份。如果服务器空间紧张,至少要打包 member 目录和 include 目录。
- 数据库备份:用 DedeCMS 后台自带的“系统 - 数据库备份/还原”功能,或者直接用 mysqldump 把整库导出。注意数据库备份完成后,把备份文件下载到本地,不要把 SQL 文件长期留在网站目录里。
- 配置信息记录:把当前使用的 PHP 版本、Web 服务器类型、伪静态规则、邮件发送方式记录下来。这些信息在后期排查修复后遗症时非常有用。
备份完成以后,强烈建议在本地或者一台测试服务器上先做一遍修复演练。DedeCMS 这种老系统,生产环境里往往叠加了无数二次开发和模板改动,直接在线上改代码风险极高。测试环境里模拟一次完整的修复流程,确认会员注册、登录、找回密码都正常后,再把修复步骤搬到生产环境。
3. 手动修复核心代码的完整步骤
3.1 修复密码重置主流程:member/resetpassword.php
这个文件是整个漏洞的核心战场,修复的重点是给密码重置流程加上“一次性临时凭证”机制,并在每个关键节点都重新校验操作者身份。
先说修复思路。正规的密码重置流程应该拆成三个独立步骤:第一步是用户提交找回请求,系统生成一次性 token 并发送到用户邮箱;第二步是用户点击邮件中的链接,携带 token 访问重置页面,系统校验 token 的合法性;第三步是用户提交新密码,系统二次确认 token 和用户身份一致后,才真正执行密码更新操作。漏洞版本的问题在于,第二步和第三步之间存在逻辑断层,攻击者可以绕过第二步直接提交第三步。
以下是一个修复后的流程示意代码,供你结合自己站点的实际版本调整:
// 第一步:用户提交找回密码申请 if ($dopost == 'send') { $username = trim($_POST['username']); // 根据用户名查询会员信息,确认账号存在 // 生成一次性 token,与账号绑定并写入数据库(或缓存) $token = md5(uniqid(rand(), true)); // 将 token、用户ID、申请时间存入临时表或 session // 构造重置链接:/member/resetpassword.php?dopost=getpasswd&token=$token // 通过邮件发送给用户 exit(); } // 第二步:用户从邮件点击链接进入 if ($dopost == 'getpasswd') { $token = preg_replace('/[^a-z0-9]/i', '', $_GET['token']); // 查询临时凭证记录,校验 token 是否存在、是否过期 // 校验通过后将用户 ID 写入 session,并显示设置新密码的表单 if (!$valid) { ShowMsg('链接无效或已过期,请重新申请', 'resetpassword.php'); exit(); } } // 第三步:提交新密码 if ($dopost == 'resetpwd') { // 二次校验:表单中的 token 必须与第一步生成的 token 一致 $token = isset($_POST['token']) ? trim($_POST['token']) : ''; $sessionToken = isset($_SESSION['reset_token']) ? $_SESSION['reset_token'] : ''; if ($token == '' || $token !== $sessionToken) { ShowMsg('安全校验失败,请重新申请', 'resetpassword.php'); exit(); } // 强制转换用户 ID 为整型,防止参数注入 $uid = intval($uid); // 校验通过后,才允许执行 UPDATE 密码操作 $pwd = md5(trim($_POST['pwd'])); $dsql->ExecuteNoneQuery("UPDATE `#@__member` SET `pwd`='{$pwd}' WHERE `id`={$uid}"); }这段代码不是让你原样复制的,不同版本的 DedeCMS 在变量命名和函数调用上有很大差异。核心要把握三个修改原则:第一,重置密码前必须有一次性 token 校验;第二,token 必须绑定到具体用户,不能是全局通用凭证;第三,用户 ID 参数必须做 intval 强制转换并和后端查询结果做比对,避免直接使用前台提交的 UID 去执行 SQL 更新。
我在实际修复中还遇到过一种情况,有些二次开发版本会直接跳转到/member/index.php?action=resetpwd&uid=某个数字这样的地址,整个流程没有任何凭证校验。这种情况非常危险,等于把修改密码的接口完全暴露给外界。遇到类似代码,必须把参数传递方式改成“仅从 session 或临时记录中读取用户 ID”,完全忽略 GET/POST 提交的 uid 参数。
3.2 处理其他前台会员入口并收紧全局校验
修完主流程文件,还差一步收尾工作。member 目录下还有其他前台入口文件,比如会员中心首页、投稿处理、资料修改等模块,在修复过程中也需要顺手加固。主要做以下几个动作:
- 检查 member 目录下所有 PHP 文件,看是否存在类似的“参数驱动操作”逻辑,尤其是涉及 UPDATE、DELETE、INSERT 语句的位置。凡是直接从前台接收用户 ID 并执行数据库更新操作的,都要改成基于 session 来判断当前登录用户。
- 全局变量过滤。DedeCMS 的 common.inc.php 里有全局变量注册相关的机制,不同版本处理方式不同。如果你的版本还保留了比较宽松的变量注册方式,建议结合站点实际情况收紧密集,避免变量覆盖类攻击和参数注入同时发生。
- 如果站点实际上并没有开放会员注册或投稿功能,最省事的做法是直接在后台关闭会员模块,并在 Web 服务器层面禁止外网访问 member 目录。暴露面越少,被利用的概率越低。
这轮收尾做完,基本能保证漏洞入口被有效封堵。
3.3 配置层面的访问控制与权限收紧
代码层面的修复到位之后,我建议再往上叠一层配置加固,这样即使以后又出现类似逻辑漏洞,攻击者也要先过配置这一关。
首先是文件目录权限。member 目录本身只需要读取和执行权限,不应该允许 PHP 写入,建议设置成 755(属主可写,其他用户只读),底层文件属主和 Web 运行用户如果不是同一个,还需要仔细确认两者关系。上传目录、data 目录、cache 目录这类需要写权限的地方,要设定严格的目录白名单,禁止跨目录执行脚本。
其次是 PHP 运行环境。在 php.ini 或站点配置中启用 open_basedir,把 PHP 可读取的目录限制在网站根目录范围内;同时根据实际情况,考虑禁用部分危险函数,比如 eval、assert、system、exec 等。DedeCMS 某些功能可能依赖这些函数,建议先在测试环境验证,确认无影响后再上线,避免因为配置过死导致站点功能崩溃。
再就是数据库账号权限。给 DedeCMS 单独创建数据库账号,并只授予当前数据库的 SELECT、INSERT、UPDATE、DELETE 权限,不要给 FILE、PROCESS、SUPER 等高权限。即便某个 PHP 文件被拿下,攻击者也很难利用数据库账号去读取服务器文件或提权。
4. 官方补丁安装与长期安全运维
4.1 官方升级包和补丁的正确安装方式
代码手动修复是最直接的方案,但如果你手头的版本还没有经过大规模二次开发,优先考虑安装官方补丁会更省事。DedeCMS 官方针对这一漏洞发布过安全更新,补丁会同步修改多个受影响文件,比手动逐个文件修复覆盖得更全面。
补丁安装的流程要注意几个步骤:
- 先去 DedeCMS 官网或官方安全公告页面,找到对应你当前版本的安全补丁下载链接。注意区分 GBK 版本和 UTF-8 版本,编码搞错了文件直接乱码。
- 下载完补丁先不要急着传生产环境,放到测试站点解压,对比一下补丁文件是否和你当前修改过的文件存在冲突。如果你的站点之前已经改动过同路径文件,需要手动合并,直接覆盖会丢掉原有功能。
- 测试环境确认无误后,在生产环境进行操作。操作前再次确认整站代码和数据库已备份。按补丁说明覆盖到对应目录,然后登录后台执行“系统 - 数据备份/还原 - 更新缓存”,或者直接删除 data/cache 下的缓存文件,强制系统重建缓存。
- 升级完成后,第一时间验证会员注册、登录、找回密码流程是否正常。务必用真实邮箱走一遍完整的找回密码流程,确认邮件能收到、链接能用、密码能改成功。
这里必须强调一点:如果站点已经被二次开发过很多次,官方补丁很可能覆盖不进去,甚至部分补丁默认基于原版代码制作,和你改过的文件结构对不上。这种情况下反而建议直接走上一章的手动修复方案,起码改动范围和影响面是可控的。
4.2 一套长期有效的 DedeCMS 安全加固清单
修复完一个漏洞不代表以后就安全了。以我维护 DedeCMS 站点的经验,只要站点还在用这套老系统,安全加固就是一个需要持续跟进的长期事项。下面这套清单是我每处理完一次安全事件都会顺手过一遍的,建议你也留着:
- 修改后台登录路径。把 admin 目录名改成一段随机字符串,降低被批量扫描工具直接命中的概率。
- 修改数据库表前缀和数据库账号密码。DedeCMS 默认前缀是 dede_,扫描工具会优先尝试用默认前缀构造 SQL 注入,改掉前缀等于多一层防护。
- 删除 install 目录。很多 DedeCMS 站点安装完成后没有删除安装脚本,攻击者可以直接重装系统覆盖管理员账号。
- 定期备份数据库和站点文件。备份文件不要放在网站目录内,否则一旦备份路径被猜到,等于把整站数据直接送给攻击者。
- 关注官方安全公告,尤其是 DedeCMS 官方停止更新后的安全预警信息。如果站点已经没有维护团队,尽量关闭投稿、会员注册等交互功能,最大程度减少攻击面。
这些操作听起来零碎,但每一项都能提升攻击者的利用成本。安全没有一劳永逸的解决方案,只有把每一层防护都做到位,才能把风险压到最低。
5. 实际操作中遇到的问题与排查记录
5.1 修复后会员忘记密码功能无法正常使用
这个是我处理过程中遇到最多的问题,也是修复后最常见的“后遗症”。具体症状表现为:用户提交找回密码后收不到邮件,或者收到邮件点击链接提示无效、已过期,甚至直接报 500 错误。
排查顺序一般是这样:
- 确认邮件是否真的发送成功。到服务器上检查邮件发送日志,不少 DedeCMS 站点用的是 SMTP 方式发送,如果 SMTP 账号密码配置失效、发信频率受限,邮件根本出不去。这一步能排除掉大部分故障。
- 如果邮件能收到但链接报错,重点检查 token 校验逻辑。手动修复时如果 token 的生成和校验用的不是同一套规则,或者 session 写入时机不对,就会出现“链接无效”的提示。常见错误是第一步生成 token 后没有正确写入 session,第二步校验时又读取了一个空的 session 变量。
- 检查 PHP 错误日志。把 display_errors 临时打开,看页面报错的具体文件和行号,能快速定位是语法错误还是函数不存在。定位完记得把 display_errors 关回去,避免线上暴露调试信息。
这种问题多是因为修复代码写得太“想当然”,没有充分考虑 DedeCMS 原有代码的调用方式。对照原版代码逐行比对,通常很快就能找到问题。
5.2 补丁升级后出现白屏或 500 错误
另一种常见情况是升级补丁后整站白屏。绝大多数原因是文件覆盖不完整,比如补丁包里包含 10 个文件,实际只上传了 8 个,或者上传时目录层级不对,文件根本没落到正确位置。
解决办法也不复杂:把补丁包里的文件按照官方说明逐一核对,确保每个文件都覆盖到了对应路径;然后清空缓存目录重新加载;如果仍然白屏,开启 PHP error_log 看具体报错信息。如果是因为兼容性问题导致的语法错误,就要考虑是不是补丁版本和你当前站点版本不匹配,需要去下载对应历史版本的兼容补丁。
5.3 日常日志监控与可疑行为识别
修复完成且各功能验证正常之后,别忘了把日志监控补上。很多时候漏洞利用并不是一次性动作,攻击者可能会在站点里留后门、添加隐藏管理员,等到合适时机再回来操作。
日常监控主要关注这几个方向:
- Web 访问日志里,member 目录相关的请求是否异常增多,尤其是 resetpassword.php 这个文件被反复请求的情况。
- 数据库里是否有新增的、典型管理员用户名的记录,会员表里是否出现大量非正常注册的账号。
- 网站根目录是否出现不认识的 PHP 文件,特别是文件名看起来像是随机字符串、内容又非常短的文件,这类文件多半是恶意脚本。
我之前给朋友站点做排查时,就是通过访问日志里发现某个 IP 短时间内连续请求了上百次 member 目录下的接口,才顺藤摸瓜找到了被写入的后门文件。有条件的话,可以写一个简单的脚本,每周扫描一次新增文件和最近被修改的文件列表,把常见的可疑特征过滤出来,这样即便出现新的问题,也能在早期发现并处理。
再分享一个我自己的小习惯:每次处理完这类漏洞修复,我都会顺手把整站文件备份打包一次,并在备份文件旁边写一个 TXT 说明,记录这次修复涉及的文件列表和修改日期。这样下次再出问题时,光看这份记录就能快速判断哪些文件被动过,省去大量重新排查的时间。对于 DedeCMS 这种老系统,这种“留痕”的习惯比任何安全工具都实在。