命令执行过滤绕过:为什么黑名单会失败¶
本文是命令执行与命令注入的配套指南,只用于自有系统、明确授权环境和本地 CTF 靶场。示例仅使用
printf、whoami等无破坏命令,不涉及持久化、反向连接、权限提升或破坏操作。
一、本文讨论的“绕过”是什么¶
这里的重点不是收集可直接套用的字符串,而是理解一个工程事实:
当不可信输入仍会进入 Shell 时,黑名单必须正确模拟所有解析层,才能判断输入最终会变成什么。这通常做不到。
一个请求可能依次经历:
过滤器若只看某一层的原始字符串,后续层可能重新生成被禁止的空白、分隔符、命令名或选项。正确目标不是扩充黑名单,而是让数据根本不进入解释器。
二、本地观察方法¶
可在隔离的 POSIX 靶场中使用以下玩具代码观察过滤缺陷。不要把它部署到可被他人访问的环境:
<?php
// 故意有漏洞,只用于本地 CTF。
$value = $_GET['value'] ?? 'lab';
$blocked = [';', '&&', 'whoami'];
foreach ($blocked as $token) {
if (str_contains($value, $token)) {
http_response_code(400);
exit('blocked');
}
}
echo '<pre>' . htmlspecialchars(
shell_exec("printf 'Value: " . $value . "\n'") ?? '',
ENT_QUOTES,
'UTF-8'
) . '</pre>';
这个过滤器即使继续添加关键词,也无法证明安全,因为输入仍在单引号引用和命令字符串中。下面各节按“Shell 如何产生新语法”分类分析失败原因。
三、分隔符与换行¶
3.1 为什么只拦截分号不够¶
Shell 通常有多种组织命令的方式:顺序、条件执行、后台执行、管道和实际换行。过滤器可能禁止 ;,却遗漏换行或其他控制运算符。
本地实验可以只观察无害输出,例如在 POSIX Shell 中比较:
它们的字符不同,但都可能导致多个命令被解释。HTTP 参数中的换行还可能以编码形式到达应用,并在解码后才变成真实字符。
3.2 防御结论¶
不要尝试列完所有分隔符。使用参数数组启动固定程序,不经过 Shell;对只需显示文本的功能直接使用语言 API。
四、空白替代与字段分割¶
Shell 并非只把普通空格视为参数边界。制表符、换行、环境中的字段分隔规则以及展开结果都可能产生参数切分。某些 Shell 还支持用特殊展开生成空白。
本地无害示例:
第二行中的 <TAB> 表示一个真实制表符,不是字面文本。过滤普通空格不会阻止参数边界出现,而且错误过滤还可能破坏合法的国际化输入。
结构性修复是把每个参数作为数组元素传给进程 API。此时空格和制表符只是该参数中的数据,不参与 Shell 分词。
五、引用、转义与字符串拼接¶
Shell 的单引号、双引号和转义规则可以把多个片段组合成同一个单词,也可以让部分字符保留语法意义。命令名不一定在原始输入中连续出现:
在常见 POSIX Shell 中,这些片段可能在解析后组合为同一个命令名。不同 Shell 的行为并不完全相同,因此关键词搜索既容易漏报,也容易误伤包含相同子串的正常数据。
如果代码试图用固定引号包住输入,攻击者关注的是如何结束该引用上下文。escapeshellarg() 比手写替换可靠,但仍要考虑平台差异和参数注入;优先删除 Shell 层。
六、通配符与 Glob 展开¶
*、? 和字符集合可由 Shell 展开为现有文件名。最终参数取决于工作目录内容,而不一定出现在输入字符串中。
在专用空目录中可安全观察:
第一行展示目录条目;第二行只有目录中存在匹配名称时才会展开。风险在于过滤器审查的是模式,被调用程序接收的却是展开后的文件名。文件上传功能还可能让用户影响可匹配名称。
参数数组通常不会执行 Shell glob。若业务确实需要文件匹配,应由应用使用目录 API 枚举文件,并对得到的路径执行授权与边界检查。
七、编码、解码与规范化¶
7.1 多层解码¶
Web 服务器、框架、中间件和业务代码可能分别执行 URL 解码、表单解码或 JSON 反转义。如果过滤发生在第一次解码前,后续解码可能恢复特殊字符;如果同一数据被解码两次,也可能产生意外结果。
7.2 Unicode 与字符集¶
全角字符、兼容字符、非断行空格和不同 Unicode 规范化形式可能在某一层被映射。操作系统命令行、日志和数据库采用不同编码时,安全判断与实际执行结果可能不一致。
7.3 正确顺序¶
推荐的数据处理顺序是:
- 在边界处按一种明确字符集解码一次。
- 若业务需要,执行明确且唯一的 Unicode 规范化。
- 按业务数据模型验证长度、字符集和结构。
- 将值作为单独参数传递,不再进行字符串求值。
“先过滤危险字符,再不断解码”是反模式。日志应同时保留请求关联信息和安全的规范化摘要,避免原始控制字符污染日志格式。
八、环境变量与 Shell 展开¶
Shell 可以展开环境变量,变量值可能生成命令片段、路径或空白。POSIX 常见 $NAME/${NAME},cmd.exe 常见 %NAME%,PowerShell 使用 $env:NAME。
本地无害观察:
输入中未出现环境变量的实际内容,但 Shell 在执行前将其替换。更危险的是运行时相关变量可能影响动态库、解释器模块、代理或配置文件加载。
进程启动时应提供最小且固定的环境;不要继承请求可控值。即便环境已最小化,也不能把“删除几个变量”当作命令注入修复。
九、替代工具与等价能力¶
关键词黑名单常围绕某个命令名编写,但系统通常有多种工具和内置命令提供相近能力。即使禁止外部程序,Shell 内建、语言运行时或脚本解释器也可能完成同类动作。
本地环境中,下面两个命令只是说明“不同程序可产生类似文本输出”:
不断加入工具名会导致依赖操作系统版本、已安装软件和 PATH,并产生无法维护的名单。应允许固定业务动作,而不是禁止已知危险动作。例如用户选择 identity 时,服务器映射到固定的 ['/usr/bin/whoami'];任何其他动作都不存在。
十、嵌套解释器与命令替换¶
当命令启动另一个解释器时,过滤器需要同时理解每一层语法:
POSIX Shell 的命令替换会先运行内部命令,再把输出放回外层命令。下面示例只输出当前用户名:
原始字符串中即使没有命令分隔符,也发生了第二次执行。类似风险存在于反引号、PowerShell 子表达式、批处理变量扩展、构建工具和模板表达式中。
修复时应逐层删除解释器。若业务需要计算或格式化,使用明确的库 API;若需要执行固定作业,使用结构化消息描述作业类型和参数。
十一、Windows 变体¶
Windows 上至少要区分进程创建 API、cmd.exe 和 PowerShell。
11.1 cmd.exe¶
cmd.exe 支持命令连接、管道、重定向、百分号变量、延迟扩展和插入符转义。例如以下本地命令只打印文本和身份:
其中 printf 并非 Windows 默认命令,仅在安装了相应工具的实验环境中可用。批处理文件中的 %、!、^、括号和引号可能经历额外解析;从 PHP、Java或 C 运行时生成 Windows 命令行时,又会先经历各自的参数序列化规则。
11.2 PowerShell¶
PowerShell 具有变量、子表达式、调用运算符、脚本块和对象管道。以下示例仅观察本地身份:
为 cmd.exe 编写的字符替换不能迁移成 PowerShell 防护,反之亦然。也不要通过 powershell -Command <用户输入> 提供“受限命令”功能。
11.3 Windows 防御要点¶
- 使用运行库提供的参数数组 API,并确认该版本不会隐式调用
cmd.exe。 - 固定可执行文件完整路径,禁止用户控制脚本或模块搜索路径。
- 不使用
.bat/.cmd作为需要强参数隔离的安全边界,因为它们由命令解释器解析。 - 对目标程序的 Windows 参数规则做集成测试,特别是引号、尾随反斜杠和空参数。
- 固定最小环境,限制 PowerShell 和脚本宿主并不能替代修复调用代码。
十二、黑名单为什么在工程上不可证明¶
黑名单面临的不是一个固定字符集合,而是多个上下文相关的语法系统:
- 相同字符在引用内外含义不同。
- 不同 Shell、版本和选项启用不同展开。
- 规范化和解码可在过滤后生成新字符。
- 文件系统与环境变量能参与生成最终参数。
- 被调用程序拥有自己的选项和配置语言。
- 新增工具、升级依赖或改变操作系统会扩大可用语法。
因此,黑名单适合做告警和遥测信号,不适合作为执行边界。WAF 规则也只能提供纵深检测,不能修复应用中的字符串命令调用。
十三、结构性修复¶
按优先级实施:
- 用语言或框架库替代外部程序。
- 用户只提交业务数据或动作 ID,服务端映射到固定实现。
- 必须创建进程时使用参数数组、固定绝对路径并绕过 Shell。
- 按业务语义验证每个参数,处理目标程序的选项注入。
- 固定最小环境、工作目录、超时、输出大小和并发。
- 在低权限、隔离、受限网络的工作进程中运行高风险任务。
- 用日志和告警监测元字符类别,但不把检测规则当作唯一防线。
安全的固定映射示例见命令执行与命令注入。
十四、本地测试清单¶
测试重点是覆盖“语法类别”和“解析层”,而不是复制某份 payload 列表:
- 确认测试范围仅包含自有系统、授权环境和隔离靶场。
- 枚举所有 Shell、进程、脚本、模板、表达式和动态求值入口。
- 记录输入经历的 URL、JSON、字符集和 Unicode 解码/规范化顺序。
- 分别覆盖分隔符、真实换行、空白、引用、转义和命令替换。
- 检查通配符是否受工作目录或上传文件名影响。
- 检查环境变量、
PATH、工作目录和运行时加载配置。 - 检查替代工具、Shell 内建和嵌套解释器是否让关键词规则失效。
- 在实际部署的 POSIX Shell、
cmd.exe或 PowerShell 上分别测试,不假设语法相同。 - 检查不经过 Shell 时仍存在的选项注入和危险文件路径。
- 使用
printf、whoami、测试日志等无害信号,不使用外连、持久化或破坏动作。 - 验证无回显场景的超时和重复采样不会影响其他用户或服务稳定性。
- 确认修复后输入作为单个参数到达测试桩,并为边界值添加回归测试。
十五、防御检查清单¶
- 代码中不存在将不可信输入拼入命令字符串的路径。
- 没有通过
/bin/sh -c、cmd.exe /c或 PowerShell-Command转发用户输入。 - 可执行文件和动作集合由服务端固定,用户不能控制程序名。
- 参数数组 API 在目标操作系统和运行库版本上经过验证。
- 输入验证表达业务允许格式,而不是一组禁止字符。
- 目标程序的选项、配置、插件和输出路径受到限制。
- 子进程采用低权限账号、最小环境、固定目录和资源上限。
- WAF/黑名单仅用于监测,并有应用层结构性修复作为安全边界。
- 日志记录动作 ID、退出码和关联 ID,不泄露敏感参数或允许日志注入。
- CI 中包含 POSIX/Windows 适用平台的参数边界回归测试。
十六、总结¶
分隔符、空白替代、引用拼接、通配展开、编码规范化、环境变量、替代工具和嵌套解释器只是黑名单失败的不同表现。共同根因始终是:不可信数据仍被送进了能够解释语法的组件。可靠防御应删除解释器边界,使用固定动作、参数数组和业务允许模型,再通过最小权限与隔离降低残余风险。