远程代码执行(RCE)不是单一漏洞,而是一类结果:攻击者让服务器执行了本不该执行的代码。修复方案如果只盯着某个CVE编号打补丁,下次换个参数照样被打穿。真正有效的防护必须从代码层、运行环境、网络边界、监控响应四个维度同时收紧,并且把“不允许执行”设为默认状态。
先把输入当作不可信数据处理
绝大多数RCE的入口是用户可控的参数,包括URL参数、表单字段、HTTP头、Cookie、文件上传内容,甚至日志里拼进去的User-Agent。修复第一步不是过滤,而是定义“合法值”。例如某个参数只允许数字,就强制转换为整型;只允许字母和下划线,就用白名单正则校验。黑名单过滤永远不靠谱,因为编码方式、大小写、Unicode等价字符、换行符都能绕过。
以PHP为例,如果业务代码里出现过这样的写法:
system("ping -c 1 " . $_GET['host']);
攻击者传入 127.0.0.1; cat /etc/passwd 就能执行额外命令。修复方式不是过滤分号,而是根本不要调用系统命令。如果必须调用,应使用参数数组形式,避免shell解析:
$cmd = ['ping', '-c', '1', $host]; $process = proc_open($cmd, $descriptors, $pipes);
Java中同样避免 Runtime.getRuntime().exec("cmd /c " + input),改用 ProcessBuilder 并传入参数列表。Python里避免 os.system 和 subprocess.shell=True,使用 subprocess.run([...], shell=False)。
危险函数不是删掉就行,要建立调用清单
PHP的 eval、assert、preg_replace 的 /e 修饰符、unserialize,Java的 Runtime.exec、ProcessBuilder、JNDI lookup,Python的 eval、exec、pickle.loads,Node.js的 child_process.exec、eval、Function 构造器,这些都是RCE的高发点。修复方案中必须列出一份“禁用函数清单”,并在代码审计工具中配置规则,发现即阻断合并。
对于反序列化导致的RCE,单纯禁用函数不够。Java中如果业务必须使用反序列化,应引入对象输入流过滤,例如使用 ObjectInputFilter 限制可反序列化的类。Python的pickle不应处理任何来自外部的数据,换成JSON或MessagePack。PHP的unserialize如果必须使用,应确保数据来源可信,并对序列化字符串做HMAC签名校验。
模板注入和表达式注入要单独处理
很多RCE发生在模板引擎里。例如FreeMarker、Velocity、Thymeleaf、Jinja2,如果用户输入被直接拼进模板,攻击者可以调用底层Java或Python对象执行命令。修复原则是:用户输入只能作为模板变量渲染,绝不能作为模板代码解析。配置模板引擎时关闭沙箱逃逸能力,限制可访问的类和方法。对于表达式引擎如SpEL、OGNL、MVEL,除非业务必须,否则不要暴露给用户输入。
一个常见的错误是使用 Runtime.getRuntime().exec() 执行外部程序处理文件,比如用ImageMagick转换图片、用FFmpeg处理视频。这类场景中,文件名或参数来自用户。修复方案是:将文件重命名为随机字符串,放在隔离目录,调用外部程序时只传绝对路径和固定参数,不拼接任何用户原始输入。同时对外部程序的版本保持更新,因为ImageMagick、FFmpeg自身的历史漏洞也常导致RCE。
文件上传必须做到三重隔离
上传点导致RCE通常因为文件被保存后又被当成脚本执行。修复不是简单检查扩展名。第一重,文件内容检测:图片用 getimagesize 或ImageIO读取验证,文档用文件头魔数校验,不允许仅凭Content-Type判断。第二重,存储隔离:上传目录必须关闭脚本执行权限。Nginx中对该目录配置 location ~* \.(php|jsp|py|sh)$ { deny all; },Apache中用 php_admin_flag engine off 或直接放在Web根目录之外。第三重,访问隔离:上传文件通过独立域名或CDN提供,该域名不解析任何动态脚本。
如果业务允许上传SVG、PDF、Office文档,要额外注意这些格式本身可以内嵌脚本或触发解析器漏洞。SVG应作为纯文本处理并过滤 script、foreignObject 等标签,PDF和Office文档建议用转换服务生成预览图,而不是直接返回原文件给用户浏览器。
中间件和框架的配置加固
很多RCE不是应用代码的问题,而是中间件默认配置暴露了管理接口。例如Tomcat的 /manager、/host-manager,WebLogic的 /console,JBoss的 /jmx-console,如果弱口令或未授权访问,直接就能部署war包拿权限。修复方案中必须包含:删除或重命名管理后台,限制访问IP,修改默认账号密码,关闭不必要的协议端口。
对于Struts2、Spring、Fastjson、Log4j这类框架组件,RCE漏洞往往来自组件自身的解析机制。修复方案要明确:所有第三方组件必须有版本清单,定期对照安全公告升级。不能等到漏洞被公开才行动,因为攻击者会在公告发布后几小时内扫描全网。对于无法立即升级的系统,应配置虚拟补丁,例如在WAF上拦截特定攻击载荷,但这只是临时措施。
系统层面的权限收敛
即使代码被攻破,如果Web进程权限足够低,攻击者能做的事情也有限。运行Web服务的用户不应是root或管理员,应使用独立低权限账号。PHP-FPM的 php.ini 中设置 disable_functions = system,exec,passthru,shell_exec,proc_open,popen,pcntl_exec,但要注意某些函数如 pcntl_exec 在CLI模式下可能被绕过。更彻底的方式是用容器或chroot限制文件系统可见范围。
Linux下可以配合SELinux或AppArmor给Web进程设置强制访问控制策略,即使代码执行了 /bin/sh,也无法读取敏感文件或发起外连。Windows服务器上应使用应用程序池独立账户,并设置目录ACL,禁止IIS进程对系统目录和Web目录之外的写权限。
出网限制比入站防护更关键
RCE攻击成功后,攻击者通常需要下载恶意工具、反弹shell、外传数据。如果服务器出网被严格限制,攻击链条就会中断。修复方案中应包含:Web服务器只允许向必要的内部服务(如数据库、缓存、消息队列)发起连接,禁止主动访问公网。通过防火墙或安全组配置出站白名单,DNS解析也走内部DNS服务器。反弹shell常用TCP、UDP、ICMP隧道,限制出站协议和端口能大幅提高攻击成本。
对于需要从外部拉取资源的业务,例如支付回调、第三方API,应通过代理服务器统一出口,代理上做域名白名单和内容过滤,不让Web服务器直接访问任意公网地址。
日志和监控要能发现RCE尝试
修复漏洞只是防守的一半,另一半是假设漏洞仍然存在,要能及时发现攻击行为。Web访问日志中记录完整请求参数,应用日志中记录危险函数调用和外部命令执行。监控规则关注:请求中包含 eval、base64_decode、cmd.exe、/bin/sh、wget http、curl http 等特征;单个IP短时间内尝试多个参数注入;服务器出现异常进程或异常外连。
对于Java应用,可以在JVM层面开启安全管理器或使用RASP(运行时应用自我保护)技术,在 Runtime.exec、ProcessBuilder.start、表达式引擎执行时进行拦截。RASP的优势是能感知应用上下文,区分正常业务调用和攻击调用,误报率比WAF低。但RASP本身有性能损耗,需要先在测试环境压测。
修复后的验证不能只看扫描器结果
很多团队以为跑一遍AWVS或Nessus没报高危就完事了,实际上扫描器对逻辑型RCE和需要多步构造的利用链覆盖有限。修复完成后,应使用专门的漏洞复现工具手动验证,例如针对Struts2的检测脚本、针对Log4j的JNDI探测、针对Fastjson的DNSLog回显。验证时要覆盖所有已知入口:GET、POST、Cookie、Header、文件上传、API接口。
更严格的做法是引入自动化安全测试,在CI/CD流水线中加入SAST(静态应用安全测试)和DAST(动态应用安全测试)。SAST扫描代码中的危险函数和污点传播路径,DAST在测试环境对运行中的应用发起攻击。两者结合能覆盖开发和运行两个阶段。
建立应急响应和复盘机制
如果已经发生RCE入侵,修复漏洞后还要做彻底排查。检查Web目录下是否有新增的webshell文件,检查计划任务、启动项、系统账号是否有异常,检查数据库是否被拖取或篡改。攻击者可能留下多个后门,只删一个webshell没有意义。必要时从干净备份重建系统,并重置所有密码和密钥。
复盘时要回答三个问题:漏洞是怎么引入的,为什么测试和上线前没发现,同类问题在哪些系统还存在。把修复方案沉淀成开发规范,例如禁止在业务代码中直接调用系统命令,所有外部输入必须经过统一校验层,第三方组件升级纳入发布流程。只有把单点修复变成流程约束,RCE才不会反复出现。
远程代码执行漏洞的修复从来不是打一个补丁那么简单。它要求从代码编写习惯、框架配置、系统权限、网络策略到监控响应形成完整的闭环。任何一个环节松懈,攻击者就能找到新的执行路径。把“不信任任何输入、不保留多余权限、不允许任意出网”作为三条铁律,比记住一百个CVE编号更有用。
