HKCERT CTF 2023: Mongo Jail
Escaping a restricted mongosh sandbox using constructor.constructor to reach the Function constructor, then loading Node built-ins for arbitrary RCE.
This challenge drops us into a restricted mongosh sandbox where the goal is to escape and execute arbitrary code. At first glance, the environment looks pretty locked down: dangerous keywords like require and eval are filtered, and direct access to sensitive global objects is blocked. The sandbox clearly expects us to get stuck inside harmless JavaScript execution, unable to reach the underlying system.
However, JavaScript sandboxes are notoriously hard to secure, mainly because the language exposes powerful features indirectly. Even if certain functions are blacklisted, JavaScript's object model often provides alternative paths to reach them. In this case, the main weakness comes from the fact that constructors are still accessible. By chaining the constructor property twice, we can reach the Function constructor, which effectively behaves like eval and allows us to execute arbitrary JavaScript code.
[]['constructor']['constructor']('var exec = require("child_process").exec;
exec("ls", function(err, stdout, stderr) { console.log(stdout); });')()To confirm code execution, we start with a payload that rebuilds access to require and loads Node's child_process module. From there, we can call exec and run system commands like ls, printing the output directly back to the console. This proves the sandbox is broken and that we have achieved command execution through the Function constructor trick.
[]['constructor']['constructor']('var fs=require("fs"); console.log(fs.readdirSync("venv/bin"))')()Once RCE is confirmed, the next step is to switch from shell commands to file system access, since flags are usually stored in files. Using the same constructor.constructor technique, we load the fs module and list directories to understand the environment. This makes it easy to locate interesting paths and files without relying entirely on external command execution.
Eventually, we identify a suspicious file located at /proof_CBg0IiyEoIHTxFLZEaB4mKma9TlC1UmFCsVdnyuH.sh, which strongly suggests it contains the flag or the logic to generate it. Using fs.readFileSync, we directly read the file contents and print them to the output. At that point, the sandbox is fully bypassed, and we have complete access to sensitive data.
[]['constructor']['constructor']('var fs=require("fs"); console.log(fs.readFileSync("/proof_CBg0IiyEoIHTxFLZEaB4mKma9TlC1UmFCsVdnyuH.sh", "utf8"))')()In the end, this challenge is a good reminder that keyword filtering is not real sandboxing. Even if direct calls to require or eval are blocked, JavaScript provides too many indirect ways to recover equivalent functionality. The constructor.constructor technique remains one of the most reliable jailbreak methods, and once we reach Node's built-in modules like fs, the challenge is essentially over.
