这两年只要听到“被拿权限”“数据库被拖”“服务器被种马”,十有八九都能扯到RCE漏洞上。RCE,Remote Code Execution,远程代码执行,翻译成大白话就是攻击者能在你的服务器上直接敲命令、跑程序,跟坐在机房抱着键盘没区别。这个东西在漏洞圈里是天花板级的存在,因为它跳过了所有“读文件”“读数据库”的中间步骤,直接拿到了系统最高话语权。
这篇文章不搞花架子,我会从RCE的成因逻辑讲起,结合我在实战中遇到的利用场景和踩过的坑,把我自己的排查思路和防御方案完整梳理一遍。适合刚接触安全的开发、运维,以及想系统补强Web安全体系的安全工程师参考。我会尽量把每个环节的“为什么”讲明白,毕竟知其然不知其所以然,下次漏洞换个姿势出现,你还是会懵。
1. 先弄清楚攻击者到底拿到了什么
1.1 RCE的本质:从“输入”到“执行”的距离
RCE漏洞之所以危险,是因为它打破了“数据”和“代码”之间的边界。正常情况下一套Web系统处理用户输入,是把输入当数据看待的,比如你提交一个用户名,程序把你输入的字符串跟数据库里存的值做比对;但RCE出现的时候,程序把一段输入当成了代码去执行,比如你提交的内容里有;id,程序直接拼到了系统命令后面,然后按命令执行了。
我用个生活化的类比:你去饭店点餐,正常是告诉服务员“我要一碗面”,服务员把需求传给后厨。但如果你说的话被饭店当成“做菜步骤”直接执行,你说“把厨房门打开”,后厨真的就去开门了,那这个饭店就出大问题了。RCE就是这样的“点餐失控”,你提交的字符串变成了操作系统的指令。
理解了概念之后,关键问题就来了:哪些地方最容易出现数据变代码的转换?我做了这么多年的排查,总结下来,全在“间接执行”这四个字上。所谓间接执行,就是程序本身没有直接调用系统命令,但它调用了某些能执行代码的底层函数,这些函数把用户可控的字符串当作代码来解析了。
这种间接执行最常见的三个载体是:
- 反序列化:把对象的序列化字符串还原成对象,还原过程可以触发魔术方法
- 表达式执行/模板渲染:把字符串按表达式或模板的语法解析
- 文件包含/动态文件加载:代码里通过变量拼接文件路径,再由解释器加载执行
后面我会分别展开讲,这里先记住一个核心判断原则:凡是用户输入能触达“解析器”的地方,都要警惕RCE。判断的关键不在于“参数叫什么”,而在于“这个参数最终被谁消费了”。
1.2 攻击链的完整拼图:RCE不是一步达成的
很多刚接触安全的朋友,以为RCE就是一行payload打得通就成功了,实战里其实极少这么简单。一次完整的RCE利用,通常是这样的攻击链:
- 入口:一个可被外部访问的HTTP接口、文件上传点、消息队列消费逻辑、甚至一个定时任务依赖的数据源
- 传递:攻击者的恶意输入,经过URL编码、Base64编码、序列化包装等方式,绕过基础校验到达危险函数
- 触发:代码中的危险函数把输入当作代码或命令执行,完成RCE
- 落地:攻击者写入Webshell、反弹连接、创建系统后门账号
- 持久化:通过计划任务、服务注册、启动脚本等机制,保证自己拿到的权限不会因为重启而丢失
我强调这一点,是因为防御视角和利用视角差异很大。如果你只盯着“危险函数”去查,忽略了整个链路上的输入通道,那排查起来会漏掉很多变体。比如攻击者把恶意对象塞进消息队列,Web系统本身没有RCE,但消费消息队列的那个服务有反序列化漏洞——从Web流量里根本看不到攻击痕迹。
所以我在做排查和防御的时候,习惯先画一条“输入流转图”:用户的输入从哪个接口进来、经过了哪些过滤和编码、最后落到哪个解析器。这个流转图画清楚,RCE的排查就成功了一半。
2. 常见的RCE触发场景与原理拆解
2.1 反序列化漏洞:隐蔽性最强的RCE入口
Java和PHP生态里,反序列化RCE是重灾区。原理说穿了很简单:序列化是把对象变成一段字节流或字符串,方便存储和传输;反序列化就是还原过程。问题来了,还原的过程中,对象的一些魔术方法会被自动调用,比如PHP的__wakeup()、__destruct(),Java的readObject()、readResolve()。
如果序列化字符串里包了一个精心构造的类,而这个类的方法链上有危险操作(比如调用eval、system、反射执行命令),那服务端一反序列化,等于把攻击者构造的“定时炸弹”拆开了,炸的就是服务器本身。
我在代码审计里看反序列化漏洞,重点关注两个点:
- 入口:token/session的存储反序列化、
O开头字符串的PHP输入、Java RMI接口、JMX接口、XMLDecoder、ObjectInputStream.readObject()等 - 利用链(gadget chain):目标环境里有哪些类库、组件,组成了什么样的调用链
这里要强调一个很多人容易忽略的点:反序列化漏洞能不能利用,和业务代码写没写危险函数关系不大,它更多取决于依赖的库。比如Commons-Collections这个依赖版本不对,即使业务代码干干净净,攻击者也能用库里自带的类构造出命令执行链。
防御端怎么防?我的实操经验是三层:
- 升级依赖库版本,把有公开利用链的库升到修复版本
- 重写
ObjectInputStream/resolveClass做类白名单校验,只允许反序列化已知可信的类 - 尽量不用Java原生序列化,改用JSON等安全的文本格式
2.2 表达式注入与模板注入:开发者的“便利”成了突破口
这类RCE特别容易出现在“为了让产品更灵活”的功能里,比如:
- 管理后台提供一个“计算表达式”的功能,支持用户输入EL表达式、SpEL表达式、OGNL表达式
- 邮件服务支持Freemarker、Velocity模板
- 报表系统支持Python 表达式计算
- 规则引擎允许用户自定义条件表达式
灵活是灵活了,但表达式解析器有个共通特性——它内部太强大了,你只用了它5%的能力,但攻击者可以通过它调用到100%的能力。拿Java的SpEL举例,一个简单的表达式T(java.lang.Runtime).getRuntime().exec("whoami")就能执行系统命令。这种表达式看起来不像SQL注入那么“扎眼”,很容易骗过开发者的眼睛。
我在测试这类漏洞的时候,常用的探测姿势是先触发一次报错看回显,比如输入${1+1},如果返回了2,那就说明表达式被执行了,接下来就是根据表达式语言的语法尝试调用Runtime执行命令。
防御建议放到后面统一讲,这里就提一个核心原则:用户输入永远不能直接拼进表达式模板。如果业务上没办法避免“动态表达式”的需求,那就用白名单方式只允许有限操作符和字段名,或者把表达式解析放到独立的沙箱环境里。
2.3 文件上传与文件包含:从“传文件”到“跑代码”只差一步
文件上传本身不一定是漏洞,但“上传之后可以被当作脚本执行”就变成RCE了。典型场景:头像上传功能没校验文件内容,只校验了后缀,攻击者传了一个shell.php.jpg,服务器如果配置了把.jpg也交给PHP解析,那这个文件就变成了Webshell,攻击者访问这个文件即可执行任意代码。
{% raw %} 这里我要多说一句,很多人以为“只允许上传图片”就安全了,这是天大的误区。攻击者可以把恶意代码藏在图片的EXIF信息里、藏在文件末尾的GIF89a注释后面,用图片马绕过内容校验。更隐蔽的是利用文件内容解析差异:同一个文件在WAF眼里是图片,在服务端脚本引擎眼里却是可执行代码。 {% endraw %}
文件包含导致的RCE也是非常经典的场景。PHP里include $_GET['page'];,当$_GET['page']是用户可控的,攻击者可以包含日志文件、/proc/self/environ等系统文件,往里面注入PHP代码,最后通过include触发执行。这种漏洞不只是PHP有,Java的Class.forName、Python的os.popen拼接路径、Node的require动态加载都可能踩坑。
3. 自己系统里怎么排查RCE隐患
3.1 代码审计法:从源头快速定位危险调用
如果系统是自己的,代码在那儿,排查质量最高的方式就是代码审计。不需要把所有代码都读一遍,重点盯几类特征函数就能快速圈定可疑范围:
| 语言 | 需重点关注的高风险函数 |
|---|---|
| PHP | eval、assert、system、exec、shell_exec、popen、include/require(变量拼接)、unserialize |
| Java | Runtime.exec、ProcessBuilder、getRuntime、Method.invoke、XMLDecoder、ObjectInputStream.readObject |
| Python | eval、exec、os.system、subprocess、pickle.loads、yaml.load(不指定Loader会触发任意代码) |
| Node.js | eval、Function、child_process.exec、child_process.spawn、vm.runInNewContext |
定位到危险函数之后,关键动作是“回溯参数来源”。从危险函数往上看,一层层倒推变量从哪来。如果变量最终来源是$_GET、request.getParameter、user_input这种不可信输入,基本就是高危了。
这里有一个我在审计时常用的“污点分析”小技巧:写代码的时候可以刻意给输入源头打标记,比如在入口统一做一层INPUT封装,之后所有从INPUT取出来的变量在IDE里都高亮跟踪,这样回溯调用链能省一半时间。
3.2 黑盒测试法:用少量请求快速探测RCE
如果看不到代码,就得靠黑盒测试。黑盒测试的思路是“先用无害命令探测执行,再考虑利用”。比如目标参数疑似存在命令拼接,我可以提交ip=127.0.0.1|whoami,如果回显里出现了当前系统用户名,说明管道符后面的命令被执行了。
针对不同类型的入口,探测方式也要区分:
- 命令执行入口:输入
;id、|id、&&id、`id` - 表达式注入入口:输入
${7*7}、#{"A".class}、{{7*7}} - 模板注入入口:Freemarker测
<#assign x="freemarker.template.utility.Execute"?new()>${x("id")};Velocity测#set($x="")$x.class.forName("java.lang.Runtime").getRuntime().exec("id") - 反序列化入口:这个一般没法直接靠请求参数看出端倪,需要结合响应报错、响应时间、含有序列化特征的请求体判断
黑盒测试的注意事项:尽量用id这种无害但结果唯一的命令验证执行;不要在没授权或非测试环境乱打,RCE探测本身就属于攻击行为,边界要拿捏好。
3.3 依赖与指纹排查:不写代码也可能中招
RCE还有一大类是“代码没问题,但依赖的地基坏了”。比如用了某个开源组件,这个组件有个1day,你的代码只是正常调用了组件功能,但组件内部出问题。这类问题靠审计业务代码解决不了,必须做依赖排查。
我的做法分两步:
- 盘点资产台账,搞清楚每个服务用了哪些中间件、开源组件、版本号
- 订阅漏洞情报,重点关注这些组件版本是否存在已知RCE漏洞,有就确认是否受影响
常见踩坑组件包括但不仅限于:Log4j2、Fastjson、Shiro、Struts2、WebLogic、Elasticsearch、Confluence这些大名声的组件。这类RCE的特点是:利用门槛低、影响面大、Payload在网上一搜一大把,所以边界防护如果没跟上,基本是裸奔状态。
4. 实战防御:从代码到运行时逐层加固
4.1 代码层的修复思路:不仅补函数,更要补设计
有了排查结果,修复不能只盯着“把eval删了换别的函数”这种表面动作。因为很多RCE问题本质是设计缺陷——把用户输入和代码执行混在了一起。修复的时候,我建议按优先级从高到低推进:
- 禁止用户输入进入任何可执行解析器。反序列化、表达式、模板、拼接命令,全部不允许直接使用原始输入
- 严格输入校验。如果业务上必须接收某些格式(IP地址、端口号、编号),校验“白名单格式”而不是“黑名单过滤”。黑名单只能防已知姿势,白名单才能防未知
- 参数化/白名单映射。比如用户要执行某个命令,不要允许他传命令本身,而是传一个预定义好的命令ID,由服务端映射成具体命令
- 最小权限运行。Web服务进程不能用root跑,这是很多案例里系统被彻底打穿的关键原因
除了这几个动作,我还特别建议把“安全编码规范”落到Code Review里。我见过太多修完一个点、测试通过就上线的操作,结果一个月后攻击者换个参数名又打进去了。RCE的变体太多,代码评审时必须有意识地问一句:这个参数最终会流到哪个解析器去?
4.2 运行时与中间件加固:在攻击路径上多设几道闸
单靠代码修可能跟不上所有场景,所以运行时加固是必要的补充。我自己用下来比较有效的手段:
- WAF规则:对特定危险特征做拦截,比如命令执行关键字、反序列化特征(
rO0AB这种Java序列化头)、表达式注入特征。但WAF不能当主力,只能作为第一道减速带,它最大的价值是延长攻击时间、为应急响应争取窗口 - RASP(运行时应用自保护):监控代码里危险函数的实际调用,只有当“不可信数据”流向“危险函数”时才产生告警。RASP相比WAF的优势在于它基于上下文判断,误报率低很多,能够应对0day在代码层的表现
- 沙箱隔离:表达式/脚本执行需求放到独立沙箱环境,比如用Seccomp限制系统调用、用容器限制资源,即使被RCE,攻击者拿到的也只是一个受限环境
- 出网管控:很多RCE利用都依赖“反弹Shell”,攻击者需要服务器主动连回外部。在防火墙上限制服务器出站方向只允许必要的服务端口,能大幅提升利用难度
这里插一个实际案例:有次我们排查一个被种了挖矿木马的主机,发现攻击入口是一个旧版Fastjson反序列化漏洞。木马本身是靠bash脚本拉起来的,内网横向打了一通。当时负责运维的同事很委屈,说“防火墙、WAF都上了怎么还被攻击了”。复盘发现WAF确实拦截了一部分常见payload,但攻击者用了Fastjson带AutoType绕过的变体,WAF语义特征没识别出来,流量直接放行到后端Java服务了。后来我们补了一台RASP,这一类绕过马上就暴露了。这个故事说明:单点方案永远不够,纵深防御不是喊口号。
4.3 检测与响应:假设已经被攻击,怎么不被动
有一句话我在安全圈里听了很多年,越品越觉得有道理:不要假设自己不会被攻破,要假设自己已经被攻破,然后去想怎么尽早发现、怎么止损。所以在RCE防御体系里,检测与响应至少占了半壁江山。
我在检测层面比较看重这几类数据:
- 日志完整性与集中采集:Web访问日志、命令执行日志、安全设备审计日志集中到统一平台,方便事后回溯
- 进程行为监控:RCE之后通常伴随创建新进程、网络外连、写文件等异常行为,HIDS(主机入侵检测)重点盯这些行为特征
- 文件完整性监控:对Web目录、系统关键目录做hash基线,文件被改动立即告警
响应层面一定要有预案,并且最好演练过。我自己参与过多次应急,最深的体会是:RCE应急真正难的,不是杀掉某个可疑进程,而是确认攻击者的完整路径,把入口、落地、持久化全部清除。如果只杀进程不查启动项、不看计划任务、不翻crontab,第二天起来服务器大概率又被种回去了。
5. 常见问题与排查技巧实录
5.1 复现和排查中的典型问题速查
| 问题现象 | 可能原因 | 排查建议 |
|---|---|---|
| 盲打RCE无回显 | 命令执行了但输出没回显,可能被重定向或过滤了输出 | 用curl带参外带数据、用DNSLog验证执行 |
| WAF拦截了所有常见payload | WAF基于规则拦截,绕过姿势不够 | 尝试编码、混淆、拆分语义绕过,或换协议层绕过 |
| 反序列化PoC触发但没执行 | 利用链与目标库版本不匹配 | 检查目标依赖版本,确认利用链的每个类都存在且版本匹配 |
| 打了payload后服务崩溃 | payload与应用程序冲突,可能异常未被处理 | 用更稳定的利用链,先在测试环境验证 |
| 确认RCE但找不到入口 | 入口不在Web流量里,可能在消息队列、定时任务 | 排查外网日志、内网行为、异常出站连接 |
| 修好后又被同样方式打穿 | 只修了一个点,同产品线还有同样问题 | 全量扫描相似参数,做横向排查,不要只修单点 |
5.2 一些只有踩过坑才懂的小技巧
最后分享几个实战心得,不算正统教程里的内容,但确实管用:
- DNSLog是盲打RCE的神器。没有回显的情况下,先构造一个
ping命令让目标服务器访问你控制的DNS域名,DNSLog收到解析记录就证明命令执行成功。这个方法在测试和排查里都极好用 - 请求拼上随机参数更容易找到日志。排查问题需要回看日志时,如果每个请求都带上一个自定义的随机参数(比如
rnd=abc123),在大量日志里定位个人请求会非常快 - 注意时间侧信道。有些RCE执行后没有回显也不出网,但执行操作本身会消耗时间。比如让目标执行
sleep 5,如果请求响应也延迟了5秒,那就是命令执行的信号。这个技巧用来确认盲RCE特别有效 - 内网信息收集比弹Shell重要。如果你在做红队或者攻防演练,拿到一台机器的RCE之后,第一件事往往不是弹Shell,而是快速信息收集:当前用户权限、网络连通性、是否存在同网段的重要目标。信息量充足,决策才有依据
走到这儿,RCE从成因、利用到防御的完整链路就都梳理完了。说到底,RCE漏洞的本质就是边界失控,数据变成了代码。我的体会是,防御这类漏洞没有银弹,靠的是代码层面的敏感、运行时的防护、检测响应的配合,再加上平时多演练、多复盘,该堵的口子都堵上,该看得见的异常都看得见,才能在真实的攻防对抗里不落下风。