命令注入防御实战:代码过滤与WAF规则配置指南

命令注入(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。三层同时到位后,即使是历史遗留代码也能获得有效保护。