StylishBossOfficialWriteup|CATCTF25
Omar Mohamed
Thanks for sharing!
بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

Welcome welcome welcome! Today I have a special write up for you, the official write up for the Stylish Boss challenge from CAT CTF 25.
Which is my first challenge that I made for a Live CTF, and I am very excited to share it with you!
You can download the challenge files from here and give it a try before reading.
Let's get started!
Challenge Description

First Look
The challenge is a web application where users can customize the page font. It also includes a 'Metadata Stripper API', restricted to the Boss role. A report feature lets you submit your profile, causing the Boss to view it with your chosen font settings.


That's all from UI. Now let's dive into our code
Code Analysis
In
auth.js
, the Register and Login functionality are pretty standard, there is just an ApiKey
generated randomly for each user upon registration.In
main.js
, the /
and /profile
endpoints render the home and profile pages. Both apply the user’s saved font preference from the database, and /profile
additionally embeds the user’s API key.Since the
/profile
route renders the profile
view, we inspect views/profile.ejs
and find the following noteworthy code:The value of
<%- userFont %>
is inserted directly into the font-family
property without sanitization, which makes it vulnerable to CSS Injection. Additionally, the API key is rendered inside a <div>
via the data-internal-api-key
attribute.In EJS,<%- %>
outputs raw, unescaped content, unlike<%= %>
which escapes HTML characters and would have prevented this issue.
Next we have
/set-font
endpoint which allows users to set their font, which is then stored in the database.The font is sanitized by removing
<
and >
characters to prevent HTML injection .The other endpoints:
/report
and /boss/view-theme
are simple logic to make the Boss view his own profile with the font of the user who reported.Third file
api.js
has the following code:The middleware
apiKeyAuth
checks if the request has an X-API-Key
header and verifies it against the database to ensure the user is a Boss. If the API key is valid, it allows access to the /strip-metadata
endpoint.The objective is to steal the Boss API key, then use it to access the
/strip-metadata
endpoint and ultimately retrieve the flag. To start, we’ll focus on leaking the Boss’s API key through the CSS Injection vulnerability.Exploitation: CSS Injection
For background on exploiting CSS Injection to exfiltrate data, this article is a great reference: How you can steal private data through CSS injection.
We know the API key is in the
data-internal-api-key
attribute of the <div id="api-key-container">
inside views/profile.ejs
.In CSS, certain properties can fetch external resources, such as:
This makes it possible to exfiltrate data through requests. For example:
Here, whenever a
<div>
exists, the browser will try to load an image from our server.To make this conditional, we can use CSS attribute selectors. For instance:
This triggers only if the element has the
data-internal-api-key
attribute. Even better, the ^=
operator lets us check prefixes:That means the browser will only send a request if the API key starts with
sk
.Using this trick, we can leak the key character by character. For example:
If the key starts with
sk_a
, our server gets a request. If not, nothing happens. By iterating over possibilities (sk_aa
, sk_ab
, …) we can brute-force the full API key.A working injection payload might look like this:
Here we break out of the
font-family
, inject our CSS, and close cleanly with a comment.Instead of guessing one by one, we can test multiple prefixes at once:
Whichever rule matches causes the request, revealing the correct character.
You can first test this on your own profile, then trigger the Boss to load your malicious font via the report feature. This way, his profile page leaks his API key straight to your server.
And just like that.. you’ve got the Boss’s API key.
Code Analysis
Now that we have the Boss API key, we can use it to access the
/strip-metadata
endpoint.The
/strip-metadata
endpoint expects an X-File-Name
header containing the file name. It then calls the MetadataStripper
class to process the file and return its output.At first glance, there doesn’t seem to be much room for exploitation. However, since the
MetadataStripper
class comes from the metadata-stripper
npm package, and our input is passed directly into its processFile
method, it’s worth inspecting the package code itself on npmjs.com.
We can quickly spot a potential command injection in the package, but there’s some sanitization in place that tries to block it:
- The first regex strips anything that isn’t a word character, space, or one of these:
${}:()" |*
. - The second regex removes any occurrence of the substring
flag
(case-insensitive).
So, what’s left?
- Allowed:
a-zA-Z0-9_
, spaces,$ { } : ( ) " | *
- Blocked:
f
,l
,a
,g
The executed command is:
Exploitation: Command Injection
Our task: escape this command and inject our own. Luckily, the pipe operator
|
is allowed, which means we can chain another command.From the
Dockerfile
we know the flag is stored in /flag
. Normally, we’d use cat
, less
, or head
, but all of those has blocked characters. However, more
is still available! Perfect for reading files.So a basic injection would be:
But there’s a catch: we can’t directly type
flag
(f, l, a, g
are removed), and /
is also disallowed.Wildcards
*
are allowed, so *
could match the filename, but more *
will only do it in the current directory, not /flag
.Here’s where a neat Linux trick comes in: parameter expansion. In Bash, we can grab parts of environment variables. For example:
This returns
/
, since the first character of $HOME
is /
. And because ${}
syntax is allowed, we can use it to build the path.Final payload:
Which resolves to:

CATF{Y0U_M4D3_1T_B055_Y0U_D3S3RV3_4_MU5HR00M_45_4_G1FT!}
That was it! You have successfully exploited the Stylish Boss challenge and obtained the flag!
IMPORTANT: Don't you dare miss out my other challenge "Underground Maze" which I made with my awesome friend Korea: Click here. Thank You <3
Conclusion
This was a fun challenge to make and I hope you enjoyed it as much as I did. It was a great opportunity to be a part of CAT CTF 25 and to create a challenge that is both challenging and educational. I am looking forward to your feedback and suggestions for future challenges. Thank you for reading my write up and I hope you learned something new today!
Tags: