//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
-
Spoof a modern Firefox
User-Agentand pull down the real landing page. -
Save the referenced
style.css– its@keyframes blinkanimation encodes Morse timing in the opacity toggles. -
Parse those timing values to recover the string
YLVIS2013. -
Directory brute-force (still using the Firefox UA) to discover an altered
robots.txt, which in turn points to/secret.txt. -
Base32-decode the blob in
secret.txt, then XOR it with the hintYLVIS2013to recover the flagnullctf{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:
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:
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):
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:
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:
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.
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
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;
}
}