很多同学第一次见到 RCE(Remote Code Execution,远程代码执行),会以为必须配得上一个“惊天 0day”。真到业务里看事故单,起点往往土得令人发指:一个网络诊断页,把用户输入的 IP 拼进ping;一个后台“测试过滤器”,把表达式丢进eval;一个运维接口,把文件名丢进Runtime.exec却还走了 shell。
命令注入和代码注入是两条最接近“从参数到 RCE”的直道。它们和 SQL 注入同族——都是不可信输入进了解释器——只是解释器换成了系统 shell、或语言自己的求值器。
一、先把两个名词掰开
1. 命令注入(OS Command Injection)
应用拼出一条要交给操作系统执行的命令行,用户输入改变了命令的结构或追加了新命令。
例如本意:
ping -c 4 用户输入的主机若用户输入被拼进 shell,攻击者可能用命令分隔符塞进额外指令,让系统在ping之外还执行别的东西。
2. 代码注入(Code Injection)
用户输入被送进语言/模板/表达式引擎执行,例如:
eval、assert、动态Function- 不安全的模板引擎(SSTI 常单列,本质近亲)
- 反序列化触发的魔术方法调用(常单列,结果也是 RCE)
Runtime反射乱调
它不一定经过/bin/sh,但一样能在进程里跑任意逻辑。
3. 一张对照表
| 命令注入 | 代码注入 | |
|---|---|---|
| 解释器 | shell / 命令行 | 语言求值器/模板等 |
| 典型函数 | system、popen、exec(走 shell 时) | eval、动态代码编译 |
| 经典入口 | ping、转换、打印、备份脚本 | 规则引擎、计算器、调试开关 |
| 修复关键 | 不调 shell;参数数组 API;白名单 | 删除求值;沙箱极难做对 |
下面两条线都会写到;实战里它们还经常接力——代码注入里再拼命令,或命令注入里再写文件落地。
二、为什么偏偏是 ping 参数
1. 业务理由很充分
网络设备面板、IDC 管理、监控系统、售后诊断页,都需要“帮用户 ping 一下”。产品经理觉得这是运维友好;开发随手:
system("ping " + ip);于是安全问题从第一天就埋好了。
2. Shell 元字符是引爆点
若命令经过 shell 解析,用户输入中的分隔与链式符号就可能改变语义(具体哪些字符危险,因 Windowscmd/ PowerShell / Linuxsh而异)。
教学与靶场里会演示“追加命令”的效果;真实授权测试只需证明:输入能改变命令语义或触发额外执行,不必上破坏性指令。
3. 过滤为什么总失败
常见假修复:
- 黑名单删掉
;、|、&——总有漏网与编码变体; - 只允许数字和点——却忘了 IPv6、主机名、或旁路参数位置;
- 正则看起来很严,实际只校验了部分分支;
- 换行、管道、命令替换在不同层被解码回来。
过滤命令字符串,几乎注定输给坚持不懈的人。正解是:不要用 shell 拼命令。
4. 正确实现 ping 的思路
- 严格校验:IPv4/IPv6/主机名白名单规则;
- 使用参数数组形式的进程 API(不经过 shell);
- 超时与输出长度限制;
- 仅管理员可用,并审计;
- 能改成“对固定探针地址探测”就不要让用户指定任意主机。
能做到前两条,ping 页从“RCE 温床”变回“诊断工具”。
三、命令注入的一般形态
1. 拼接位置
不止主机名:
- 文件名、路径(备份、压缩、转换);
- 邮件地址(老脚本发给
mail); - 图片尺寸、视频码率(ImageMagick、ffmpeg 命令行);
- 打印机名、共享路径。
审计时搜:system、popen、shell_exec、Runtime.getRuntime().exec(注意重载是否走 shell)、ProcessBuilder用法、subprocess的shell=True、os.system。
2. 无回显与有回显
有的接口把命令输出回页面——好证。
有的只返回成功失败——要用时间差、外带通道(授权下对你控制的接收端)等谨慎证明,生产环境避免大面积盲打。
3. 到 RCE 的“短路径”
命令注入一旦成立,通常已经是 RCE:你能执行的就是系统命令。
后续是权限多大(www-data 还是 root)、能不能写 web 目录、能不能碰云密钥——那是影响评估,不是“还没 RCE”。
报告里写:命令注入导致远程命令执行,并注明运行身份与可达资产,即可。
四、代码注入:eval 家族与朋友们
1. 为什么开发爱用 eval
规则引擎、动态公式、功能开关、灰度表达式、竞品“脚本化配置”——都想让用户或运营写一段逻辑。于是:
eval(user_input)或稍加伪装的动态编译执行。这等于把解释器端口暴露给输入。
2. SSTI 模板注入(近亲)
用户输入进了服务端模板引擎并被当成模板语法解析。表现像代码注入,常能读配置或执行命令。修法是:用户内容当数据渲染,禁止用户模板进服务端引擎;或使用沙箱极严的引擎(仍要谨慎)。
3. 反序列化
不可信数据反序列化导致任意代码执行——许多报告直接写 RCE。防御是:不碰不可信反序列化;换安全格式(JSON)+ 校验;升级补丁。专题可另写,这里只强调:它和“从参数到 RCE”同一终点。
4. 修复代码注入
- 能删 eval 就删;
- 换成白名单操作符的表达式解析器(自己做要极慎重);
- 运营脚本走人工审核发布,不进线上动态执行;
- 语言沙箱不要迷信,历史逃逸太多。
五、从“参数”到 RCE 的链路怎么讲给领导听
① 功能需要调用外部命令或动态代码 ② 开发用字符串拼接把用户输入塞进去 ③ 攻击者构造输入改变语义 ④ 进程以 Web/应用身份执行任意命令或代码 ⑤ 读配置、弹 shell、打内网、拿云凭证……打断点:
- ① 换安全 API 或不让用户指定;
- ② 禁止拼接;
- ③ 纵深:最小权限、只读文件系统;
- ④⑤ 检测与分段。
六、授权测试方法
1. 发现
功能关键词:诊断、ping、traceroute、nslookup、转换、渲染、打印、备份、升级包处理。
白盒搜危险函数与shell=True。
2. 确认
在授权环境提交无害差异输入(如改变睡眠时间、回显固定标记),证明注入点。
避免rm、挖矿、大规模扫内网。
3. 影响评估
谁身份执行?能否读业务配置?是否容器内?是否连到生产库?
给出级别:通常是高危/严重。
4. 报告
复现步骤最小化;修复给“参数数组示例”与“删除 eval”的明确建议;复测标准写清。
七、防御工程清单
命令执行类
- 禁止
shell=True/system拼用户输入 - 使用参数列表 API
- 主机/文件名等严格白名单
- 应用账号最小权限
- 上传与命令处理隔离
代码执行类
- 无
eval/assert处理用户数据 - 模板不渲染用户模板
- 表达式引擎白名单化
- 反序列化不接不可信源
检测
- Web 日志中的命令分隔特征(辅);
- WAF/RASP 规则(辅);
- 主机侧异常子进程:
www用户起bash/curl(参见 HIDS 文); - 云侧异常行为。
八、常见假修复与真翻车
“我转义了所有特殊字符。”
平台差异与二次解码会打脸;且业务字符可能被误伤。
“我用了 escapeshellarg。”
有帮助,但仍要正确用法与参数结构;复杂命令行依旧危险。能不用 shell 就不用。
“内网系统无所谓。”
内网 RCE 往往是勒索与漫游的跳板。
“容器里没事。”
容器逃逸与云元数据故事你我都写过;RCE 仍是事故起点。
“只允许数字。”
下一个需求就要支持域名,一放开又拼回去。
九、和 SQL 注入、XSS、SSRF 的关系
- SQL 注入:进数据库解释器;
- 命令/代码注入:进 OS 或语言解释器;
- XSS:进浏览器解释器;
- SSRF:进服务端请求器。
同一原则:数据与代码分离;用户输入只能当数据。
命令注入常与上传、文件名、图片处理链组合;代码注入常与“运营可配置脚本”组合。架构评审时把这些功能单独标红。
十、开发规范:可写进 Wiki 的几条
- 任何
system/exec/eval必须安全评审;默认拒绝。 - 子进程只允许参数数组形式,禁止拼接命令行字符串。
- 用户输入进入子进程前必须白名单校验。
- 禁止上线面向用户的动态代码执行功能;内部工具也要鉴权与审计。
- 发现一处拼接,回归测试留样例。
十一、学习路径
- 靶场命令注入关卡:理解分隔与无 shell API 的差异;
- 自己写坏再写好一个 ping(先拼接,再改 ProcessBuilder/参数数组);
- 搜公司代码库危险函数清单,出整改工单;
- 配一条 HIDS 规则:Web 用户起 shell 告警(预发验证)。
十二、收尾
从 ping 参数到 RCE,中间往往没有华丽漏洞编号,只有一次“图省事的拼接”。命令注入把输入交给 shell,代码注入把输入交给语言求值器——终点都是别人在你的进程里说话。
防住它们不靠更长的黑名单,靠三样老实东西:
不要用 shell 拼用户输入;
不要 eval 用户输入;
给应用最小权限,让出事时少丢一点。
今晚若只做一件事:在代码库搜system(、popen(、shell=True、eval(,打开每一处看有没有用户输入。
搜完,你对“我们离 RCE 有多远”的判断,会比听十个黑客故事扎实得多。
附:危险 API 速记(按语言自查)
- PHP:
system、shell_exec、passthru、proc_open、eval、assert - Python:
os.system、subprocess(..., shell=True)、eval、exec - Java:
Runtime.exec字符串版、脚本引擎 - Node:
child_process.exec、eval、vm使用不当 - 运维脚本:Bash 里未加引号的变量展开
附:修复优先级
| 优先级 | 动作 |
|---|---|
| P0 | 下线或修复可触发的注入点 |
| P1 | 改为无 shell 参数数组 + 白名单 |
| P2 | 删除 eval/动态代码能力 |
| P3 | 最小权限 + 子进程监控 |