输入拼进 shell 命令,元字符就成了语法。
;、&、|、反引号在不同平台上含义不同,但只要能碰到 shell 就能插进去。
根治的办法是不经过 shell,参数作为独立元素传进去。
上一节是数据变成 SQL,这一节是数据变成命令——宿主换成了操作系统。
一、元字符就是语法
服务端有时候要调外部程序:查 DNS、转图片、压缩文件。
如果代码写成 system("ping -c 1 " + host),那个 host 就是拼进去的。
host 一旦能塞进 ;,shell 就会把它当成「命令分隔符」:前半句还是 ping,后半句是攻击者的命令,
两条都会执行。
同一类元字符还有 &(后台执行/顺序执行)、|(管道)、反引号和 $()(命令替换)。
它们都是 shell 的语法,不是普通字符。
所以「输入里有没有危险字符」这个问题,答案取决于这段输入会被谁解析。
要判断的是解析器,不是字符表。
二、一条实测,两处意外
Demo 里五种输入各跑一遍:正常、分号、和号、管道、反引号。结果有两处和直觉不一样。
第一处:分号在本机没注进去。
因为本机的 shell 是 Windows 的 cmd.exe,它的命令分隔符是 &,不是 ;。
换成 Unix 的 bash,; 立刻就生效。
第二处:反引号也没生效,同样因为 cmd.exe 不做命令替换。真正成功的是「和号」和「管道」这两行。
这说明一件重要的事:同一个漏洞,换个平台,可用的载荷就不一样。
写测试用例时不能只按自己的机器来。
三、不经过 shell 就好了
修正版把命令改成列表:[python, echo, 查询, 输入],shell 完全不参与。
同样五种输入,全部被当成普通字符串原样输出,一行都没注进去。
这一条的机制值得记:列表传参时,进程收到的是一个已经切好的参数数组,没有「再解析一遍」的环节,
元字符就只是元字符。
如果非要用 shell(比如确实需要管道),就只能自己转义——而转义是最容易漏的,
因为每个 shell 的规则都不一样。能用列表就别用 shell。
还有一条底线:不要用固定的、写死的命令去执行用户可控的文件名。
真实案例里出事的往往是「转码」「解压」这种看起来无害的工具。
四、动手 Demo:五种输入,哪几种真注进去了
用 echo 和 whoami 两个无副作用命令做演示。
先把输入拼进 shell 命令跑一遍,看 whoami 有没有被顺带执行;再换成列表传参重跑。
# 朴素版:拼进 shell
cmd = u'echo 查询 %s' % user_input
subprocess.run(cmd, shell=True, capture_output=True)
# 修正版:不经过 shell
subprocess.run([u'echo', u'查询', user_input], capture_output=True)
实跑输出:
--------------------------------------------------------------------
OS 命令注入:分号之后那半句谁执行
--------------------------------------------------------------------
这一段用 `echo` 和 `whoami` 两个无副作用命令做演示,不动任何文件。
朴素版:把用户输入直接拼进 shell 命令
手法 用户输入 shell 实际输出 注进去了吗
正常输入 8.8.8.8 查询 8.8.8.8 否
分号 8.8.8.8; whoami 查询 8.8.8.8; whoami 否
和号 8.8.8.8 & whoami 查询 8.8.8.8
/ admin 是
管道 8.8.8.8 | whoami admin 是
反引号 8.8.8.8 `whoami` 查询 8.8.8.8 `whoami` 否
`whoami` 本来不该被调用。哪几行它真的跑了,看最后一列。
有一件事这里必须说明白:**本机是 Windows,shell 是 cmd.exe**。
分号在 Unix 的 shell 里是命令分隔符,在 cmd.exe 里不是——所以上面「分号」那行没注进去,换成「和号」才成功。同一段代码,换个平台注入面就不一样。
修正版:不经过 shell,参数作为一个独立元素传进去
正常输入 查询 8.8.8.8
分号 查询 8.8.8.8; whoami
和号 查询 8.8.8.8 & whoami
管道 查询 8.8.8.8 | whoami
反引号 查询 8.8.8.8 `whoami`
区别 列表传参时 shell 不参与解析
元字符会怎样 被当成普通字符原样输出
这就是为什么各语言都提供「参数数组」形式的进程调用。用它,元字符就只是元字符,没有特殊含义。
最后一列「注进去了吗」有两行是「是」:和号 和 管道。
它们的输出里只剩 admin,whoami 真的跑了。
分号 那一行是「否」——因为本机是 Windows,cmd.exe 不把 ; 当分隔符。
这条差异本身就是收获:载荷要跟着平台换。
反引号 也是「否」,同样是 cmd.exe 不做命令替换。在 bash 上这两行都会成功。
修正版五行的输出里都带着完整输入,包括 ; whoami、& whoami——它们被当成普通字符了,
一个都没执行。
30 秒实验
- 把
whoami换成一个更有说服力的(比如列出当前目录),看输出差多少。 - 试试
%PATH%(Windows 的环境变量展开)能不能注进去——它是另一类元字符。 - 想一想:如果你的服务跑在 Linux 容器里,这个 Demo 的结论会怎么变?
常见误区
- 「过滤
;就安全了」——&、|、反引号、$()都能用。 - 「我的代码不调外部命令,所以无关」——转码、压缩、查 DNS 都算调命令。
- 「同一个 payload 在哪都能用」——平台不同,分隔符和替换语法都不同。
自测三问(点开对答案)
1. 命令注入的成因是什么? —— 用户输入被拼进 shell 命令,输入里的元字符被当成语法解析。 2. 为什么列表传参能根治? —— shell 不参与,进程收到的是切好的参数数组,元字符没有特殊含义。 3. 为什么分号在本机没生效? —— 本机 shell 是 cmd.exe,命令分隔符是 `&` 不是 `;`。命令是一类宿主。还有一类更隐蔽:输入被拼进了 HTTP 首部。
- ☐ (本节完,下一节 11.2.4 HTTP首部注入攻击)