2. 命令注入漏洞的识别与过滤绕过分析
漏洞代码与注入点定位
下面是一段典型的“网络诊断”接口,它把用户提交的 host 参数直接拼进 shell 命令:
# 漏洞版本:app.py
import subprocess
from flask import Flask, request
app = Flask(__name__)
@app.route("/ping")
def ping():
host = request.args.get("host", "")
# 危险点:字符串拼接后交给 shell 解释
cmd = "ping -c 1 " + host
out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
return "<pre>" + out.decode(errors="ignore") + "</pre>"
风险根源有两处:shell=True 让 /bin/sh 解析整条字符串;host 未做任何校验。shell 会把 ;、|、&&、`、$() 等当作命令分隔或替换语法,因此攻击者可以追加任意命令。
注入点与数据流
下图展示从 HTTP 请求到 shell 执行的数据流,以及黑名单过滤在其中的位置与绕过路径:
构造绕过 payload
假设开发者只过滤了 ;、|、&& 和反引号,但漏掉了换行符 %0a。shell 把换行视为命令分隔符,因此以下请求仍可执行任意命令:
# 绕过黑名单的请求(URL 编码后)
GET /ping?host=127.0.0.1%0acat%20/etc/passwd HTTP/1.1
# 解码后实际拼接的命令
ping -c 1 127.0.0.1
cat /etc/passwd
其他常见绕过手法包括:用 || 替代 |、用 & 替代 &&、用 $IFS 或 ${IFS} 替代空格、用 %09(Tab)替代空格。黑名单永远无法穷举所有 shell 元字符,因此它只能作为辅助手段,不能作为主要防线。
安全修复方案
修复的核心是消除 shell 解释层,并对外部输入做白名单约束:
# 修复版本:app_fixed.py
import re
import subprocess
from flask import Flask, request
app = Flask(__name__)
HOST_RE = re.compile(r"^[A-Za-z0-9.-]{1,253}$")
@app.route("/ping")
def ping():
host = request.args.get("host", "")
if not HOST_RE.match(host):
return "invalid host", 400
# shell=False,参数以列表传入,不经过 shell 解析
result = subprocess.run(
["ping", "-c", "1", host],
capture_output=True, text=True, timeout=5
)
return "<pre>" + (result.stdout or result.stderr) + "</pre>"
关键改动:shell=False 使参数作为独立 argv 传递,;、|、换行等字符不再具有特殊含义;正则白名单只允许合法主机名字符,从源头拒绝注入输入;timeout=5 防止命令挂起导致资源耗尽。
验证修复效果
| 测试输入 | 修复前行为 | 修复后行为 |
|---|---|---|
127.0.0.1 | 正常返回 ping 结果 | 正常返回 ping 结果 |
127.0.0.1;id | 执行 id 命令 | 400 invalid host |
127.0.0.1%0acat /etc/passwd | 读取 /etc/passwd | 400 invalid host |
127.0.0.1||whoami | 执行 whoami | 400 invalid host |
验证时优先使用
;id、%0aid、||id 三类 payload 覆盖分隔符、换行和逻辑或三种绕过路径,确认修复后均返回 400 且无命令输出。