CubeCTF 2025 Writeup

CubeCTF 2025 Writeup

July 25, 2025

Here are a few challenges I found interesting and fun to solve from CubeCTF 2025.

Discord

I got a really awesome picture from my friend on Discord, but then he deleted it! I asked someone for a program that could get those pictures back, but when I ran it, all it did was close Discord! Send help, I need that picture back!

Initial findings

From the disk image, I found a user named Admin. In Downloads, there are two executables: a Discord installer and encrypt.exe.

Running DIE on encrypt.exe shows it is a PyInstaller-packed Python script. I extracted it with pyinstxtractor_ng, which suggested encrypt.pyc as the entry point. Decompiling it gives the following:

import json
import os
from pathlib import Path
import psutil
from Cryptodome.Cipher import AES
from Cryptodome.Protocol.KDF import PBKDF2
from Cryptodome.Util.Padding import pad

def get_appdata_path() -> Path:
    if os.getenv('APPDATA') is None:
        raise RuntimeError('APPDATA environment variable not set??')
    return Path(str(os.getenv('APPDATA'))).resolve()

if __name__ == '__main__':
    for proc in psutil.process_iter():
        if proc.name() == 'Discord.exe':
            print(f'Killing Discord (pid {proc.pid})')
            try:
                proc.kill()
            except psutil.NoSuchProcess:
                print('Process is already dead, ignoring')

    sentry_path = get_appdata_path() / 'Discord' / 'sentry' / 'scope_v3.json'
    with open(sentry_path, 'rb') as f:
        sentry_data = json.load(f)

    user_id = sentry_data['scope']['user']['id']
    salt = b'BBBBBBBBBBBBBBBB'
    key = PBKDF2(str(user_id).encode(), salt, 32, 1000000)
    iv = b'BBBBBBBBBBBBBBBB'

    cache_path = get_appdata_path() / 'Discord' / 'Cache' / 'Cache_Data'
    print(f'Encrypting files in {cache_path}...')

    for file in cache_path.iterdir():
        if not file.is_file():
            continue
        if file.suffix == '.enc':
            print(f'Skipping {file} (already encrypted)')
            continue
        try:
            with open(file, 'rb') as fp1:
                data = fp1.read()
        except PermissionError:
            print(f'Skipping {file} (file open)')
            continue

        cipher = AES.new(key, AES.MODE_CBC, iv=iv)
        ciphertext = cipher.encrypt(pad(data, 16))
        print(f'Encrypting {file}...')
        with open(file.with_suffix('.enc'), 'wb') as fp2:
            fp2.write(ciphertext)
        file.unlink()

This script kills Discord, reads the user ID from the sentry scope file, and encrypts cache files with AES-CBC. The encrypted files are stored with a .enc extension.

Decryption

To recover the images, I derived the same key and decrypted all .enc files:

import json
from pathlib import Path
from Cryptodome.Cipher import AES
from Cryptodome.Protocol.KDF import PBKDF2
from Cryptodome.Util.Padding import unpad

def detect_file_type(data):
    if not data:
        return None

    signatures = {
        b'\xff\xd8\xff': 'jpg',
        b'\x89PNG\r\n\x1a\n': 'png',
        b'GIF87a': 'gif',
        b'GIF89a': 'gif',
        b'RIFF': 'webp',
    }

    for signature, ext in signatures.items():
        if data.startswith(signature):
            if signature == b'RIFF' and len(data) > 12 and data[8:12] == b'WEBP':
                return 'webp'
            return ext

    return None

base = Path('artifacts')
sentry_path = base / 'scope_v3.json'
cache_path = base / 'Cache_data'

with sentry_path.open('r', encoding='utf-8') as f:
    sentry_data = json.load(f)

user_id = sentry_data['scope']['user']['id']
salt = b'BBBBBBBBBBBBBBBB'
key = PBKDF2(str(user_id).encode(), salt, 32, 1000000)
iv = b'BBBBBBBBBBBBBBBB'

print(f"Decrypting .enc files in {cache_path}...")

image_exts = {"jpg", "png", "gif", "webp"}

for file in cache_path.iterdir():
    if not file.is_file() or file.suffix != '.enc':
        continue

    try:
        ciphertext = file.read_bytes()
    except PermissionError:
        print(f"Skipping {file} (file open)")
        continue

    cipher = AES.new(key, AES.MODE_CBC, iv=iv)
    try:
        plaintext = unpad(cipher.decrypt(ciphertext), 16)
    except ValueError:
        print(f"Skipping {file} (decryption failed)")
        continue

    detected_ext = detect_file_type(plaintext)
    base_name = file.stem

    if detected_ext in image_exts:
        out_name = f"{base_name}.{detected_ext}"
    else:
        out_name = f"{base_name}.decrypted"

    out_path = file.with_name(out_name)

    counter = 1
    while out_path.exists():
        out_path = file.with_name(f"{base_name}_{counter}{out_path.suffix}")
        counter += 1

    out_path.write_bytes(plaintext)
    print(f"Decrypted {file} -> {out_path}")

The decrypted image contained the flag.

Operator

I think someone has been hiding secrets on my server. Can you find them?

Initial findings

From the TCP streams, I observed an executable being transferred and extracted the ELF. Loading it in IDA revealed it’s a simple chat server/client that uses XOR encryption for communication with the following key:

[04, 07, 17, 76, 42, 69, B0, 0B, DE, 18, 23, 22, 1E, ED, F7, AE]

Solution

The chat function reads from stdin and the socket, XORing all data with the 16-byte key before sending or after receiving. After decrypting the traffic with the extracted key, we can recover the conversation:

Hi, is this a secure line?
I sure hope so
These are some very sensitive notes so I want to be sure they're not exposed
Anyway, here's my top secret information:
cube{c00l_0p3r4t0rs_us3_mult1_st4g3_p4yl04ds_8ab49338}
I hope nobody finds that...

Flag: cube{c00l_0p3r4t0rs_us3_mult1_st4g3_p4yl04ds_8ab49338}