Page cover image

Nullcon HackIM CTF 2025

Hey, fellow hackers! 🏴‍☠️

In this writeup 📝, I’ll break down some of the exciting challenges solved by myself and my squad P4rad0x in the Nullcon HackIM CTF 2025.

Reversing

Scrambled - 50

Challenge Description

In this challenge, I was given a Python script (main.py) that encrypts a flag using XOR and shuffling. A scrambled hex output containing the encrypted flag.

Our goal❓ Recover the original flag!.

Looking at main.py, we can break down the encryption process:

import random

def encode_flag(flag, key):
    xor_result = [ord(c) ^ key for c in flag]

    chunk_size = 4
    chunks = [xor_result[i:i+chunk_size] for i in range(0, len(xor_result), chunk_size)]
    seed = random.randint(0, 10)
    random.seed(seed)
    random.shuffle(chunks)
    
    scrambled_result = [item for chunk in chunks for item in chunk]
    return scrambled_result, chunks

def main():
    flag = "REDACTED"
    key = REDACTED
    scrambled_result, _ = encode_flag(flag, key)
    print("result:", "".join([format(i, '02x') for i in scrambled_result]))

if __name__ == "__main__":
    main()

XOR Encoding:

  • Each character of the flag is XORed with a single-byte key.

  • This converts the flag into a scrambled list of integers.

Chunking and Shuffling:

  • The encrypted data is split into chunks of 4 bytes.

  • A random seed (0-10) is used to shuffle the chunks.

  • The shuffled bytes are then converted to hex and printed as the output.

I was given this scrambled hex output:

1e78197567121966196e757e1f69781e1e1f7e736d6d1f75196e75191b646e196f6465510b0b0b57

To undo the encryption, I needed to reverse each step:

Convert Hex to Bytes

  • The scrambled hex string is converted back into a list of numbers.

Brute-force the Shuffling Seed

  • Since the seed used for shuffling is between 0 and 10, I can try all possible seeds to reverse the shuffle order.

  • I reconstruct the original order by checking all possible shuffled indices.

Recover the XOR Key

  • Since flags typically follow a format like "ENO{", I can use known plaintext attack to determine the XOR key.

  • Compare "ENO{" with the first 4 decrypted characters to derive the key.

Apply XOR Decryption

  • Once the key is found, XOR it with all the scrambled bytes to reveal the flag.

import random

scrambled_hex = "1e78197567121966196e757e1f69781e1e1f7e736d6d1f75196e75191b646e196f6465510b0b0b57"
scrambled_bytes = [int(scrambled_hex[i:i+2], 16) for i in range(0, len(scrambled_hex), 2)]
chunk = 4
n = len(scrambled_b) // chunk

# Try seeds from 0 to 10
for s in range(11):
    parts = []
    
    # Split into chunks
    for i in range(0, len(scrambled_b), chunk):
        parts.append(scrambled_b[i:i+chunk])

    random.seed(s)
    indices = list(range(n))
    random.shuffle(indices)

    # Reverse shuffle
    orig = [None] * n
    for i in range(n):
        orig[indices[i]] = parts[i]

    # Flatten list
    orig_xor = []
    for p in orig:
        for b in p:
            orig_xor.append(b)

    # Try to recover key using "ENO{"
    plain = "ENO{"
    key_try = []
    
    for i in range(len(plain)):
        key_try.append(orig_xor[i] ^ ord(plain[i]))

    # Ensure key is consistent
    if len(set(key_try)) == 1:  # Single-byte XOR key
        k = key_try[0]
        flag = ""

        for b in orig_xor:
            flag += chr(b ^ k)

        print(f"Possible flag: {flag} (Key: {k})")
        break

When executing dec.py, it successfully reverses the shuffle, finds the XOR key, and decrypts the flag! 🎉

ENO{5CR4M83L3D_3GG5_4R3_1ND33D_T45TY!!!}

Flag Checker - 50

Challenge Description

This challenge gave me a binary file—a simple program that asks for input and checks if it’s the correct flag. But instead of guessing, I need to dig into the code and find the flag myself. Let’s go! 🚀

First, I ran the file command to see what I was dealing with:

  r1pp3r 🔱 god2eye ~/Documents/CTF/nullconctf25/rev 
  λ file flag_checker 
flag_checker: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4aca4cf0854ed6ff188432f4e2fd4352913b822d, for GNU/Linux 3.2.0, stripped

So, it’s a 64-bit ELF binary and stripped (which means no function names—just raw assembly).

Time to crack it open! 🔥

I loaded the binary into Ghidra—a software reverse engineering (SRE) framework and found the main function. Here's what it does:

  • Prompts for user input.

  • Passes the input to a flag-checking function.

Main Function
  • This function encrypts the user input and then compares it to a stored encrypted flag in the .rodata section.

And there it was—the encrypted flag hidden in memory. Here's the encryption logic :

Each byte of the input gets modified:

  • It adds the byte’s index to itself.

  • Then it XORs the result with 0x5A.

Bitwise shifts happen next:

  • The encrypted byte is shifted left by 3.

  • OR’ed with the same byte shifted right by 5.

Flag Check Function
Encryption Function

Now, I just needed to decrypt it!. I just had to reverse the process to get back the original flag!

Here's my Python script to decrypt the flag:

def dec(enc_hex: str) -> str:

    enc_b = bytes.fromhex(enc_hex)
    dec_b = bytearray(len(enc_b))
    
    for i in range(len(enc_b)):
        # Reverse the bit shifts
        rot_back = ((enc_b[i] >> 3) | (enc_b[i] << 5)) & 0xFF
        # Subtract index value
        org_byte = (rot_back - i) & 0xFF
        # XOR with 0x5A
        dec_b[i] = org_byte ^ 0x5A
    
    return dec_b.decode(errors='ignore')  # Ignore errors if non-printable characters exist
# Encrypted input as hex string
enc_hex = "f8 a8 b8 21 60 73 90 83 80 c3 9b 80 ab 09 59 d3 21 d3 db d8 fb 49 99 e0 79 3c 4c 49 2c 29 cc d4 dc"

# Decrypt the message
dec_msg = dec(enc_hex)

print("Decrypted Message:", dec_msg)

I ran the script, and… BOOM!💥

ENO{R3V3R53_3NG1N33R1NG_M45T3R!!!}

Web

Paginator - 50

Challenge Description

In this challenge, I had to use SQL injection techniques to uncover the hidden flag. Let’s dive in!

Clicking on the provided link took me to a paginated web application—a site that loads data in chunks instead of displaying everything at once.

I also noticed a "Source" button that led me to: http://52.59.124.14:5012/?source

Here, I found the source code of the backend, which was a crucial clue.

Source code

Reading through the code, I discovered an important detail:

🚨 The flag (id = 1) was restricted! 🚨

  • The site’s database had an "id" field where each entry had a unique number.

  • Direct access to id = 1 (the flag) was blocked—so I couldn’t just go to ?id=1.

  • However, there was a vulnerability in how the pagination feature handled input.

The pagination feature worked with two parameters:

?p=<page_number>,<max_items>

But here’s the catch: the "max_items" value wasn’t properly validated.

I crafted a simple SQL injection payload:

?p=2,10 OR id=1
  • 2,10 → Normal pagination parameters.

  • OR id=1 → A condition to forcefully retrieve the entry where id=1 (which contains the flag).

By entering this into the URL: http://52.59.124.14:5012/?p=2,10%20OR%20id=1

Boom! 💥 After decoding got the flag!

Flag

ENO{SQL1_W1th_0uT_C0mm4_W0rks_SomeHow!}

Misc

Profound Thought - 50

Challenge Description

This challenge gave me a PNG image file, there was something hidden inside. But where? How?

First things first, I ran the usual checks:

🛠 exiftool – Nothing suspicious in the metadata. 🛠 file command – Just a regular JPG file.

 r1pp3r 🔱 god2eye ~/Documents/CTF/nullconctf25/misc 
  λ file l5b245c11.png 
l5b245c11.png: PNG image data, 480 x 360, 8-bit/color RGB, non-interlaced
 r1pp3r 🔱 god2eye ~/Documents/CTF/nullconctf25/misc 
  λ exiftool l5b245c11.png 
ExifTool Version Number         : 12.76
File Name                       : l5b245c11.png
Directory                       : .
File Size                       : 222 kB
File Modification Date/Time     : 2025:02:01 22:51:58+05:30
File Access Date/Time           : 2025:02:04 15:33:54+05:30
File Inode Change Date/Time     : 2025:02:01 22:52:51+05:30
File Permissions                : -rwxrwx---
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 480
Image Height                    : 360
Bit Depth                       : 8
Color Type                      : RGB
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Image Size                      : 480x360
Megapixels                      : 0.173

No hidden comments, no strange data, just a plain old image. Since metadata didn’t give me anything, I turned to a more powerful tool: zsteg.

Zsteg is also a tool used to detect LSB steganography only in the case of PNG and BMP images.

I ran:

Zsteg output

And BOOM! 💥 A flag appeared!

ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy}

USBnet - 50

Challenge Description

This one started with a pcapng file (a packet capture file) containing USB traffic. First step?

Wireshark—the go-to tool for network forensics!

Opening the file in Wireshark, I spotted several packets with NCMA and NCM0 identifiers. Looking deeper, I found the USB device that used for this capture was a Gigabit Ethernet adapter (AX88179) from ASIX Electronics Corp. Cool

Wireshark Capture

While scrolling through the packet list, I noticed something cool—a PNG file signature inside the hex data. That meant an image was hidden in the traffic!

Packet with PNG file signature

I grabbed the hex data of the packet containing the PNG signature and ran it through CyberChef.

CyberChef

💥 Boom! A QR code appeared!

Extracted QR

ENO{USB_ETHERNET_ADAPTER_ARE_COOL_N!C3}

Last updated