DownUnderCTF: mini-me
Room recap:
Mini-me was honestly a surprising CTF room. I didn’t expect them to go full brain rot mode as the core theme for this challenge. Basically, all you need to do is carefully read through the source code provided. Then, exploit a vulnerability related to the .js.map file, and finally decode the hidden message to uncover the “secret” API key that’s been stashed away.
Room link:
Key Takeaways
🧠 Learned about .js.map files, why they exist, and how they’re used.
🧠 Understood why many .js files on websites end with .min.js.
🖥️ Used tools like Curl/Burp Suite to interact with the website.
❌ Never, EVER store API keys, tokens, or security logic in client-side JS.
Ight. Let’s do this!
First off—as always—I read the room description carefully, including the room’s title, to see if there were any clues dropped.
Here, there was a phrase: “or right at the front!” — which might hint that the vulnerability lies on the front-end, client-side, or even that the flag might be embedded in some image or media on the site. Let’s keep that in mind—it’ll make more sense later.
Then, I accessed the “brain rot” website.
At first glance, it looked like it wanted me to log in. So I tried throwing in some random credentials to see if there was an SQLi vulnerability hiding there.
Surprisingly, it just let me in. I retried with different usernames and passwords, but it kept logging me in regardless. So I figured this login screen was probably just a decoy—meant to mislead or waste your time at the start.
After “logging in” and hitting the “Begin Experience” button, the site transitioned to what looked like a romantic candlelit evening between Tung Tung Tung Sahur and Ballerina Cappuccina. The image gently danced around the screen with a very “ear-bleeding” audio…
Nothing exciting to see besides that, so…naturally, I dove straight into the page sources with Ctrl + U.
First Breadcrumb
Since we already suspected a client-side/front-end vulnerability, I started clicking through every linked file I could find.
First up was the .css file—pretty vanilla, nothing interesting. Then I tried downloading the brain rot .png image and threw some stego tools at it like zsteg and binwalk, hoping for hidden goodies. Nada. Just pain.
But then I clicked into main.min.js—and bingo. There it was: a suspicious little comment hinting at the existence of a .map file.
I tried visiting the direct URL for the .map file and, sure enough, the browser served it up like warm cookies.
At this point, I’d recommend using a tool like SourceMapper to make the map file human-readable. (i was not using it here so it looks pretty messy.)
Second Breadcrumb
Inside, a helpful comment warns that the code is “heavily obfuscated” for production—which honestly just made me more curious (red box).
After some digging (and with AI assist), I noticed there was a function named qyrbkc and was using XOR encoding to generate a variable called lmsvdt (blue box). This could be a flag, or at least the next breadcrumb.
From here, we have two ways to extract the value of this variable:
Extracting the hidden key!
Method 1:
Leverage existing console.log The obfuscated script already uses console.log—we just tack on the variable name at the end like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<SNIP>
function qyrbkc() {
const xtqzp = ["85"], vmsdj = ["87"], rlfka = ["77"], wfthn = ["67"], zdqo = ["40"], yclur = ["82"],
bpxmg = ["82"], hkfav = ["70"], oqzdu = ["78"], nwtjb = ["39"], sgfyk = ["95"], utxzr = ["89"],
jvmqa = ["67"], dpwls = ["73"], xaogc = ["34"], eqhvt = ["68"], mfzoj = ["68"], lbknc = ["92"],
zpeds = ["84"], cvnuy = ["57"], ktwfa = ["70"], xdglo = ["87"], fjyhr = ["95"], vtuze = ["77"], awphs = ["75"];
const dhgyvu = [xtqzp[0], vmsdj[0], rlfka[0], wfthn[0], zdqo[0], yclur[0],
bpxmg[0], hkfav[0], oqzdu[0], nwtjb[0], sgfyk[0], utxzr[0],
jvmqa[0], dpwls[0], xaogc[0], eqhvt[0], mfzoj[0], lbknc[0],
zpeds[0], cvnuy[0], ktwfa[0], xdglo[0], fjyhr[0], vtuze[0], awphs[0]];
const lmsvdt = dhgyvu.map((pjgrx, fkhzu) =>
String.fromCharCode(
Number(pjgrx) ^ (fkhzu + 1) ^ 0
)
).reduce((qdmfo, lxzhs) => qdmfo + lxzhs, "");
console.log("Note: Key is now secured with heavy obfuscation, should be safe to use in prod :)", lmsvdt);
}
<SNIP>
Then paste the entire obfuscated qyrbkc function (with that line edited in) into your browser’s DevTools Console. Boom—it’ll print the real value of lmsvdt.
Method 2:
You can also recreate the core logic in a clean format. Here’s the minimal deobfuscated version:
1
2
3
4
5
6
7
8
9
10
11
const chars = [
"85","87","77","67","40","82","82","70","78","39",
"95","89","67","73","34","68","68","92","84","57",
"70","87","95","77","75"
];
const key = chars.map((code, i) =>
String.fromCharCode(Number(code) ^ (i + 1))
).join('');
console.log(key);
Paste this into the browser console and it will print out the same value as method 1.
What’s next?
After obtaining the hidden key: TUNG-TUNG-TUNG-TUNG-SAHUR, something clicked in my mind. There was a zipped folder provided by the CTF, and up to that point, I hadn’t touched it at all. So…there had to be more clues buried inside.
Unfortunately, during the time of writting this writeup, the zip file is no longer downloadable :< (for me at least)
So I did what any curious hacker would do: I started “catting” through every single file in that source folder like a man possessed. That’s when one file stood out—app.py.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from flask import Flask, render_template, send_from_directory, request, redirect, make_response
from dotenv import load_dotenv
import os
load_dotenv()
API_SECRET_KEY = os.getenv("API_SECRET_KEY")
FLAG = os.getenv("FLAG")
app = Flask(__name__, static_folder="static", template_folder="templates")
@app.after_request
def add_header(response):
response.cache_control.no_store = True
response.cache_control.must_revalidate = True
return response
@app.route("/")
def index():
return render_template("index.html")
@app.route("/login", methods=["POST"])
def login():
return redirect("/confidential.html")
@app.route("/confidential.html")
def confidential():
return render_template("confidential.html")
@app.route("/admin/flag", methods=["POST"])
def flag():
key = request.headers.get("X-API-Key")
if key == API_SECRET_KEY:
return FLAG
return "Unauthorized", 403
Scrolling through the file, I hit gold near the bottom. There it was: a juicy route called /admin/flag. This was almost certainly where the final flag was hiding. Even better, the code clearly referenced a security check—it would return the flag only if a correct API key was provided in the header, specifically via “X-API-Key”.
All the pieces fell into place.
Now we know “TUNG-TUNG-TUNG-TUNG-SAHUR” is an API key. I then crafted a simple curl POST request targeting /admin/flag, including the key in the headers.
1
curl -X POST -H "X-API-Key: TUNG-TUNG-TUNG-TUNG-SAHUR" https://web-mini-me-ab6d19a7ea6e.2025.ductf.net/admin/flag
And just like that, we got THE FLAG!
Room complete. Let’s GOOOOO 💪💪
Thoughts?
This challenge felt strangely personal. The whole brainrot theme resonated with me deeply (im joking 😭). But aside from the memes, the room taught me a bunch about client-side security issues, how secrets can be hidden in plain sight, and how source code often reveals more than we expect…