BugForge - Shady Oaks Financial Lab Writeup
Executive Summary
Overall Risk Rating: π΄ Critical
Key Findings:
- Broken Access Control on all admin API endpoints (CWE-862) β no role check enforced server-side
- Rounding exploit in stock trading (CWE-682) β fractional share purchases round to zero cost, enabling free share accumulation
Business Impact: An attacker can access all admin functionality including user data, transaction history, and system statistics. Additionally, the rounding exploit allows money generation from nothing on low-priced stocks, potentially draining platform funds.
Objective
Find and exploit vulnerabilities in the Shady Oaks Financial stock trading web application.
Initial Access
# Target Application
URL: https://lab-1773951285118-jdyd1f.labs-app.bugforge.io
# Auth details
POST /api/register with {username, email, password}
Returns JWT HS256 Bearer token
Registered as: haxor (id:4, role: user)
Key Findings
Critical & High-Risk Vulnerabilities
-
Broken Access Control on Admin Endpoints (CWE-862: Missing Authorization) β All
/api/admin/*endpoints (flag, stats, users, transactions, stocks) are accessible to any authenticated user regardless of role. The server validates the JWT but never checks therolefield. A regular user can access the flag, view all users, and read all transactions. -
Rounding Exploit in Stock Trading (CWE-682: Incorrect Calculation) β The
POST /api/tradeendpoint does not enforce a minimum transaction cost. Buying 0.001 shares of a cheap stock (e.g., PONZI at ~3.70 EUR) produces a cost of 0.0037 EUR, which rounds to 0.00. Shares are credited despite zero cost, allowing unlimited free share accumulation and eventual sale for profit.
CVSS v3.1 Score for Broken Access Control: 9.8 (Critical)
| Metric | Value |
|---|---|
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | Low |
| User Interaction | None |
| Scope | Unchanged |
| Confidentiality | High |
| Integrity | High |
| Availability | High |
CVSS v3.1 Score for Rounding Exploit: 6.5 (Medium)
| Metric | Value |
|---|---|
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | Low |
| User Interaction | None |
| Scope | Unchanged |
| Confidentiality | None |
| Integrity | High |
| Availability | None |
Enumeration Summary
Application Analysis
Tech Stack:
- Backend: Express (Node.js) β
X-Powered-By: Express - Frontend: React SPA (static/js/main.e2f3b33f.js)
- Auth: JWT HS256 β payload:
{"id":4,"username":"haxor","role":"user","iat":...} - CORS:
Access-Control-Allow-Origin: *
Target Endpoints Discovered:
| Endpoint | Method | Auth | Notes |
|---|---|---|---|
/api/register |
POST | No | Returns JWT + user object |
/api/login |
POST | No | Standard login |
/api/verify-token |
GET | Yes | Token validation |
/api/profile |
PUT | Yes | Whitelisted fields only |
/api/portfolio |
GET | Yes | Userβs stock holdings |
/api/transactions |
GET | Yes | Userβs transaction history |
/api/stocks |
GET | Yes | Stock listings |
/api/trade |
POST | Yes | Buy/sell stocks |
/api/convert-currency |
POST | Yes | Currency conversion |
/api/admin/flag |
GET | Yes | No role check β VULN |
/api/admin/stats |
GET | Yes | No role check β VULN |
/api/admin/users |
GET | Yes | No role check β VULN |
/api/admin/transactions |
GET | Yes | No role check β VULN |
/api/admin/stocks |
GET | Yes | No role check β VULN |
Attack Chain Visualization
ββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββ
β Register βββββΆβ Map API Surface βββββΆβ Extract Admin β
β Account β β (Caido + JS β β Routes from JS β
β (id:4) β β bundle) β β Bundle β
ββββββββββββββββ ββββββββββββββββββββ βββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β GET /api/admin/flag β
β with regular user β
β JWT β 200 OK β
β FLAG RETURNED β
ββββββββββββββββββββββββ
ββββββββββββββββββββ Secondary Finding ββββββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββ
β Buy 0.001 shares βββββΆβ Cost rounds to βββββΆβ Sell accumulated β
β of PONZI (~3.70) β β 0.00 β shares β β shares for profit β
β ~70 times β β credited free β β +11.19 EUR β
βββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββ
Attack Path Summary:
- Register account and authenticate via JWT
- Map all API endpoints through Caido HTTP proxy
- Extract admin routes from React JS bundle (main.js)
- Access
/api/admin/flagwith regular user JWT β 200 OK, flag returned - Discover rounding exploit: buy 0.001 shares at zero cost, accumulate, sell for profit
Exploitation Path
Step 1: Reconnaissance β Registration and API Enumeration
Registered an account and explored the application through Caido, capturing all HTTP requests. The app is a stock trading platform with support for buying/selling stocks, currency conversion, and portfolio management.
POST /api/register HTTP/1.1
Content-Type: application/json
{
"username": "haxor",
"email": "haxor@test.com",
"password": "password123"
}
Response returned a JWT with payload {"id":4,"username":"haxor","role":"user","iat":...}. The role field in the JWT and the existence of user IDs 1-3 indicated an admin role likely exists.
Step 2: JavaScript Bundle Analysis β Admin Route Discovery
Extracted API routes from developer tools Debugger. Found references to admin endpoints not exposed in the regular UI:
/api/admin/flag/api/admin/stats/api/admin/users/api/admin/transactions/api/admin/stocks
Step 3: Broken Access Control β Admin Endpoints Accessible as Regular User
Tested the admin flag endpoint using the regular user JWT:
GET /api/admin/flag HTTP/1.1
Authorization: Bearer <haxor_jwt_with_role_user>
Response: 200 OK
{"flag":"bug{vjsRfxts5fAaDF1hkkZmhzd8NpW74xCT}"}
No role check performed. Also confirmed /api/admin/stats returns full system statistics (4 users, all stock data, total cash) with the same regular user token.
Step 4: Rounding Exploit β Free Share Accumulation
Discovered that buying very small fractional shares of cheap stocks results in zero cost:
POST /api/trade HTTP/1.1
Authorization: Bearer <haxor_jwt>
Content-Type: application/json
{"stock_id":3,"shares":0.001,"action":"buy"}
Response:
{"total_cost":"0.00","new_balance":"987.52","shares":"0.0010"}
The cost of 0.001 shares of PONZI (~3.70 EUR/share) is 0.0037 EUR, which rounds to 0.00. Shares are credited despite zero cost.
Repeated ~70 times β balance remained at 987.52 throughout. Accumulated shares from 3.001 to 3.07.
Step 5: Profit Realization β Selling Free Shares
Sold 3 shares of the accumulated PONZI stock:
POST /api/trade HTTP/1.1
Authorization: Bearer <haxor_jwt>
Content-Type: application/json
{"stock_id":3,"shares":3,"action":"sell"}
Balance increased from 987.52 to 998.71 β a profit of 11.19 EUR generated from nothing.
Scope note: This exploit is price-dependent. It works on stocks where price * 0.001 < 0.005 (rounds to 0.00). On expensive stocks like 404EX at 116.23/share, 0.001 shares costs 0.12 β correctly rounded up and charged.
Flag / Objective Achieved
β
bug{vjsRfxts5fAaDF1hkkZmhzd8NpW74xCT} β Retrieved via broken access control on /api/admin/flag
Key Learnings
- Authentication is not authorization. The admin endpoints required a valid JWT (authentication) but never checked whether the user had the admin role (authorization). These are separate concerns that must both be implemented.
- Client-side route hiding is not security. The admin routes were absent from the regular UI but present in the JS bundle. Hiding routes in the frontend provides zero protection β the server must enforce access control.
- Always review JS bundles for hidden endpoints. React SPAs compile all route definitions into the JavaScript bundle, making them trivially extractable even when not rendered in the UI.
- Rounding errors in financial calculations enable money generation. When fractional transactions round to zero but the system still processes them, an attacker can accumulate assets for free.
- Test all price tiers for rounding exploits. The vulnerability is price-dependent β it works on cheap stocks but not expensive ones. Testing only one stock could miss the issue.
Tools Used
- Caido - HTTP proxy for API enumeration, request interception and replay
- Browser DevTools - React SPA analysis, JS bundle route extraction
- curl - Direct API endpoint testing
Remediation
1. Missing Authorization on Admin Endpoints (CVSS: 9.8 - Critical)
Issue: All /api/admin/* endpoints are accessible to any authenticated user. The server validates the JWT but does not check the role field before processing admin requests.
CWE Reference: CWE-862 β Missing Authorization
Fix:
// BEFORE (Vulnerable)
router.get('/admin/flag', authMiddleware, (req, res) => {
res.json({ flag: process.env.FLAG });
});
// AFTER (Secure)
const requireAdmin = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
};
// Apply to all admin routes
router.use('/admin', authMiddleware, requireAdmin);
router.get('/admin/flag', (req, res) => {
res.json({ flag: process.env.FLAG });
});
2. Rounding Exploit in Stock Trading (CVSS: 6.5 - Medium)
Issue: The trading endpoint does not enforce a minimum transaction cost. When the calculated cost rounds to 0.00, shares are still credited, allowing unlimited free accumulation.
CWE Reference: CWE-682 β Incorrect Calculation
Fix:
// BEFORE (Vulnerable)
const totalCost = (shares * price).toFixed(2);
// totalCost can be "0.00" but shares still credited
// AFTER (Secure)
const Decimal = require('decimal.js');
const preciseCost = new Decimal(shares).times(new Decimal(price));
const MIN_TRANSACTION = new Decimal('0.01');
if (preciseCost.lt(MIN_TRANSACTION)) {
return res.status(400).json({
error: 'Minimum transaction amount is 0.01 EUR'
});
}
Failed Attempts
Approach 1: Mass Assignment on Profile Update
PUT /api/profile HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json
{"full_name":"test","email":"test@test.com","role":"admin","balance_eur":999999}
Result: Failed - Server whitelists fields, only full_name and email accepted. Extra fields silently dropped.
Approach 2: Negative Values on Trade Endpoint
POST /api/trade HTTP/1.1
Content-Type: application/json
{"stock_id":3,"shares":-1,"action":"buy"}
Result: Failed - Server validates that amounts must be positive (400 response).
Approach 3: Negative Values on Currency Conversion
POST /api/convert-currency HTTP/1.1
Content-Type: application/json
{"from_currency":"EUR","to_currency":"GBP","amount":-100}
Result: Failed - Server validates that amounts must be positive (400 response).
Approach 4: Same-Currency Conversion
POST /api/convert-currency HTTP/1.1
Content-Type: application/json
{"from_currency":"EUR","to_currency":"EUR","amount":100}
Result: Failed - Server correctly rejects with βCannot convert currency to itselfβ.
OWASP Top 10 Coverage
- A01:2021 - Broken Access Control (admin endpoints enforce authentication but not authorization β any user can access admin resources)
- A04:2021 - Insecure Design (admin routes lack systematic authorization layer; role checks should be middleware, not per-endpoint)
- A08:2021 - Software and Data Integrity Failures (rounding exploit stems from trusting floating-point arithmetic without edge case validation)
References
Access Control Resources:
- CWE-862: Missing Authorization
- OWASP Top 10:2021 β Broken Access Control
- OWASP API Security: Broken Function Level Authorization
Financial Logic Resources:
Tags: #broken-access-control #missing-authorization #rounding-exploit #financial-logic #bugforge #webapp