命令注入与黑名单绕过
命令注入的本质是 用户输入被拼进一个由 Shell 解释的字符串。只要最终执行走的是 bash -c 一类的解释路径,换行、变量展开、编码变形都可能是绕过黑名单的入口。
命令注入验证会真实执行系统命令,只能在授权靶场或明确授权的测试中进行。
常见根因模式
| 语言/场景 | 缺陷写法 |
|---|---|
| PHP | shell_exec("cat logs/" . $_POST['file']) |
| Shell 包装 | bash -c "cmd ${VAR}",VAR 来自外部输入 |
| Python | subprocess.check_output("ping -c 1 " + host, shell=True) |
共同点:shell=True 或等价的字符串拼接调用,输入中的元字符会被 Shell 解释成命令分隔符、管道或替换语法。
黑名单绕过手法
换行与空白字符
换行符 %0a:多数黑名单只过滤 ; & | 这几个显式分隔符,换行符同样是合法的命令分隔符:
file=logs/app.log%0aid
URL 解码后实际执行 cat logs/app.log 和 id 两条命令。
空格用 Tab 替代:空格被过滤时用 %09(Tab),Shell 会把 Tab 当参数分隔符:
%0a%09id%09>&2
base64 编码整条命令
payload 中特殊字符被逐个过滤时,把命令整体 base64 编码再解码执行,注入串里只保留管道和字母数字,引号、分号、空格全部消失:
echo aWQ= | base64 -d | bash
命令长度受限的环境里,这层编码同时能 缩短注入串:长命令放在别处,注入串只留一层解码调用。
分隔符稳定性差分
;、&&、|、$() 在不同包装方式(PHP shell_exec、bash -c、Python shell=True)下成功率不同——有的包装层会剥离某类字符,有的上下文里 && 被当普通文本。逐一测试并记录响应差分:
| 分隔符 | 测试输入 | 判断依据 |
|---|---|---|
; | x;id | 回显是否包含 uid= |
&& | x&&id | 同上 |
| | x|id | 管道后半段输出是否出现 |
$() | $(id) | 替换结果是否拼进原命令输出 |
%0a | x%0aid | 换行后的命令是否执行 |
先用 id 这类必然产生 uid= 特征输出的命令做探测,确认可用分隔符后再上真实 payload。
无回显技巧
借 stderr 回显
$(cmd>&2) 把命令输出写到标准错误。部分框架只吞掉 stdout,但 stderr 会进入错误页或日志,从而带出结果:
$(id>&2)
配合 base64 包裹框架,规避对特殊字符的检查:
$(echo aWQ=|base64 -d|bash >&2)
输出落盘再读
os.system() 这类调用没有回显通道时,把输出重定向到可读取的位置,再用文件读取功能取回:
RUN id > /tmp/out.txt 2>&1
之后通过任意文件读取/下载接口读 /tmp/out.txt。
复核要点
| 检查点 | 说明 |
|---|---|
| 是否真的走了 Shell | shell=True / shell_exec / bash -c 才可注入,argv 数组不可 |
| 分隔符验证 | 逐个测试 ; && | $() %0a,记录差分 |
| 字符过滤面 | 空格、引号、分号分别被过滤时选对应替代方案 |
| 回显通道 | 直接回显 → stderr → 落盘,按顺序尝试 |
先用 id 级短命令验证,确认执行路径后再上真实 payload,避免误伤目标环境。
防御建议
- 避免 Shell 拼接:能不调用 Shell 就不调用,直接用库函数完成同等功能。
- 必须调用外部命令时使用参数化(argv 数组)并保持
shell=False。 - 对输入做白名单校验(如文件名只允许
[A-Za-z0-9._-]),黑名单不可作为唯一防线。 - 确需在 Shell 层转义时使用
printf '%q',不要手写转义规则。 - 对
;、|、$()、反引号、换行等元字符进日志的位置保持监控与告警。