ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

老旧CMS的XSS与getshell风险防护及应急治理

老旧CMS的XSS与getshell风险防护及应急治理

项目中遇到过不少次类似的场面:群里忽然冒出一条消息,标题写得耸人听闻,核心关键词是XSS、0day、通杀所有版本,还带着一个"可 getshell"的尾巴,配图往往是半个命令行窗口。消息传得飞快,转发的人绝大多数并没有真的验证过,只是觉得"这事挺大"。作为长期在甲方做安全建设和应急的人,我对这类标题的第一反应从来不是"赶紧下载试试",而是三个问题:它描述的缺陷类型是什么、影响面到底有多大、我手上的资产有没有暴露在同样的风险结构里。这三个问题才是有价值的,而"怎么打进去"从来不是这篇文章要谈的内容。

需要把话说在前面:下面所有内容都围绕防护、检测、治理和应急展开,不涉及任何利用代码、注入字符串、绕过手法或入侵步骤。原因很简单,任何一篇公开的"通杀型"利用教程,最终受益的都不是防守方。真正需要被讲清楚的,是为什么一类老系统会反复出现同类问题、防守方如何快速判断自身暴露面、以及在无法立刻重写系统的现实约束下怎么把风险压下去。这篇文章适合三类人看:负责企业资产安全的运维和安服同学、正在维护老项目的开发者、以及对 Web 安全刚入门、容易被这类标题带偏方向的学习者。

1. "通杀所有版本"这句话到底在说什么

1.1 同一套代码骨架衍生出的共性缺陷

一款内容管理系统会被说成"通杀所有版本",通常不是因为它每个版本都被单独挖出了洞,而是因为它的代码骨架在多个版本之间几乎没变。老牌 CMS 有一个很典型的特征:核心的模板解析、参数接收、文件写入这几条链路,从早期版本一直沿用下来,中间只做加法不做减法。新版本加功能、改界面、修某些具体报错,但那几条老链路依然躺在那里,甚至连函数名都没改过。

这就导致一个结果:只要某个位置存在输入未过滤、输出未编码的问题,那么所有继承这套骨架的版本都会中招。攻击者不需要针对每个版本单独构造,因为调用路径是一样的。所谓"通杀",本质上是"同源代码的批量复用",而不是什么高深的技术突破。理解这一点非常重要,因为它直接决定了防守方的应对思路——你不需要去背某个具体版本的差异,你需要判断的是这条代码链路在你的部署里是否存在、是否可达。

我在实际审计中见过太多类似情况:一个参数从入口传进来,经过两三层函数调用,最后被直接拼进了页面模板,中间没有任何一处做过类型约束或字符过滤。这种链路的生命力极其顽强,因为它"能跑",而且改动它可能影响大量正常业务,于是每一任维护者都选择绕开而不是修掉。技术债就是这么积累成攻击面的。

1.2 XSS 到 getshell 之间隔着什么

标题里最容易让人恐慌的是"可 getshell"。这里需要把概念拆清楚,否则很容易被带节奏。XSS 是浏览器端的问题,它的作用域是访问者的页面上下文;getshell 是服务器端的结果,作用域是操作系统。两者之间并不是一步到位的关系,中间必须存在某种"把前端能力转化为后端能力"的桥。

常见的桥有三类,我用生活化的方式解释。第一类是后台身份借用:管理员的浏览器被脚本影响后,攻击者可以以管理员身份去操作后台功能,而后台里往往存在文件管理、模板编辑这类高危能力。第二类是存储型内容落地:某些位置允许写入内容并被服务端当作代码执行,典型的就是模板缓存目录。第三类是凭证或会话泄露:通过在页面注入脚本获取到某些令牌,再用这些令牌去调用更高权限的接口。

看懂这个结构的意义在于:防守方不需要等全链路打通才行动。这条链上有好几个断点可以掐,掐住任何一个,链条就断了。最常见也最有效的断点就是"后台高危功能"和"可写目录的可执行性"。很多人只盯着 XSS 的输入过滤,其实那是性价比最低的一环,因为过滤规则永远在被打补丁。

1.3 为什么这类标题传播快但信息量低

这类标题的传播逻辑和它的技术含量几乎无关。它踩中了几个心理点:"通杀"意味着覆盖面广,让人本能觉得自己的资产可能中招;"0day"意味着没有补丁,制造紧迫感;"getshell"是结果导向,比起"存在反射型 XSS"这种描述,威慑力大得多。三条叠在一起,转发是必然的。

但从信息价值看,这类描述几乎没有提供任何可行动的信息。它没说触发条件、没说前置权限、没说受影响的具体功能模块、没说部署环境要求。我曾经跟进过一次类似的舆情,最后核实下来,那条链路要求攻击者已经具备后台低权限账号,而那个环境的默认配置根本不允许自助注册。也就是说,对绝大多数实际部署来说,风险是收敛的。

提示:看到任何"通杀""秒杀""一键"字样的安全情报,先按这三条过滤——是否需要前置权限、是否依赖非默认配置、是否有可验证的公开编号。三条都过不了的,优先级可以往后放。

2. 老旧建站系统的风险画像怎么画

2.1 先搞清楚自己的资产边界

所有安全工作的第一步都是资产清点,但这一步恰恰是很多团队做得最糊的。我见过不止一次,出事后才发现某个业务子目录里还挂着一套三年前的老系统,早就没人维护,也没人记得是谁装的。这种情况在老牌 CMS 上尤其普遍,因为它当年部署门槛低,随手一装就是一个站点,后来业务迁移了,站点还留着。

资产清点要做到能回答四个问题:有哪些实例在跑、分别是什么版本、部署在哪个网络位置、暴露到什么程度。前两个决定"有没有已知问题",后两个决定"谁能碰到它"。一个只在内部测试网段开放、且需要跳板才能访问的实例,和一个直接对公网 80 端口开放的实例,风险完全不是一个量级。

实操上,我建议用被动加主动两条腿走路。被动侧从网关日志、DNS 解析记录、证书透明日志里捞域名和 IP;主动侧在授权范围内做指纹识别和目录探测。这里必须强调授权——任何主动探测都必须有书面授权范围和窗口期,否则就是违规操作,性质完全变了。企业内部的扫描也要走流程,因为老系统本身很脆,扫崩了业务是要担责的。

2.2 版本识别与补丁状态的判断

版本识别是判断风险的起点,但不等于全部。老牌 CMS 的版本号通常会在几个位置暴露:静态资源路径里的版本标识、页脚的版权信息、特定文件的哈希特征、以及接口返回里的字段。有些站点做了隐藏处理,这时候就要靠文件指纹比对,也就是把已知版本的若干静态文件算哈希,跟目标比对。

但我要提醒一个常见误区:版本号新不代表没问题,版本号旧也不代表一定有问题。因为这类系统的版本迭代里经常出现"回退"现象——某个修复在 A 版本上线,在 B 版本分支合并时又被覆盖掉了。判断补丁状态更可靠的方式是直接看代码特征,而不是看版本号。

下面这张表是我在做老系统评估时常用的一页纸清单,把风险维度拆开打分,避免拍脑袋。

评估维度判断依据高风险特征
版本与分支静态指纹、文件哈希比对多年未更新、分支已停止维护
网络暴露面端口与路径可达性公网直连、后台登录页对外开放
默认配置残留安装目录、示例文件、默认账号安装入口未删、示例数据未清
权限模型后台角色划分、文件操作权限单一管理员权限、全站可写
运行环境Web 服务与解释器版本运行账号过高、目录可执行脚本
内容来源是否允许外部用户投稿或留言匿名可写、审核环节缺失
历史处置应急记录、日志留存无日志、无隔离、无备份

这张表的价值不在于分数本身,而在于它逼你把"感觉有风险"变成"具体哪一条有风险"。我一般会让负责的同学先把表填完,再决定处置优先级。填表的过程中往往会自己发现一些问题,比如某个实例的后台登录页居然是对公网开放的,这个发现本身就比任何扫描报告都值钱。

2.3 把"可写"和"可执行"两个属性彻底分开

在老系统治理里,我认为最值钱的一条原则是:让可写的目录不可执行,让可执行的目录不可写。这条原则听起来朴素,但它能挡掉绝大多数从内容落地到代码执行的链路,不针对任何一个具体缺陷,通用性极强。

具体怎么落地?以常见的部署结构为例,上传目录、缓存目录、附件目录这些必然要写入的位置,在 Web 服务的配置里明确关掉脚本解析能力,只允许返回静态内容。而程序代码目录,则把运行账号的写权限收掉,只保留读和执行。这两条做完,即便某条链路成功让攻击者写入了一个文件,这个文件大概率也跑不起来。

我在实际操作中踩过的坑是:只改了主站配置,忘了虚拟目录或者别名路径也继承了同样的解析规则。结果主站防护到位,通过一个二级路径别名绕了过去。所以配置改完之后一定要逐路径验证,用不同的访问路径去测试同一个目录下的文件,确认没有漏网的分支。这一步不能用"应该生效了"来交差。

3. 防护侧的核心动作:把攻击面收窄

3.1 入口收敛是最便宜的高收益操作

在成本有限的前提下,我优先做的一定是入口收敛,而不是去改代码。原因很实际:改代码要回归测试、要走发布流程、可能引入新问题;而入口收敛是配置层面的动作,风险低、见效快、可回滚。

入口收敛具体做三件事。第一,后台入口不对公网开放。后台管理是整条链上价值最高的目标,把它挪到内网、或者至少加一层访问控制,直接砍掉一大块风险。第二,安装和升级入口彻底删除。很多老系统的安装目录一直留着,虽然通常有"已安装"的检测,但这类检测逻辑本身也是代码,历史上出过问题。最稳的做法就是物理删除。第三,示例文件和数据清空。示例文件经常带有演示性的功能入口,属于典型的"开发者为了方便留下的后门"。

这里分享一个我自己的习惯:接手任何一套老系统,先花二十分钟把能删的都删掉,再看好不好用。删掉之后如果业务报错,说明业务依赖了不该依赖的东西,这本身也是个应该被记录的问题。这个动作做完,攻击面往往能缩小三分之一以上。

3.2 输出编码比输入过滤更可靠

关于 XSS 的防护,行业里争论了很多年"过滤还是编码"。我的观点很明确:输出编码是底线,输入过滤是补充。原因是输入过滤面临"黑名单永远不全"的困境,而且不同上下文的合法字符集不一样,用一个统一的过滤规则去卡所有输入,不是漏就是误伤。

输出编码的正确做法是按上下文选择编码方式。内容进入 HTML 文本节点,做 HTML 实体编码;进入 HTML 属性,除了实体编码还要确保引号处理正确;进入 JavaScript 字符串,要用 JS 转义而不是 HTML 转义;进入 URL 参数,要用 URL 编码。这四种上下文混用编码方式是常见错误,比如把 JS 上下文当成 HTML 来转义,看起来做了处理,实际一点用没有。

对于老系统,现实的做法是在模板层做统一封装。把输出点收敛到几个封装的函数里,在函数内部根据调用位置选择编码策略,业务代码只调函数不直接拼接。这样即便有遗漏,也能通过函数调用点的统计来做覆盖度检查,比满代码找拼接点靠谱得多。

注意:模板引擎自带转义不等于万事大吉。很多老模板引擎的转义只覆盖默认输出语法,一旦使用了"原样输出"的标记,转义就失效了。审计时要把这类标记的使用位置全部列出来单独看。

3.3 运行环境的最小权限

这一块经常被忽略,但它对"getshell"类结果的抑制效果非常直接。核心思路是:假设代码一定会被打穿,那么打穿之后能拿到什么。这就是纵深防御。

具体做四件事。第一,Web 服务的运行账号必须是专用的低权限账号,不能是管理员,也不能和数据库、缓存等其他服务共用。第二,禁止该账号在系统临时目录、用户目录下执行脚本。第三,数据库账号只授予业务必需的最小权限,绝对不要用高权限账号连业务库,更不要让业务库账号具备文件读写或命令执行类能力。第四,配置好出站限制,让被攻陷的主机难以横向或者外联。

我见过最离谱的一次,是某套系统的 Web 运行账号居然是超级管理员,而且这台机器上还跑着其他三个业务。这种情况下任何一个入口被打穿,影响都是灾难级的。后来整改就是把服务拆开、账号降权,虽然过程麻烦,但这是必须还的债。

3.4 检测规则的设计思路

规则这件事,我不建议去堆砌现成的特征串。原因很直接:针对具体载荷的特征串生命周期极短,改个编码方式就绕过了。更稳的思路是盯着行为而不是盯着内容。

值得监控的行为有几类:后台高危功能在非常规时间被调用、单个账号短时间内大量访问不同管理页面、模板类文件或脚本文件出现内容变更、Web 目录下出现新增的可执行文件、异常的进程创建行为。这些行为的组合,比任何单条特征都更能说明问题。因为这些动作在正常业务里极少发生,一旦发生,误报率低、处置价值高。

落地的时候要注意基线问题。老系统的访问模式往往很固定,先跑两周收集基线,再设阈值,比上来就配规则靠谱。我踩过的坑是一上来就设了很严的阈值,结果业务做了一次批量数据导入,直接告警刷屏,值班同学直接把规则关掉了。规则一旦被关掉就很难再打开,这个教训挺深刻。

4. 代码审计的通用方法:从数据流看问题

4.1 危险函数的追踪路径

做代码审计时,我的习惯是先列"危险函数清单",再反查调用链。所谓危险函数,指的是那些能触达文件系统、命令执行、模板解析、反序列化的能力点。清单不需要很长,但必须完整覆盖语言本身的这类能力。

列完之后做的是反向可达性分析:从危险函数出发,往上追参数来源,看能不能追到外部输入。能追到的,标为待确认;追不到的,暂时搁置。这个方法的好处是不依赖你事先知道漏洞长什么样,纯靠数据流判断,对新出现的缺陷类型同样有效。

追的过程中要特别注意几类"隐身"的传递方式:经过数组或对象包装后的传递、经过序列化再反序列化的传递、经过数据库或缓存中转的传递、以及通过全局变量或注册机制传递的。这几种情况在静态阅读时很容易断链,但对攻击者来说是完全连通的。我一般会把这几类传递方式单独做标记,重点跟。

4.2 变量流向的记录方式

人脑记不住复杂项目里的数据流,必须借助工具或者手工记录。我习惯用一个简单的表格来跟,每追一条链路就记一行:入口参数名、经过的函数、做过的处理、最终落点。追到落点之后再看这个落点有没有做上下文适配的编码,没有的话基本可以确认问题。

举个抽象的例子来说明这套方法怎么用。某个参数从请求里取出来,经过一次字符串拼接,赋给一个模板变量,模板里用原样输出的语法渲染。这条链路上没有任何一处做过编码,落点是 HTML 上下文,那么这就是一个典型的输出未编码问题。整个过程不需要记住具体函数名,只需要记住"取—拼—赋—渲染"这四个动作。

这种记录方式的另一个好处是可以做同类问题批量排查。当你在一个位置发现问题后,把同样的四个动作在代码库里搜一遍,往往能找到一批同样的写法。老系统的代码重复度极高,一处有问题,通常意味着一片都有问题。

4.3 审计过程中容易走偏的地方

第一个容易走偏的地方是只看入口不看上下文。看到有过滤函数就放心了,但过滤函数是在什么上下文里用的?如果参数最终进入的是 JS 上下文,而过滤是按 HTML 上下文设计的,那等于没过滤。上下文这个概念必须贯穿始终。

第二个是忽略二次注入。数据第一次写入时经过了处理,看起来是安全的,但读出来二次使用的时候又被拼进了危险位置,这时候原始处理就失效了。这类问题在老系统里非常常见,因为数据经常在多个模块之间流转。

第三个是低估配置类问题的危害。很多人审代码只审逻辑,不看配置文件。实际上老系统的配置文件里经常有影响巨大的开关,比如是否开启调试、是否记录详细错误、是否允许某种解析。调试模式开着,报错信息里可能直接带出路径和参数,这对攻击者来说是现成的信息源。

5. 常见问题与应急处置速查

5.1 怀疑被入侵后的处置顺序

这一步顺序错了会很麻烦。我的建议顺序是:先隔离、再取证、后清理、最后复盘。隔离指的是把受影响实例从网络上摘下来或者限制访问,目的是止血,防止影响扩大。这一步要果断,不要因为"先看看情况"而拖延。

取证指的是保存现场。具体要做的是对磁盘做镜像、导出关键日志、记录进程和网络连接状态。这里有个细节:日志要先导出再清理,很多同学着急清理,把日志一起删了,后面根本没法分析。另外要注意日志的时间基准,多台机器的时区要统一,否则拼不出时间线。

清理阶段要做的是彻底清除。经验上,只删掉发现的那个文件往往不够,因为攻击者通常会留多个落点。稳妥的做法是比对文件完整性、检查计划任务和启动项、检查账号变更、检查数据库里的异常内容。清理完之后要验证,验证的方式是从外部重新做一次检查,而不是自己看一遍觉得干净了。

复盘阶段最有价值的问题不是"他是怎么进来的",而是"为什么这么久才发现"。检测能力的缺口往往比漏洞本身更值得投入。

5.2 高频问题速查表

下面这张表是我整理的、在老系统安全治理里被问得最多的问题,以及我给出的处理方向。

现象或疑问常见原因处理方向
后台登录页暴露在公网部署时未做网络隔离迁移到内网或加访问控制
上传目录能直接访问脚本Web 服务未关闭该目录解析按目录配置解析策略并逐路径验证
页面报错带出物理路径调试模式未关闭生产环境关闭详细报错,错误统一落日志
数据库账号权限过大部署时图省事用了高权限账号按业务重建最小权限账号
日志缺失无法回溯未开启或未集中收集开启访问与错误日志并集中存储
老系统无法升级业务强依赖定制改动先做隔离与加固,同步规划替代方案
打补丁后功能异常补丁与定制代码冲突建立定制点清单,补丁前做影响评估
扫描导致业务中断老系统对并发与异常输入不健壮扫描走窗口期,限制速率,先备后扫

表格里的每一条我都至少遇到过一次。其中"打补丁后功能异常"是最容易让团队放弃治理的一条。我的经验是把定制改动做成清单,每次升级前逐条核对,把冲突点提前暴露出来。这个过程一开始很痛苦,做两轮之后就顺了。

5.3 迁移与替代的现实路径

说句实在话,对于已经停止维护多年的系统,长期方案只有一个:迁移。加固只能降低风险,不能消除风险,因为你看不到的那些代码路径永远可能存在未知问题。但迁移不是一拍脑袋就能做的,尤其当系统上跑着核心业务的时候。

我推荐的路径是"先隔离、再并行、后切换"。先通过入口收敛和网络隔离把风险压到可接受水平,给迁移争取时间。然后新老并行,把流量逐步导过去,同时验证新系统的功能覆盖度。最后切换并保留老系统只读一段时间,作为回退保障。

这个过程中最容易出问题的是数据迁移的一致性。老系统往往有大量历史数据,字段定义不规范,迁移时容易丢或者错。我的做法是先做全量比对,再做增量同步,最后做抽样人工核验。抽样这一步不能省,机器比对通过不代表业务上没问题,很多逻辑上的错误只有人看得懂。

6. 我在实际处置中的几点体会

关于这类"通杀型"情报,我最大的体会是:恐慌的成本远高于漏洞本身。一条消息出来,如果团队没有判断框架,就会陷入"全部系统都要排查"的混乱,资源被摊薄,真正该优先处理的地方反而没人管。建立一个简单的评估框架——能不能被外部触达、需不需要前置权限、有没有可验证的公开信息——比任何扫描工具都管用。

第二个体会是关于优先级。很多人第一反应是去修输入过滤,我反而会先去做两件事:把后台入口收起来,把可写目录的执行权限关掉。这两件事加起来可能只需要半天,但能挡掉整条链上最关键的两个环节。修过滤是个长期工程,而这两件事是立刻见效的。安全工作的性价比判断,很多时候就是看你能不能找到那个"杠杆点"。

第三个体会是关于文档。我坚持每处置一次就留一份记录,写清楚发现时间、影响范围、处置动作、遗留问题。这份记录的价值在当时看不出来,但在半年后做同类评估、或者人员变动交接的时候,价值巨大。老系统的知识最容易随着人员流失而蒸发,文档是唯一的对抗手段。

最后一个偏实操的小技巧:给每套老系统建一个"最小必要清单",写清楚它到底需要哪些功能、哪些目录、哪些端口。清单之外的东西一律关掉。这个清单做起来很土,就是一张纸的事,但它能把"这个能不能删"这种反复纠结的问题一次性解决掉。我在几个项目里推行过这个方法,效果比任何一次集中整治都持久,因为它把决策前置了,后面的人不需要重新判断一遍。

返回列表