命令注入(Command Injection)是指应用程序将不可信的用户输入拼接到操作系统命令中执行,攻击者借此在服务器上执行任意命令,危害等级通常为最高危。本篇从代码层、系统层、WAF层三个维度给出可直接落地的命令注入防御配置方案。
适用场景
- 业务必须调用系统命令的场景:文件转换、压缩解压、ping 检测、PDF 生成等
- 老旧系统无法立即重构、需要过渡性防护的存量代码
- 需要在网关层兜底拦截注入 payload 的运维团队
前置条件
- 一台 Linux 服务器(本文以 Debian/Ubuntu 为例)
- PHP 7.4+ 或 Python 3.8+ 运行环境
- 已部署 Nginx 与 ModSecurity 3.x(WAF 层可选)
- 具备站点代码修改权限
命令注入的原理
当代码出现类似 system("ping -c 4 " . $ip) 的写法时,攻击者输入 127.0.0.1; cat /etc/passwd,shell 会把分号解释为命令分隔符,额外执行第二条命令。常用注入元字符包括 ;、|、&&、||、$()、反引号与换行符。防御核心是三点:绝不拼接字符串、降低执行权限、网关层兜底。
操作步骤
第一步:代码层安全改造(根治手段)
PHP 使用 escapeshellarg 对参数整体转义,并先做白名单校验:
<?php
// 错误示范:system("ping -c 4 " . $_GET['ip']);
$ip = $_GET['ip'] ?? '';
// 白名单校验:只允许合法 IP
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
http_response_code(400);
exit('invalid ip');
}
$arg = escapeshellarg($ip); // 关键:整体转义为单个安全参数
system("ping -c 4 " . $arg);
Python 优先使用 subprocess 列表参数模式,并保持 shell=False:
import subprocess, ipaddress
ip = input("ip: ")
ipaddress.ip_address(ip) # 非法输入直接抛异常
# 关键:列表参数 + shell=False,参数不会被 shell 二次解析
subprocess.run(["ping", "-c", "4", ip], shell=False, timeout=10)
- 能用标准库替代的绝不调用系统命令(如用 socket 检测端口而非执行 ping)
- 禁止
shell=True与 f-string/格式化拼接命令 - 先白名单校验、再参数转义,两层防护缺一不可
第二步:系统层禁用危险函数(PHP)
修改 php.ini,禁用命令执行类函数,即使代码存在漏洞也无法拿到 shell:
; /etc/php/8.2/fpm/php.ini
disable_functions = exec,system,passthru,shell_exec,proc_open,popen,pcntl_exec
systemctl restart php8.2-fpm
php -r "var_dump(function_exists('exec'));" # 输出 bool(false) 即生效
注意:若业务确实依赖 exec 类函数,跳过本步,依靠代码层与 WAF 层防护,同时执行第四步降权。
第三步:ModSecurity WAF 兜底规则
OWASP CRS 已含 932xxx 命令注入规则集,以下自建规则用于补充拦截:
# /etc/nginx/modsec/custom-rules.conf
# 拦截分号/管道符后接常见命令特征
SecRule ARGS "@rx (?i)(?:;|\||&&)\s*(?:cat|id|whoami|wget|curl|nc|bash|sh|python|perl)\b" \
"id:1000101,phase:2,deny,status:403,msg:'Command Injection Attempt',log"
# 拦截命令替换语法 $() 与反引号
SecRule ARGS "@rx (?:\$\(|`)" \
"id:1000102,phase:2,deny,status:403,msg:'Command Substitution Detected',log"
nginx -t && systemctl reload nginx
tail -f /var/log/modsec_audit.log # 观察拦截记录
第四步:最小权限降权
运行 Web 服务的账号禁止 sudo、收敛目录写权限,即使被注入也难以提权:
# 确认运行账号
ps aux | grep -E 'php-fpm|gunicorn' | head -3
# 锁定 sudo:确保 sudoers 中不存在 www-data 的 ALL 授权行
visudo -f /etc/sudoers.d/www-data
# 收敛站点目录写权限
chown -R www-data:www-data /var/www/html/uploads
chmod -R 750 /var/www/html/uploads
配置验证
用注入 payload 实测拦截效果:
# 假设业务存在 /ping.php?ip= 参数
curl -s "https://your-site.com/ping.php?ip=127.0.0.1;cat%20/etc/passwd" -o /dev/null -w "%{http_code}\n"
# 期望:代码层返回 400,或 WAF 层返回 403
curl -s "https://your-site.com/ping.php?ip=\$(id)" -o /dev/null -w "%{http_code}\n"
# 期望:同样被拦截
- 确认
php -r "var_dump(function_exists('exec'));"返回 false - 检查 ModSecurity 审计日志中出现 id:1000101 命中记录
常见问题
FAQ1:已经用了 escapeshellarg,为什么还被报命令注入?
escapeshellarg 只保护参数位置。若用户可控部分充当命令名(如 system($_GET['cmd'])),转义无效;Windows 下 escapeshellarg 行为与 Linux 不同,管道符仍可能被解析。正确做法是命令名写死在代码里,用户输入只允许出现在参数位置。
FAQ2:disable_functions 会不会影响正常业务?
只禁用 exec 族函数,不影响文件操作、数据库与网络请求。部署前用 grep -rn "system\|exec\|shell_exec" /var/www/html 排查调用点;若 Composer 依赖包需要 proc_open(部分图片处理库),可保留 proc_open 并配合 WAF 规则兜底。
总结
命令注入防御遵循纵深防御:代码层用白名单校验加参数转义根治漏洞,系统层用 disable_functions 与降权限制爆炸半径,WAF 层用规则兜底拦截未知 payload。三层同时到位后,即使是历史遗留代码也能获得有效保护。