NNS CTF: Translator, not clanker, base64 unused bits

Challenge

I AM A TRANSLATOR. GIVE ME A WORD AND I WILL TRANSLATE. GIVE ME A LIFE STORY AND I WILL TRANSLATE. GIVE ME NOTHING AND I WILL NOT UNDERSTAND WHAT YOU MEAN, NOT BECAUSE I AM DUMB, BUT BECAUSE I DO NOT WANT TO UNDERSTAND. CALL ME A CLANKER AND I WILL UPSET YOU IN WAYS THAT YOU CAN NOT COMPREHEND, YET.

  • Source: encode.py (convert and b64e redacted in the handout)
  • Category: misc

The service base64-encodes whatever words you send it. What’s fun is what it does with the padding. I wrote this one for NNS CTF 2025.

The quirk

Encoding a should produce YQ==, but the service answers YU==:

  • a is one byte, 8 bits: 01100001
  • Split into sextets: 011000 01xxxx
  • Standard base64 zeroes the unused bits: 011000 010000 = YQ==
  • The service fills them with flag bits instead: 011000 010100 = YU==

Both strings decode to a. A regular base64 decoder will ignore the last sextet’s unused bits. So each word you send quietly carries a few flag bits (4 bits for a one-byte input) that normal base64 decoding never shows. The solution is to collect all the nibbles and print the resulting bytes to see the flag.

Exploit

Send a bunch of short words whose sextets each reveal 4 unused bits. Convert them to binary, extract and collect the 4 unused bits off each base64 encoded word and lastly combine the bits resulting in the challenge flag.

from string import ascii_lowercase, ascii_uppercase, digits

from pwn import remote

BASE64 = ascii_uppercase + ascii_lowercase + digits + "+/"


def b64d(encoded: str) -> str:
    result = ""
    for e in encoded.replace("=", ""):
        n = BASE64.index(e)
        result += f"{n:06b}"

    return result


with remote("<instance>", 3452) as io:
    # Just a high enough number
    payload = "a " * 100
    io.sendlineafter(b"< ", payload.encode("utf-8"))

    io.recvuntil(b"> ")
    output = io.recvline().decode("utf-8").split()


flag_bits = "".join(b64d(o)[-4:] for o in output)
flag = int.to_bytes(int(flag_bits, 2), len(flag_bits) // 8)
print(flag.decode("utf-8", errors="ignore"))

Why base64 has unused bits in the first place is covered in Base64.