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

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

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.

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.


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

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.

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 whereid=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!

ENO{SQL1_W1th_0uT_C0mm4_W0rks_SomeHow!}
Misc
Profound Thought - 50

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:

And BOOM! 💥 A flag appeared!
ENO{57394n09r4phy_15_w4y_c00l3r_7h4n_p0rn06r4phy}
USBnet - 50

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

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!

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

💥 Boom! A QR code appeared!

ENO{USB_ETHERNET_ADAPTER_ARE_COOL_N!C3}
Last updated