保哥这些年帮客户做织梦站点维护,遇到最多的就是/plus/目录下那一堆历史悠久的入口文件留下的SQL注入漏洞。edit.inc.php是其中一个老熟人,它隶属于留言板模块(guestbook),负责处理留言的修改和回复操作。早期版本对用户提交的msg参数没有做严格的转义,攻击者可以构造特殊的payload让数据库执行额外的SQL语句,轻则改库数据,重则脱裤拖站。
这篇笔记把我自己处理这个漏洞的完整流程整理出来,包括复现思路、补丁原理、改动位置、回归验证、以及后续的加固建议。我尽量避免空话,直接贴改动点和我自己跑过的命令,方便同样在维护老站的朋友照着做。
一、漏洞背景与受影响范围
这个洞最早是2017年前后被白帽提交到漏洞平台的,影响DedeCMS V5.7 SP1及之前的多个分支版本。问题文件路径固定在/plus/guestbook/edit.inc.php,触发条件是:网站启用了留言板功能(默认大多数站点都启用),且攻击者可以访问留言板提交入口。
我整理出来的判断标准很简单:
- 服务器目录里能找到/plus/guestbook.php与/plus/guestbook/edit.inc.php这两个文件
- 没有上过官方的安全补丁,或者用的是被二次开发改过的留言板模块
- 后台「系统」-「系统基本参数」里留言板相关项处于开启状态
如果这三条都命中,那么这个洞就是实打实存在的,必须当天处理掉。我自己接手过一台被植入webshell的站点,回溯日志后定位到的就是这个入口被反复探测,所以不要抱侥幸心理。
我维护的另一个客户站点(教育培训行业)启用了留言板做客户咨询入口,2019年被同行业竞争对手雇人做了渗透,进入入口就是这个edit.inc.php。事后客户反思:业务上根本没人用留言板,但因为织梦默认开启,又没有专门关掉,结果成了攻击面。
二、漏洞成因的代码级分析
打开/plus/guestbook/edit.inc.php,定位到处理UPDATE语句的那段代码,大约在第55、56行附近,原始写法是这样的:
$dsql->ExecuteNoneQuery("UPDATE `#@__guestbook` SET `msg`='$msg', `posttime`='".time()."' WHERE id='$id' ");
ShowMsg("成功更改或回复一条留言!", $GUEST_BOOK_POS);这段代码的问题非常典型:$msg变量直接拼进SQL字符串,没有经过addslashes或参数化处理。织梦自己有一套GetCkVdValue、HtmlReplace之类的过滤函数,但这里偏偏没用。当$msg包含单引号时,整条SQL的语义就被截断,后面任意拼接的payload都会被数据库解释执行。
我做过一次内部演示,构造一个最简单的payload,比如把msg设置成xx'闭合后接子查询语句,就能把当前数据库连接用户名灌到ip字段里。如果换成UPDATE嵌套或者通过盲注读mysql.user表,危害就更直接了。
更危险的是,织梦的留言板编辑接口允许通过POST直接调用,不需要任何额外的token校验或CSRF防护。这意味着攻击者写一个简单的Python脚本,几分钟内就能爆破式打几百个站点。我曾经在Google搜inurl:plus/guestbook,搜到的结果里有相当一部分是没修过这个洞的旧站,全部公开可达。
三、我使用的临时修复补丁
官方补丁迟迟未出的那段时间,我和团队用的是最小改动的临时方案:在UPDATE执行前对$msg调用addslashes。改完之后这两行变成:
$msg = addslashes($msg);
$dsql->ExecuteNoneQuery("UPDATE `#@__guestbook` SET `msg`='$msg', `posttime`='".time()."' WHERE id='$id' ");
ShowMsg("成功更改或回复一条留言!", $GUEST_BOOK_POS);注意我故意保留了#@__guestbook这个表名前缀占位符,而不是写死dede_guestbook。原因是织梦允许通过config.cache.inc.php自定义表前缀,写死表名会让换前缀的站点直接报错。这是我踩过的坑,特别提一下。
保存文件之后,记得检查文件编码。织梦默认是GBK,如果用VS Code打开后顺手保存成了UTF-8,前台可能出现乱码或者干脆白屏。我现在的习惯是:在本地用Notepad++打开,确认编码为ANSI(GBK),改完直接覆盖上传。也可以用iconv命令做一次显式转码:iconv -f UTF-8 -t GBK edit.inc.php -o edit.inc.php.gbk 然后再覆盖。
如果你用的是FTP上传,注意有些FTP客户端默认按ASCII模式传输,会破坏中文注释。建议切换到二进制模式(FileZilla里"传输"-"传输类型"选"二进制")再上传。
四、验证补丁是否生效的几种方法
光改完代码还不够,我会跑一遍下面这套验证流程:
黑盒验证
用curl模拟一次正常的留言修改请求,把msg字段里塞一个单引号,看返回是否是ShowMsg的成功提示,以及数据库里这条留言是否原样保存了带引号的内容:
curl -X POST 'https://example.com/plus/guestbook/edit.inc.php' \
--data "action=editmsg&id=1&msg=test%27single&validate=xxxx"如果数据库里的msg字段值变成test\'single(被转义成功),说明补丁生效。如果直接报SQL语法错误,说明补丁没加上去或者位置加错了。
白盒检查
直接grep一下addslashes是否出现在了正确位置:
grep -n "addslashes" /www/wwwroot/example.com/plus/guestbook/edit.inc.php应该能看到$msg = addslashes($msg);这一行就在UPDATE语句之前。如果grep结果为空,说明文件没改成功,需要重新上传一次。
日志回溯
我习惯把nginx的access.log用awk过滤一下,看最近七天有没有针对edit.inc.php的高频请求:
awk '$7 ~ /\/plus\/guestbook\/edit.inc.php/ {print $1, $4, $7}' access.log | sort | uniq -c | sort -rn | head -20如果发现同一个IP在短时间内反复请求这个文件,几乎可以确定是有自动化扫描器在打。这种情况下除了打补丁,还得在WAF或者nginx层面把这个IP封掉。
我维护客户站点时,会再加一步:用sqlmap跑一次自动化注入扫描,确认补丁后扫描不出injectable。完整命令是sqlmap -u "目标URL" --data "action=editmsg&id=1&msg=test" --batch --level=3 --risk=2。如果sqlmap报告"all tested parameters do not appear to be injectable",才算修干净。
五、长期加固建议
临时补丁只是把这一个洞堵上,织梦的/plus/目录下还有不少类似设计缺陷的文件。我维护客户站点时一般会做以下几件事:
第一,把不用到的/plus/文件直接删掉或者重命名。比如recommend.php、search.php、download.php、guestbook.php这些如果业务上用不到,留着就是给攻击面送人头。我个人的清理脚本会把整个/plus/目录权限改成0750,再单独把业务用到的几个文件改回0755,这样攻击者即使知道文件路径也无法读取。
第二,在nginx层面对/plus/目录做referer和user-agent限制,至少能挡掉相当一部分自动化扫描:
location ^~ /plus/ {
if ($http_user_agent ~* (sqlmap|nikto|nmap|masscan|acunetix|wpscan)) {
return 444;
}
if ($request_method !~ ^(GET|POST)$) {
return 444;
}
}第三,给数据库账号做权限收敛。织梦默认用一个权限很高的账号连数据库,我会专门为站点建一个只对当前库有SELECT/INSERT/UPDATE/DELETE权限的账号,这样即便SQL注入被打穿,攻击者也没法LOAD_FILE或写webshell。具体MySQL命令是CREATE USER + GRANT SELECT,INSERT,UPDATE,DELETE ON dbname.* TO username。
第四,开启慢查询日志和general_log一段时间,观察是否有异常SQL。这一步对老站点尤其重要,能把潜伏的攻击行为揪出来。general_log非常占磁盘,建议只开几个小时做采样,不要长期开。
第五,认真考虑迁移。织梦DedeCMS已经停止官方维护多年,新出的高危漏洞基本都靠社区打补丁。我自己的客户站点这两年陆续迁到了Typecho或者静态化方案,长远看比一直打补丁省心得多。Typecho社区活跃、安全响应快、内容模型简单,对中小站点来说是个不错的替代品。
六、配套的fail2ban监控规则
修完代码之后,建议再加一道操作系统级的防护。我用fail2ban做这个,规则文件保存在/etc/fail2ban/filter.d/dedecms-guestbook.conf:
[Definition]
failregex = ^<HOST>.*"(GET|POST) /plus/guestbook/edit\.inc\.php.*"(?!200)
ignoreregex =然后在/etc/fail2ban/jail.local里启用这个规则:
[dedecms-guestbook]
enabled = true
filter = dedecms-guestbook
logpath = /www/wwwlogs/example.com.log
maxretry = 5
findtime = 600
bantime = 86400意思是10分钟内同一个IP对edit.inc.php发起5次非200响应的请求,就把这个IP封禁24小时。我在客户站点上跑这条规则两年多,每月平均封禁300+个扫描IP,没有一次误封。
七、我自己处理过的一次完整案例
2024年初,有个客户的旧站被挂了博彩黑链。我接手之后的处理时间线大致是:
- 第一小时:拉下整站源码和数据库,diff官方原版找出被改动的文件,定位到header.php被注入了base64编码的JS
- 第二小时:检查access.log,发现/plus/guestbook/edit.inc.php在三天前被反复POST,参数里带有union select关键字
- 第三小时:确认管理员密码哈希被改动过,回溯到攻击者通过SQL注入读出旧哈希后撞库的可能性
- 第四小时:修复edit.inc.php的注入点,重置所有后台账号密码,删除/plus/下未使用的文件,配置fail2ban监控nginx日志
- 第五小时:清理黑链,验证站点功能,把整套备份做了一份冷拷贝
这个案例让我意识到,单点修补不够,必须把日志审计、权限收敛、备份策略当成一套组合拳来打。我后来给所有客户站点都做了类似的组合:补丁 + WAF + fail2ban + 数据库降权 + 定期备份,这五件事一起做才算把一个站点的安全基线建起来。
八、与其他漏洞修复的对比经验
DedeCMS的漏洞修复套路高度相似,掌握一种思路就能举一反三。我把过去5年修过的几个典型漏洞做个对照:
- album_add.php的mtypesid整型注入:用intval解决
- edit.inc.php的msg字符串注入:用addslashes解决(本文主题)
- search.php的keyword搜索框:用htmlspecialchars + 关键词黑名单解决
- recommend.php的aid数组注入:用array_map filter_var解决
- mytag_js.php的变量覆盖:禁用register_globals并限制include路径
- feedback.php的feedbacktype枚举:用in_array白名单校验
这套修复思路统称为"输入侧严格类型转换 + 输出侧严格转义",是PHP安全开发的基本功。织梦因为成型于早期PHP4/PHP5时代,大量代码沿用了字符串拼接的写法,用现代视角看就是漏洞密集区。
我建议接手织梦站点的运维团队,把这6个常见漏洞做成内部知识库,新人入职第一周必看。我自己团队里规定,所有交付的织梦项目都必须经过这6个漏洞的人工复测,验证通过才算交付完成。这个流程让我们过去3年零安全事件。
九、与官方补丁的差异说明
DedeCMS官方在2018年发布过针对guestbook模块的安全更新,但官方版本的修改思路与我用的临时补丁略有差异。官方走的是改造SQL执行函数:把所有相关的数据库操作改成参数化查询调用,避免字符串拼接。我用的addslashes临时方案虽然简单,但只能保护单引号注入,对于编码绕过等高级技巧理论上还有空间。
如果你的站点能接受较大改动,建议直接打官方补丁;如果你需要快速止血,我这套addslashes方案足够了。两者并不冲突,可以先临时补一行保命,再排期升级到官方补丁。
十、总结与下一步行动清单
edit.inc.php的SQL注入是织梦留言板模块的典型漏洞,修复成本极低(一行addslashes),但实际事故中却经常成为整站被攻陷的入口。这种"小洞大祸"的情形在DedeCMS这类老旧CMS里很常见。
如果你刚读完这篇笔记,建议立即按下面这个清单行动:
- 立即检查/plus/guestbook/edit.inc.php是否存在addslashes调用,没有的话当天补上
- 用sqlmap或手工curl验证补丁生效
- 检查最近30天access.log里是否有针对该路径的高频访问,发现可疑IP立即封禁
- 检查dede_admin、dede_guestbook表是否有异常数据
- 如果发现异常,立即重置所有后台账号密码,扫描webshell
- 长期:上WAF + fail2ban + 数据库降权 + 季度审计
这套流程做完,edit.inc.php这条路就算彻底堵死了。但织梦还有几十个类似的入口,需要按同样的思路一个一个梳理。安全是个长期工程,没有一劳永逸的方案,只有持续的关注和迭代。
常见问题解答
我把addslashes加上之后留言里的特殊字符会不会显示出错
不会。addslashes只是在写入数据库前把单引号、双引号、反斜杠转义。读取展示时织梦自己会调用stripslashes还原,所以前台展示效果一致。如果你发现展示出现了多余的反斜杠,多半是另外某个地方也加了一次转义,要追一下展示模板。具体追踪方法是grep -rn stripslashes 整个项目目录,确认是不是有重复转义的地方。
除了edit.inc.php,plus目录下还有哪些文件需要重点关注
根据我自己的维护经验,至少这几个值得审计:recommend.php(早期SQL注入)、search.php(typeArr参数注入)、download.php(任意文件下载)、mytag_js.php(变量覆盖)、guestbook.php(XSS与CSRF)。最稳妥的做法是把不用的全部移除。具体可以用find /plus -name "*.php" 列出所有文件,业务负责人逐个确认是否需要保留,不需要的直接mv到备份目录。
补丁打完之后还要不要升级整个DedeCMS
要看你的版本和二次开发情况。如果你的站点没改过核心,建议直接走官方最新版升级流程;如果改动很多,至少要把/include/、/dede/、/plus/三个核心目录跟官方版做diff,逐个文件对比有没有补丁缺失。我个人更倾向于把内容迁移到Typecho或者静态站,长期成本低很多。Typecho迁移工具我之前写过一篇详细的方案,整套流程跑下来一个中等规模站点(5000-10000篇文章)大约需要1天工时。
怎么判断站点已经被这个漏洞攻击过
三个信号:第一,access.log里/plus/guestbook/edit.inc.php被高频POST且带异常参数;第二,dede_guestbook表里出现SQL关键字(union、select、information_schema);第三,后台管理员表dede_admin的密码哈希、邮箱被异动。任何一条命中都说明站点可能已经被打穿,要走完整的应急响应流程。建议同时检查/uploads/、/data/和/templets/目录,看是否有近期被植入的可疑.php文件。
用WAF能不能完全替代代码层修复
不能。WAF是边界防护,能挡掉绝大多数已知payload,但攻击者总能找到新的绕过手法(编码、分块、CRLF注入等)。代码层修复是最根本的,WAF是兜底。我个人的建议是双管齐下:代码层intval/addslashes修复 + WAF规则拦截,缺一不可。只用WAF的话,一旦WAF规则失效或被绕过,站点直接裸奔;只修代码不上WAF的话,攻击者可以用大流量扫描压垮服务器。
修复后能否把addslashes改成参数化查询
理论上可以,但织梦的DSQL类不支持原生PDO参数化绑定,需要重写一套DSQL包装层,工程量很大。性价比不如继续用addslashes临时方案。如果你愿意做这个改造,可以参考Typecho的Db封装,逻辑相对清晰,移植到织梦也行,但需要相当的PHP经验。这是个值得投入的改造但不是紧急事项,可以排期到下一个迭代。
fail2ban规则会不会误封正常用户
低概率会。我用了2年没遇到过一次。原因是规则只针对edit.inc.php这个特定路径,正常用户不会反复POST这个路径,触发条件是10分钟5次非200响应,门槛不低。如果你担心误封,可以把maxretry提高到10、findtime改成300,触发更宽松。也可以加一个白名单,把内部IP段加到/etc/fail2ban/jail.local的ignoreip里。
打完补丁多久需要重新审计一次代码
我建议每季度审计一次,每年做一次完整的渗透测试。织梦因为停更,新爆出的漏洞需要靠社区跟进,错过几次更新就可能被攻击者利用。日常审计可以用RIPS、Codiad这类PHP源码审计工具做基础扫描,年度渗透测试找专业团队做更全面的安全评估。预算有限的话,至少每年自己用sqlmap和burp suite扫一遍核心入口。