Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
MiscMedium

What Does The Fox Say

CTF writeup for What Does The Fox Say Misc from nullCTF

//NullCTF 2025 – "What Does The Fox Say?"

>Challenge Overview

  • Category: Misc

  • Author: cshark3008

  • Remote: http://public.ctf.r0devnull.team:3012

  • Flag Format: nullctf{...}

"The fox left behind a strange melody... Hidden in the noise lies the truth."

The service pretends to be an "unsupported browser" landing page, but almost everything we need is delivered dynamically to modern desktop browsers. The trick is (1) presenting the right headers, (2) decoding a CSS-based timing channel, and (3) extracting an additional layer of obfuscation from a hidden file.

>TL;DR

  1. Spoof a modern Firefox User-Agent and pull down the real landing page.

  2. Save the referenced style.css – its @keyframes blink animation encodes Morse timing in the opacity toggles.

  3. Parse those timing values to recover the string YLVIS2013.

  4. Directory brute-force (still using the Firefox UA) to discover an altered robots.txt, which in turn points to /secret.txt.

  5. Base32-decode the blob in secret.txt, then XOR it with the hint YLVIS2013 to recover the flag nullctf{G3r1ng_din9_d1ng_d1n93r1ng3d1ng}.

>Artifacts Created During Solving

| File | Purpose |

| ---- | ------- |

| style.css | Saved remote stylesheet (contains the blinking animation). |

| index.html | Empty placeholder saved by wget -S -U ... during recon. |

| fox_data.bin | Raw socket capture attempt (unused afterward, but kept for completeness). |

| writeup.md | This document. |

All commands below were executed from /home/noigel/CTF/null/misc/fox/ on a Linux VM (Ubuntu-based) with the default toolchain plus ffuf (already installed under /usr/bin/ffuf).

>Step-by-Step

1. Initial Recon & Header Spoofing

The service returned a 302 to /wrong/wrong.html for normal curl requests, so I checked whether it was doing UA filtering:

bash

curl -I http://public.ctf.r0devnull.team:3012

To bypass the filter, I replayed with a current Firefox UA and saved both the headers and the linked stylesheet:

bash

curl -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0" -I http://public.ctf.r0devnull.team:3012

curl -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0" -o style.css http://public.ctf.r0devnull.team:3012/style.css

style.css contains only a single @keyframes blink animation that toggles opacity according to a custom steps(127, end) timing function. That screamed "data exfiltration".

2. Decoding the CSS Timing Channel

Each percentage bucket appears twice (opacity 1 then 0). Dropping the duplicated entries and treating the final opacity as our bit gives a 128-bit stream. The pattern of repeated 1s/0s (run lengths of 1 vs 3) matches Morse code timing (dot vs dash) with single-zero spacing between elements and triple-zero spacing between characters.

I threw together the following Python helper (ran with python3):

python

from pathlib import Path

from collections import defaultdict

  

lines = Path('style.css').read_text().splitlines()

vals, i = [], 0

while i < len(lines):

    line = lines[i].strip()

    if line.endswith('% {'):

        pct = float(line.split('%')[0])

        j = i + 1

        opacity = None

        while j < len(lines) and lines[j].strip() != '}':

            s = lines[j].strip()

            if s.startswith('opacity'):

                opacity = int(s.split(':')[1].strip().strip(';'))

            j += 1

        vals.append((pct, opacity))

        i = j

    i += 1

  

by_percent = defaultdict(list)

for pct, val in vals:

    if pct in (0.0, 100.0):

        continue

    by_percent[pct].append(val)

  

bits = [by_percent[p][-1] for p in sorted(by_percent)]

  

runs, prev, count = [], bits[0], 1

for b in bits[1:]:

    if b == prev:

        count += 1

    else:

        runs.append((prev, count))

        prev, count = b, 1

runs.append((prev, count))

  

morse = []

current = ''

for val, length in runs:

    if val == 1:

        current += '.' if length == 1 else '-'

    else:

        if length == 3:

            morse.append(current)

            current = ''

  

if current:

    morse.append(current)

  

print(morse)

Output:

['-.--', '.-..', '...-', '..', '...', '..---', '-----', '.----', '...--']

That Morse translates to YLVIS2013 – the year the "What Does the Fox Say" music video dropped. Definitely a hint.

3. Discovering Hidden Files (ffuf + robots)

Next, I ran a directory brute force while keeping the Firefox UA. Non-zero-length responses were rare except for robots.txt:

bash

ffuf \

  -u http://public.ctf.r0devnull.team:3012/FUZZ \

  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0" \

  -w /usr/share/dirb/wordlists/common.txt \

  -fs 0

Pulling robots.txt normally still showed only User-agent: *, so I hexdumped it to catch any hidden characters:

bash

curl -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0" \

  http://public.ctf.r0devnull.team:3012/robots.txt | hexdump -C

This revealed an extra line: Disallow: /secret.txt (it was prefixed with a line feed, so plain curl suppressed it).

4. Extracting the Secret

/secret.txt contained a lure plus a 65-character base32 block:

G44TUJJQIZLEU5DKHZTSONDNKRMF2YATGJ4D2VLPKUBDO5LFHNRFYVYCK5UCEMJU

The decode wasn’t clean ASCII, so I suspected an XOR layer using the previously recovered keyword.

python

import base64

blob = "G44TUJJQIZLEU5DKHZTSONDNKRMF2YATGJ4D2VLPKUBDO5LFHNRFYVYCK5UCEMJU"

raw = base64.b32decode(blob)

key = b"YLVIS2013"

flag = bytes(b ^ key[i % len(key)] for i, b in enumerate(raw))

print(flag.decode())

Result: nullctf{G3r1ng_din9_d1ng_d1n93r1ng3d1ng}

That’s the onomatopoeia-heavy lyric fragment from the Ylvis song, matching the challenge theme.

>Final Flag

nullctf{G3r1ng_din9_d1ng_d1n93r1ng3d1ng}

>Key Takeaways

  • Inspect CSS animations for covert channels; opacity timing is a popular choice.

  • When redirects seem unconditional, test alternate UAs – especially for retro/novelty web challenges.

  • Use hex dumps to reveal hidden control characters in seemingly plain-text files.

  • Musical Easter eggs often double as XOR keys.

Happy fox hunting! 🦊

style.css

css
html {

  display: flex;

  height: 100vh;

  overflow: hidden;

  justify-content: center;

  align-items: center;

  flex-direction: column;

  background: #222;

}

  

body::before,

body::after {

  font-weight: bold;

  font-family: "SF Mono", "Courier New", Courier, monospace;

  font-size: 42px;

  color: #ff4473;

}

  

head {

  display: block;

  background-image: url(https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ2ZnNzV6eHQ1N3c2eXI4ZWJ1aWwzNGQ1a29nNG1kaGFnNHFqYXlhaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/IMML7U3Tp7BRAmnZDL/giphy.gif);

  height: 25rem;

  width: 15rem;

  background-repeat: no-repeat;

  background-size: cover;

  border: 5px solid #fff;

  border-radius: 10px;

  border-style: dashed;

}

  

body::before {

  display: inline-block;

  padding-top: 3rem;

  content: "You just met my friendly horse...";

}

  

body::after {

  margin-left: 16px;

  display: inline;

  content: "i";

  background: #ff4473;

  animation: blink 25.4s steps(127, end) infinite;

}

  

@keyframes blink {

  0.00% {

    opacity: 1;

  }

  0.79% {

    opacity: 1;

  }

  0.79% {

    opacity: 1;

  }

  1.57% {

    opacity: 1;

  }

  1.57% {

    opacity: 1;

  }

  2.36% {

    opacity: 1;

  }

  2.36% {

    opacity: 0;

  }

  3.15% {

    opacity: 0;

  }

  3.15% {

    opacity: 1;

  }

  3.94% {

    opacity: 1;

  }

  3.94% {

    opacity: 0;

  }

  4.72% {

    opacity: 0;

  }

  4.72% {

    opacity: 1;

  }

  5.51% {

    opacity: 1;

  }

  5.51% {

    opacity: 1;

  }

  6.30% {

    opacity: 1;

  }

  6.30% {

    opacity: 1;

  }

  7.09% {

    opacity: 1;

  }

  7.09% {

    opacity: 0;

  }

  7.87% {

    opacity: 0;

  }

  7.87% {

    opacity: 1;

  }

  8.66% {

    opacity: 1;

  }

  8.66% {

    opacity: 1;

  }

  9.45% {

    opacity: 1;

  }

  9.45% {

    opacity: 1;

  }

  10.24% {

    opacity: 1;

  }

  10.24% {

    opacity: 0;

  }

  11.02% {

    opacity: 0;

  }

  11.02% {

    opacity: 0;

  }

  11.81% {

    opacity: 0;

  }

  11.81% {

    opacity: 0;

  }

  12.60% {

    opacity: 0;

  }

  12.60% {

    opacity: 1;

  }

  13.39% {

    opacity: 1;

  }

  13.39% {

    opacity: 0;

  }

  14.17% {

    opacity: 0;

  }

  14.17% {

    opacity: 1;

  }

  14.96% {

    opacity: 1;

  }

  14.96% {

    opacity: 1;

  }

  15.75% {

    opacity: 1;

  }

  15.75% {

    opacity: 1;

  }

  16.54% {

    opacity: 1;

  }

  16.54% {

    opacity: 0;

  }

  17.32% {

    opacity: 0;

  }

  17.32% {

    opacity: 1;

  }

  18.11% {

    opacity: 1;

  }

  18.11% {

    opacity: 0;

  }

  18.90% {

    opacity: 0;

  }

  18.90% {

    opacity: 1;

  }

  19.69% {

    opacity: 1;

  }

  19.69% {

    opacity: 0;

  }

  20.47% {

    opacity: 0;

  }

  20.47% {

    opacity: 0;

  }

  21.26% {

    opacity: 0;

  }

  21.26% {

    opacity: 0;

  }

  22.05% {

    opacity: 0;

  }

  22.05% {

    opacity: 1;

  }

  22.83% {

    opacity: 1;

  }

  22.83% {

    opacity: 0;

  }

  23.62% {

    opacity: 0;

  }

  23.62% {

    opacity: 1;

  }

  24.41% {

    opacity: 1;

  }

  24.41% {

    opacity: 0;

  }

  25.20% {

    opacity: 0;

  }

  25.20% {

    opacity: 1;

  }

  25.98% {

    opacity: 1;

  }

  25.98% {

    opacity: 0;

  }

  26.77% {

    opacity: 0;

  }

  26.77% {

    opacity: 1;

  }

  27.56% {

    opacity: 1;

  }

  27.56% {

    opacity: 1;

  }

  28.35% {

    opacity: 1;

  }

  28.35% {

    opacity: 1;

  }

  29.13% {

    opacity: 1;

  }

  29.13% {

    opacity: 0;

  }

  29.92% {

    opacity: 0;

  }

  29.92% {

    opacity: 0;

  }

  30.71% {

    opacity: 0;

  }

  30.71% {

    opacity: 0;

  }

  31.50% {

    opacity: 0;

  }

  31.50% {

    opacity: 1;

  }

  32.28% {

    opacity: 1;

  }

  32.28% {

    opacity: 0;

  }

  33.07% {

    opacity: 0;

  }

  33.07% {

    opacity: 1;

  }

  33.86% {

    opacity: 1;

  }

  33.86% {

    opacity: 0;

  }

  34.65% {

    opacity: 0;

  }

  34.65% {

    opacity: 0;

  }

  35.43% {

    opacity: 0;

  }

  35.43% {

    opacity: 0;

  }

  36.22% {

    opacity: 0;

  }

  36.22% {

    opacity: 1;

  }

  37.01% {

    opacity: 1;

  }

  37.01% {

    opacity: 0;

  }

  37.80% {

    opacity: 0;

  }

  37.80% {

    opacity: 1;

  }

  38.58% {

    opacity: 1;

  }

  38.58% {

    opacity: 0;

  }

  39.37% {

    opacity: 0;

  }

  39.37% {

    opacity: 1;

  }

  40.16% {

    opacity: 1;

  }

  40.16% {

    opacity: 0;

  }

  40.94% {

    opacity: 0;

  }

  40.94% {

    opacity: 0;

  }

  41.73% {

    opacity: 0;

  }

  41.73% {

    opacity: 0;

  }

  42.52% {

    opacity: 0;

  }

  42.52% {

    opacity: 1;

  }

  43.31% {

    opacity: 1;

  }

  43.31% {

    opacity: 0;

  }

  44.09% {

    opacity: 0;

  }

  44.09% {

    opacity: 1;

  }

  44.88% {

    opacity: 1;

  }

  44.88% {

    opacity: 0;

  }

  45.67% {

    opacity: 0;

  }

  45.67% {

    opacity: 1;

  }

  46.46% {

    opacity: 1;

  }

  46.46% {

    opacity: 1;

  }

  47.24% {

    opacity: 1;

  }

  47.24% {

    opacity: 1;

  }

  48.03% {

    opacity: 1;

  }

  48.03% {

    opacity: 0;

  }

  48.82% {

    opacity: 0;

  }

  48.82% {

    opacity: 1;

  }

  49.61% {

    opacity: 1;

  }

  49.61% {

    opacity: 1;

  }

  50.39% {

    opacity: 1;

  }

  50.39% {

    opacity: 1;

  }

  51.18% {

    opacity: 1;

  }

  51.18% {

    opacity: 0;

  }

  51.97% {

    opacity: 0;

  }

  51.97% {

    opacity: 1;

  }

  52.76% {

    opacity: 1;

  }

  52.76% {

    opacity: 1;

  }

  53.54% {

    opacity: 1;

  }

  53.54% {

    opacity: 1;

  }

  54.33% {

    opacity: 1;

  }

  54.33% {

    opacity: 0;

  }

  55.12% {

    opacity: 0;

  }

  55.12% {

    opacity: 0;

  }

  55.91% {

    opacity: 0;

  }

  55.91% {

    opacity: 0;

  }

  56.69% {

    opacity: 0;

  }

  56.69% {

    opacity: 1;

  }

  57.48% {

    opacity: 1;

  }

  57.48% {

    opacity: 1;

  }

  58.27% {

    opacity: 1;

  }

  58.27% {

    opacity: 1;

  }

  59.06% {

    opacity: 1;

  }

  59.06% {

    opacity: 0;

  }

  59.84% {

    opacity: 0;

  }

  59.84% {

    opacity: 1;

  }

  60.63% {

    opacity: 1;

  }

  60.63% {

    opacity: 1;

  }

  61.42% {

    opacity: 1;

  }

  61.42% {

    opacity: 1;

  }

  62.20% {

    opacity: 1;

  }

  62.20% {

    opacity: 0;

  }

  62.99% {

    opacity: 0;

  }

  62.99% {

    opacity: 1;

  }

  63.78% {

    opacity: 1;

  }

  63.78% {

    opacity: 1;

  }

  64.57% {

    opacity: 1;

  }

  64.57% {

    opacity: 1;

  }

  65.35% {

    opacity: 1;

  }

  65.35% {

    opacity: 0;

  }

  66.14% {

    opacity: 0;

  }

  66.14% {

    opacity: 1;

  }

  66.93% {

    opacity: 1;

  }

  66.93% {

    opacity: 1;

  }

  67.72% {

    opacity: 1;

  }

  67.72% {

    opacity: 1;

  }

  68.50% {

    opacity: 1;

  }

  68.50% {

    opacity: 0;

  }

  69.29% {

    opacity: 0;

  }

  69.29% {

    opacity: 1;

  }

  70.08% {

    opacity: 1;

  }

  70.08% {

    opacity: 1;

  }

  70.87% {

    opacity: 1;

  }

  70.87% {

    opacity: 1;

  }

  71.65% {

    opacity: 1;

  }

  71.65% {

    opacity: 0;

  }

  72.44% {

    opacity: 0;

  }

  72.44% {

    opacity: 0;

  }

  73.23% {

    opacity: 0;

  }

  73.23% {

    opacity: 0;

  }

  74.02% {

    opacity: 0;

  }

  74.02% {

    opacity: 1;

  }

  74.80% {

    opacity: 1;

  }

  74.80% {

    opacity: 0;

  }

  75.59% {

    opacity: 0;

  }

  75.59% {

    opacity: 1;

  }

  76.38% {

    opacity: 1;

  }

  76.38% {

    opacity: 1;

  }

  77.17% {

    opacity: 1;

  }

  77.17% {

    opacity: 1;

  }

  77.95% {

    opacity: 1;

  }

  77.95% {

    opacity: 0;

  }

  78.74% {

    opacity: 0;

  }

  78.74% {

    opacity: 1;

  }

  79.53% {

    opacity: 1;

  }

  79.53% {

    opacity: 1;

  }

  80.31% {

    opacity: 1;

  }

  80.31% {

    opacity: 1;

  }

  81.10% {

    opacity: 1;

  }

  81.10% {

    opacity: 0;

  }

  81.89% {

    opacity: 0;

  }

  81.89% {

    opacity: 1;

  }

  82.68% {

    opacity: 1;

  }

  82.68% {

    opacity: 1;

  }

  83.46% {

    opacity: 1;

  }

  83.46% {

    opacity: 1;

  }

  84.25% {

    opacity: 1;

  }

  84.25% {

    opacity: 0;

  }

  85.04% {

    opacity: 0;

  }

  85.04% {

    opacity: 1;

  }

  85.83% {

    opacity: 1;

  }

  85.83% {

    opacity: 1;

  }

  86.61% {

    opacity: 1;

  }

  86.61% {

    opacity: 1;

  }

  87.40% {

    opacity: 1;

  }

  87.40% {

    opacity: 0;

  }

  88.19% {

    opacity: 0;

  }

  88.19% {

    opacity: 0;

  }

  88.98% {

    opacity: 0;

  }

  88.98% {

    opacity: 0;

  }

  89.76% {

    opacity: 0;

  }

  89.76% {

    opacity: 1;

  }

  90.55% {

    opacity: 1;

  }

  90.55% {

    opacity: 0;

  }

  91.34% {

    opacity: 0;

  }

  91.34% {

    opacity: 1;

  }

  92.13% {

    opacity: 1;

  }

  92.13% {

    opacity: 0;

  }

  92.91% {

    opacity: 0;

  }

  92.91% {

    opacity: 1;

  }

  93.70% {

    opacity: 1;

  }

  93.70% {

    opacity: 0;

  }

  94.49% {

    opacity: 0;

  }

  94.49% {

    opacity: 1;

  }

  95.28% {

    opacity: 1;

  }

  95.28% {

    opacity: 1;

  }

  96.06% {

    opacity: 1;

  }

  96.06% {

    opacity: 1;

  }

  96.85% {

    opacity: 1;

  }

  96.85% {

    opacity: 0;

  }

  97.64% {

    opacity: 0;

  }

  97.64% {

    opacity: 1;

  }

  98.43% {

    opacity: 1;

  }

  98.43% {

    opacity: 1;

  }

  99.21% {

    opacity: 1;

  }

  99.21% {

    opacity: 1;

  }

  100.00% {

    opacity: 1;

  }

}