做应用安全这些年,我处理过不少“看似有防护还是被突破”的事件。有客户非常困惑:系统明明部署了WAF,规则里也加了各种命令关键字,攻击请求为什么还能到达业务层?后来排查发现,问题往往不在某条规则写得不够狠,而在于整个过滤机制建立在“关键词”这个基础上——聊到RCE攻击,“关键词绕过”是躲不开的话题,但这里我更想从防守视角谈:黑名单机制为什么天然存在盲区、攻击者常利用哪些思路、防守方到底该怎么加固。这篇文章适合正在维护WAF规则、做安全开发或刚接触应用安全的朋友。内容只谈原理、检测特征和加固方法,不涉及任何可利用的攻击载荷。
1. RCE漏洞与关键词过滤机制的本质
1.1 RCE漏洞从哪里来
RCE(远程代码/命令执行)是应用安全里杀伤力最大的一类风险。它不是一个具体的漏洞名字,而是一族漏洞的统称:凡是攻击者能通过输入让服务端执行了原本不该执行的代码或系统命令,都可以归到这一类。最常见的两类入口是命令执行和代码执行。命令执行的场景比如业务需要调用系统工具处理文件、执行网络诊断,开发图省事直接把用户输入拼进命令模板;代码执行的场景更隐蔽,动态执行函数、模板引擎、表达式引擎、反序列化机制,都可能成为承载点。
这类漏洞的严重性不用多说。攻击者一旦得手,能做的就不再是“读取某个页面”,而是在服务器上运行命令、读写文件、部署后门,甚至以此为跳板影响内网其他系统。这也是为什么在安全通报名次里RCE总是排在第一梯队。有些业务团队会问“我们系统里没有执行系统命令的功能,是不是就不存在RCE风险”,其实动态代码执行、模板引擎解析、反序列化等都是潜在承载点,功能表面上看不到“命令”二字,不代表没有执行入口,这些细节排查时必须注意。
正因为它杀伤力大,几乎所有防御体系都会想尽办法把危险输入挡在门外。关键词过滤就是其中最常见、最早被部署的一种手段。理解这个背景,你才能理解后面所有“绕过”问题的来龙去脉。
1.2 关键词过滤:为什么普遍存在
关键词过滤通俗点说,就是维护一张“黑名单词表”,把请求参数、请求头、文件内容里出现这些词的内容拦截下来。它被大量系统选择,原因是三个字:成本低。需求评审时很容易通过,非安全背景的同事也看得懂,而且可以快速响应合规审计——“你们有没有对用户输入做检查?”“有,关键词过滤”,这套回答在不少组织里能过审。
但低成本和直觉化的另一面是脆弱性。黑名单工作的逻辑本质是“枚举已知的坏”,它默认了一个前提:我能在规则里穷举所有恶意变体。可是攻击输入的变化空间,几乎和语言的表达一样无穷无尽。一条命令可以被写成不同的编码、被拆成多个片段、被嵌进不同的上下文。黑名单只能识别字面特征,理解不了“经过组合之后会变成危险的语义”。
顺带解释一下黑名单和白名单的区别:黑名单是“列出不允许的,其余放行”,方便但漏检率极高;白名单是“列出允许的,其余拒绝”,天然抗未知攻击,代价是需要把业务允许的形态定义清楚,落地成本高。这个差异是整个防御思维转换的核心,也是后面加固方案的出发点。
1.3 关键词过滤的常见实现方式
在实际系统里,关键词过滤的实现方式五花八门。我按匹配粒度整理过几类,几乎每个系统都能对号入座:
- 原始字符串包含匹配,代码里直接判断目标字符串是否包含关键词。
- 大小写归一化匹配,先统一转成小写,再做字符串查找。
- 正则匹配,用通配符、字符组去覆盖一批变体。
- 单次解码后匹配,先做一轮URL解码或HTML实体解码,再判断。
- 多重解码后匹配,连续解码多轮再做判断,这种相对少见。
每一类实现都对应一个关键问题:在哪个环节检查?如果检查代码在网关层只看到了原始报文,那业务代码里后续发生的解码、类型转换、拼接,它全都看不见。很多团队会以为“我们在入口做了过滤就万事大吉”,其实从请求进入系统到最终执行之间,还有无数代码路径。
这里有一个很直观的类比:请求就像一份包裹,网关看到的是封好的快递箱,WAF看到的是纸箱外壳,框架层可能会拆掉外层包装,业务代码里又会打开内层包装,最终真正被执行的命令是在最里面那层。如果你的检查只在外面几层做,里面几层的内容自然就完全没看到。这个“拆包装”的过程,就是后面所有绕过手法发挥的空间。
2. 黑名单为何总是被绕过:从防御视角拆解四类盲区
我在应急响应中反复见过同一个场景:客户的WAF规则写得满满当当,什么危险词都有,但攻击请求还是能穿透到业务层。每次复盘到最后,问题几乎都不是规则数量不够,而是“规则设立的位置和方式”存在系统性盲区。
2.1 过滤机制的设计盲区
第一种盲区叫“检查内容与执行内容不一致”。过滤代码检查的参数值,和最终进入执行函数的参数值,往往不是同一个东西。框架的自动解码、自动类型转换、业务代码里的二次拼接,都会让两边的值分叉。攻击者不需要发明什么魔法,只要利用这个分叉,让检查者看到“安全版本”,让执行者看到“危险版本”,就造成了绕过。
第二种盲区叫“单一规则无法覆盖组合语义”。关键词过滤擅长匹配“一个完整的关键词”,但它通常不擅长回答“两个无害的片段拼在一起后是否构成危险操作”。这就好比把关卡设在单个零件上,却不去管组装车间。危险的不是某一片段,而是片段之间组合后产生的能力。
第三种盲区叫“规则本身可预测”。黑名单是静态规则,一旦发布在网络边界,任何人都能通过探测来推断。攻击者发送各种输入观察拦截反应,来回几次就能画出规则的轮廓,然后针对性地设计避开这些轮廓的输入。规则库不但不能隐藏,反而成了别人做测绘的地图。
理解了这三类盲区,下面那些具体绕过的类型就很好懂了。
2.2 攻击者常用的四类思路:防守方需要掌握的检测视角
下面把我在设计检测规则时必须考虑的四类攻击者思路列一下。再次说明,这里探讨的是防御检测特征,不是教授如何构造攻击载荷。
大小写与变形。这是最基础的绕过思路。一条针对英文关键词的规则如果区分大小写,攻击者只要改变大小写形式就能让匹配落空。防御上的做法是:统一在规范化之后的文本上做匹配,用不区分大小写的正则,或者在每个检查点做同样的预处理。检测样本里要包含大小写混合的输入,把它当作规则验收的一部分。
编码与多层解码。URL编码、十六进制编码、Unicode规范化……网络传输里编码本来就普遍存在,攻击者完全可以构造“多包装一层”的输入。防御方的核心原则是“在解码完成之后再检测”,而且要按统一顺序做完整解码。如果网关做一次解码、框架做一次解码、业务又做一次解码,规则就必须在最后一步解码之后重新校验,否则中间任何一层解开的内容都可能漏掉。日志里发现参数值在不同层不一致、且存在多层编码嵌套特征时,就应当触发告警。
拼接与组合。把被拦截的关键词拆成几个部分,分布在不同的参数或不同的请求片段里,再由执行逻辑在运行时重组。这是黑名单最难防的思路,因为它让单条规则看到的所有零件都是合规的。从防御角度,单靠入口匹配已不可行,必须依靠运行时行为监控,在命令执行、代码执行的瞬间判断参数是否属于“用户输入+多段拼装”的形态。
上下文逃逸。不改变关键词本身,而是改变解析器对输入的理解。比如让输入跳出字符串字面量的边界,进入命令解析或模板解析的语境。从防御角度看,需要用“数据与代码分离”的思路:用户输入永远只能当作数据处理,不能被当作可执行的语法结构。这一步的加固通常不在WAF,而在应用代码本身——该用参数绑定就不用字符串拼接,该用模板转义就不让原始内容直通渲染引擎。
这四类思路没有高下之分,现实中攻击者往往会混用,形成一条“绕过链”。
2.3 一次“绕过链”的抽象复盘
前几年处理过某系统的一次安全评估,印象很深。系统的入口参数经过了WAF,WAF规则里有关键词检测,也做了URL解码。但问题出现在一处第三方组件上:这个组件对参数又做了一次解码和规范化,然后才把解析后的内容传给后续执行逻辑。攻击者利用“比预期多一层的解码”这个差异,让WAF看到的是被编码包裹的输入,而真正落到执行函数时已经是被还原的恶意内容。
复盘整个过程时你会发现,没有任何一步是“高科技”。多一次解码,是组件的默认行为,规则没覆盖到;缺少统一规范化顺序,是历史遗留的配置;执行前没有重新校验,是代码里少了一口检查。每一环都平淡无奇,连起来就能穿透一套看似完整的防护。这个案例带给我最大的启发是:安全评估不要只盯着某一条规则有没有覆盖某个关键词,而要沿着“输入从入口到执行”的完整链路走一遍,看清楚每一个环节做了什么转换、每层防护能看到什么内容。
3. 构建纵深防御:三个方向的加固实践
讲完了问题,再说解决方案。我始终坚持一个观点:关键词过滤可以作为辅助,但绝对不能作为主力。真正值得投入的加固方向有三个:白名单化、多层检测、执行前统一校验。
3.1 三步把过滤逻辑从黑名单迁移到白名单
第一步,给所有外部输入做类型白名单。数字参数就解析成整数,枚举参数就限定在允许的集合里,路径参数先做规范化再判断是否在允许目录内。不要试图用正则“识别非法字符”,而是直接“按合法格式解析并强制转换”。解析失败就拒绝请求,这一招能挡掉大量奇怪输入。
第二步,给命令和代码执行入口做白名单。如果业务确实需要调用系统命令,就把允许执行的命令列成一张表,命令本身不可由用户输入指定,参数再经过严格格式校验。任何涉及动态执行或模板渲染的地方,优先选择受限沙箱,把“可触达的能力”压缩到最小。
第三步,给业务逻辑做输出与能力白名单。例如文件下载接口只允许读取指定目录,写文件接口只允许写入指定命名规则的文件。白名单思维往里走一步,很多攻击面就自然封死了。这一步不是安全团队单方面能完成的,需要和业务开发一起定义“合法形态”。
3.2 四层检测:让绕过链在某一层被截断
纵深防御不是一句口号,具体到RCE防护上,我建议至少设置四层:
入口层,也就是网关和WAF,用来拦截自动化扫描、已知恶意样本和明显异常的请求。它的价值是减少下游噪音,不需要追求100%拦截率。
应用层,在业务代码入口做参数校验和规范化。这层是白名单落地的主战场,也是离业务逻辑最近的地方,能做很多WAF做不了的语义判断。
运行时层,通过RASP或类似机制在系统命令执行点、代码执行点做行为判断。这一层不问“输入长什么样”,而问“当前这个操作是否由用户输入触发、是否越过了策略边界”。它最难被绕过,因为已经贴到了执行点。
主机侧,进程行为监控、文件完整性、异常外连检测作为兜底。即使前面全被穿透,安全团队还能通过主机侧的告警发现失陷并在最短时间内响应。
这四层是层层收窄的漏斗,任何一层发现异常都可以阻断。我见过不少团队只把注意力放在WAF一层,结果WAF被绕过就全线失守。加上后面几层,效果会立刻不一样。
3.3 正则与规则设计:与其堆词表,不如抓结构
写安全规则和写正则一样,堆量不如抓结构。疯狂的负面清单既难维护,又会带来大量误报。我建议在设计规则时坚持几个原则:
第一,规则匹配的对象必须是完整解码后的内容。很多绕过之所以得手,是因为规则匹配的是原始报文或者只解码了一层。统一解码顺序,先在入口解码一次,在执行前再校验一次,比把所有变体塞进规则库要省力得多。
第二,尽量匹配“危险结构”而不是“危险单词”。与其枚举所有系统命令名,不如检测“参数中出现命令分隔符特性,并且与执行入口连接”这样的结构。结构特征不容易被变形绕过,误报也更可控。
第三,控制规则总量。规则越多,每次发布前要做的回归测试越多,出错概率越大。定期请业务方一起回放真实流量,清理已不需要的规则,合并重复的模式。
第四,用双向回放做评估。拿历史攻击样本和正常请求一起跑规则,算出拦截率和误报率,用数据决定规则的去留。安全规则不是一潭死水,它需要持续迭代。
3.4 日志与溯源:没有链路日志,就没有闭环
检测体系再完整,如果日志不完整,告警出了也只能靠猜。我在多个项目里推动过一个简单标准:敏感操作必须能回答“输入是什么、在哪一步变了形、最终执行了什么”这三个问题。
为了做到这一点,建议至少记录:原始报文,各解码层的结果,进入业务代码时的参数值,执行函数收到的最终值,以及接口名、用户标识、会话ID、来源IP和时间戳。如果可能,把参数从入口到执行的链路用结构化字段存下来,方便按请求ID串联查询。
有一次客户拿着一条可疑访问记录来找我,入口日志里参数是普通字符串,WAF日志里的值已经变成了另一种形态,到了应用日志又还原成敏感内容。如果不是三层日志都能关联起来,这个绕过的链路根本不会被发现。链路日志不是锦上添花,它决定了你到底有没有能力发现“黑名单失效”这件事。
4. 授权测试方法论:如何安全验证防护有效性
任何防御方案都需要验证,而验证最有效的方式,恰恰是用“攻击者视角”来审视自己的系统。但这道工序必须在严格的安全边界内进行。没有授权就测试,那是违法,这个边界怎么强调都不为过。
4.1 测试前提与边界
在动手之前,必须把边界写清楚。我说几个每次都要确认的点:
第一,测试对象的所有权和授权。被测系统要么是自己的,要么是客户通过书面协议明确授权的。不要因为“关系熟”“之前合作过”就省略授权流程。
第二,测试环境与生产环境隔离。优先准备与生产一致的测试环境,避免在真实业务系统上验证。如果必须在生产环境做小范围验证,需要独立的变更审批与回滚方案。
第三,范围与时间窗。明确允许测试的接口、参数范围、测试时间段,以及紧急情况下的联系人。范围之外的任何操作都不允许。
第四,红线清单。能对业务造成不可逆影响的操作、可能影响其他用户数据的操作,都属于绝对禁止的范畴。
这些边界写出来可能会让人觉得繁琐,但我见过因为边界没写清楚,测试过程中业务系统出现异常,最后连正常的验证工作都变得被动的案例。边界不是束缚,是保护所有人。
4.2 用攻击者思维做防御验证
有了授权和边界,就可以正式开始“防御验证”。我推荐把测试重点放在下面几个问题上,而不是单纯找某个漏洞:
链路解码顺序是否统一。把所有外部输入参数从入口到执行函数走一遍,确认每一层解码、转换的顺序一致,且在最终执行前有统一校验。
参数跨层传递后是否改变形态。对照中间层日志和应用层日志,检查同一个参数在不同层的值是否发生不一致,不一致就意味着存在过滤器覆盖不到的转换点。
能否通过多参数组合绕过单条规则。检查业务代码里是否有对多个参数进行拼接或组合后执行的路径,如果有,这层路径是否在检测覆盖范围内。
运行时层是否能兜住入口层的漏网。用验证样本触发执行链,观察运行时检测是否有告警。如果入口层放行、运行时层也放行,就要立即修复。
这组问题的本质是“站在对手的位置,检查自己的纵深防御有没有断点”。每发现一个断点,就补一处检测,而不是简单加一条词规则。
4.3 企业自测清单
结合自己的防护实践经验,我整理了一份适合在版本发布前或月度巡检时执行的基础自测清单,分享给大家:
| 检查项 | 检查方式 | 合格标准 |
|---|---|---|
| 参数类型校验 | 遍历所有外部输入参数,确认有类型白名单 | 非预期类型直接拒绝 |
| 命令执行入口 | 排查系统命令调用路径 | 命令来自白名单,无用户输入拼接 |
| 动态执行入口 | 排查动态执行函数与模板渲染路径 | 输入不可触达可执行能力 |
| 解码链路一致性 | 梳理各层解码与规范化顺序 | 最终执行前有完整解码与校验 |
| 日志完整性 | 抽查敏感操作的链路日志 | 可还原输入至执行全过程 |
| WAF规则质量 | 用真实流量和样本双向回放 | 零误报且样本拦截率达标 |
这张表不一定适合所有团队,但核心思路是通用的:不要只看漏洞是否存在,而是看“输入到执行”全链条上有没有失控点。
5. 常见问题与排查技巧实录
最后一部分,分享我在实际运维、应急响应中反复遇到的几类问题,以及对应的排查经验。这些问题都不是教科书上的案例,而是日常工作中真实会踩的坑。
5.1 加了规则还是被绕过,先别急着加规则
这是最常见的求助:“我们已经把关键词规则库填得很满了,怎么还有请求能打进来?”我的建议是:先还原请求的完整链路。把网关日志、WAF日志、应用日志下载下来,同一个参数逐层对比。如果同一参数在三个日志里的值不一样,说明链路中存在一个你没覆盖到的转换点。把校验点补到“最终执行前”这一位置,通常比继续扩充WAF规则库更有效。
这里的难点在于多份日志的关联。建议从请求ID维度做好串连,否则逐行对参数非常痛苦。我在实际推动中,会把“链路日志完整、可关联”作为安全日志建设的第一优先级,没有这个基础,后面所有分析都是空谈。
5.2 误报太多,业务方天天投诉
规则量大一定会带来误报。我处理误报的经验是:先定位误报来源。一种是规则本身写得太宽,关键词匹配的范围覆盖了正常业务文本。这种情况要把规则改精确,尽量匹配结构特征而不是单词。另一种是接口本身接收自由文本,比如用户昵称、评论内容,这类字段本来就不适合套用命令类关键词规则。给字段分级、给规则分层,不同接口用不同的规则集,误报率会显著下降。
还有一个小技巧:每次新增规则,先用最近三个月真实业务流量回放一遍,把会命中的正常请求列出来,人工确认后再上线。我自己的习惯是准备一套自动化的回放脚本,每月跑一次,把新增规则与历史流量做离线比对,输出命中列表后再由安全同事逐一确认。虽然前期工作量大,但长期看维护成本反而降低,业务方的信任度也会慢慢建立起来,后续再提加固方案时配合度就高很多。
5.3 日志里发现可疑请求后怎么分析
拿到一条可疑请求日志,正确的打开方式不是直接看有没有命中规则,而是把这条请求的链路串起来看。我一般按这个顺序分析:先看入口参数长什么样,再看中间层参数有没有发生变化,最后看业务代码收到的值是什么。如果某个参数在入口处是普通形态,在中间层出现了编码或拼接特征,到业务代码又还原成了敏感内容,那基本可以断定这是一条“先变形再还原”的绕过尝试。
遇到这种情况,先把完整链路日志导出、保存为独立事件,再做规则补充和代码修复。千万不要看完就删日志,很多复盘都要依靠这种第一现场记录。保存时建议连请求头、时间戳、关联的会话信息一起归档,后续做攻击画像或审计追踪时都会用到。
5.4 快速排查速查表
把经验整理成一张速查表,方便日常排障时快速定位方向:
| 症状 | 可能的根因 | 优先排查动作 |
|---|---|---|
| WAF拦截但请求仍到达业务层 | 各层解码时机不一致 | 对比网关与应用层参数值 |
| 单条规则能拦,变体拦不住 | 缺少统一规范化 | 统一解码顺序后再匹配 |
| 接口偶发异常,无法稳定复现 | 参数跨组件拼接 | 查看组件间传递的参数结构 |
| 日志参数值和入口不一致 | 框架自动解码/类型转换 | 梳理框架层转换行为并补校验 |
这四行覆盖了我日常遇到的大多数情况。每次用这张表快速定位问题后,我一般还会顺手把新的判断经验补充进去,让表越用越顺手。
写到这里,我想起自己早年间维护安全规则时的一段经历。当时我也迷信关键词库,觉得多加一条规则就多一分安全,直到一次应急响应里看到某条规则明明就在日志里出现过,攻击请求还是穿透到了业务层,才真正开始反思:关键词过滤只是无数防护环节里最薄的一层。真正决定系统安全性的,往往不是某一份词表,而是白名单默认拒绝的习惯、多层检测的冗余设计,以及执行前统一校验的纪律。这些做法不会像关键词规则那样立竿见影,但长期的防守效果会完全不同。做安全的路上,少一点对规则的依赖,多一点对链路的敬畏,系统会更稳,你也会更省心。