ImaginaryCTF 2025

Between September 5th, 2025 and September 7th, 2025, I worked on ImaginaryCTF, a Capture the Flag competition together with the Kernel Sanders #CTF team at the University of Florida (#UF). I had a lot of fun with the challenges and wanted to thank the creators.

For those of you who have never heard of CTFs, they are online cybersecurity competitions which happen almost every week and contain a variety of categories, such as

I managed to solve two challeges, redacted and certificate

Redacted

This is a #crypto challenge. In CTFs, crypto challenges allow players to apply their skills in cryptography and math to figure out an encoded message.

In this challenge, we've been given a XORed output and redacted key from CyberChef. CyberChef is an online platform commonly used for crypto CTFs. It contains multiple tools on various ciphers (XOR, Vigenere) and encodings (Base64, hex).

The challenge description is

wait, i thought XORing something with itself gives all 0s???

We're given the following image as the challenge: CTF redacted image

Manually transcribing the output yields

65 6c ce 6b c1 75 61 7e 53 66 c9 52 d8 6c 6a 53 6e 6e de 52 df 63 6d 7e 75 7f ce 64 d5 63 73

Here's a couple of basic things we know. First, the flag is 31 bytes long, including the ictf{ header. Next, the XOR algorithm is not behaving as expected, because XORing an input with itself usually returns 0.

Let's try to recreate this in CyberChef. We don't know the key, but we can put dummy characters in as a placeholder. We notice that the first 2 characters in our output match the expected key. CTF redacted potential solutions

From this point, we can try to brute-force the rest of the key, but that won't get us anywhere. We need to try a different method.

Since XOR is a symmetric operation, we notice that we are able to recover the first 5 bytes of the key by XORing the output with the input.

The first 5 bytes are

0c 0f ba 0d ba

We also notice that one of the bytes of the key must be 0e because the last byte of the ciphertext is 73 and the last character of the plaintext is '}'. We found this using Python:

print(hex(ord('}') ^ 0x73))

Now, we keep adding bytes until we get to what appears to be the correct key length, ensuring the last byte remains unchanged as }. We notice that adding 3 bytes appears to yield the correct length and a partially decrypted ciphertext. So the “key” we have so far is:

0c 0f ba 0d ba 00 0e 00

with 00 as the 2 padding bytes. Here's what we see in CyberChef

We also notice that CyberChef interprets text entered into the “HEX” XOR field by dropping all invalid characters. Since it seems like the flag only contains alphabetical characters, we have 6*6=36 possibilities for the key now (letters a-f for each byte). We can perform a manual brute force for these 36 possibilities.

After performing this simple brute-force, our key is now

0c 0f ba 0d ba 0d 0e 0c

The decrypted ciphertext

And the recovered plaintext is

ictf{xor_is_bad_bad_encryption}

Certificate

This is a #web challenge. In CTFs, web challenges have some type of website vulnerable to attacking.

The challenge description is

As a thank you for playing our CTF, we're giving out participation certificates! Each one comes with a custom flag, but I bet you can't get the flag belonging to Eth007!

We visit the website (https://eth007.me/cert/). Obviously, the first thing we'll do is try to get the Eth007's certificate.

The name is redacted

We enter “Eth007” into the “Name” field. However, the name is REDACTED instead. We also don't see a flag anywhere on the website, despite the challenge description assuring that we'd get one.

No network traffic in sight

We then open up the browser dev tools (by pressing F12) and observe that there is no network traffic happening, even when the name is changed. So it is likely that whatever is responsible for the redaction is running client-side as JavaScript.

The flag has been located

Looking through the source code, we first notice that the flag is embedded within a <desc></desc> tag in the SVG. We're not sure what the algorithm used to generate it is, but we'll treat it as a black box for now.

Searching through the source code for “Eth007” yields this interesting function:

function renderPreview(){
  var name = nameInput.value.trim();
  if (name == "Eth007") {
    name = "REDACTED"
  } 
  const svg = buildCertificateSVG({
    participant: name || "Participant Name",
    affiliation: affInput.value.trim() || "Participant",
    date: dateInput.value,
    styleKey: styleSelect.value
  });
  svgHolder.innerHTML = svg;
  svgHolder.dataset.currentSvg = svg;
}

We modify the function by removing the check for whether the name matches Eth007.

function renderPreview(){
  var name = nameInput.value.trim();
  const svg = buildCertificateSVG({
    participant: name || "Participant Name",
    affiliation: affInput.value.trim() || "Participant",
    date: dateInput.value,
    styleKey: styleSelect.value
  });
  svgHolder.innerHTML = svg;
  svgHolder.dataset.currentSvg = svg;
}

Overwriting the function

Then, we overwrite the original function by typing it into the browser DevTools console. Now, we can find the flag by looking in the source code for the desc tag:

<desc>ictf{7b4b3965}</desc>

We got the flag

That's all for now. I'll be back with more next time!

Revision 1 (2025-09-08): Fixed the broken CyberChef links.

Questions or suggestions? Reach out to me at vance.ylhuang.com!