SecureBank is a web application security challenge designed to demonstrate how seemingly independent vulnerabilities can be chained together to achieve complete application compromise. It is also the first CTF challenge I created.
The attack begins with reconnaissance and endpoint enumeration before progressing to SQL Injection against the authentication API. Successfully exploiting the login endpoint allows an attacker to obtain a valid administrator JWT, which grants access to privileged functionality. From there, an insecure EJS-powered reporting feature is abused through Server-Side Template Injection (SSTI), allowing arbitrary server-side JavaScript execution and access to sensitive files.
Rather than focusing on a single vulnerability, this challenge emphasizes the importance of understanding attack chains and demonstrates how weaknesses in authentication, authorization, and template rendering interact to produce a full compromise.
To determine whether template expressions are evaluated server-side, submit:
<%= 7*7 %>
The generated report returns 49 instead of displaying the expression confirming the application is vulnerable to Server-Side Template Injection (SSTI).
EJS is a templating language for Node.js that injects JavaScript expressions into HTML templates. Common tokens:
<%= expression %> — Evaluate expression and output the result (HTML-escaped).
<%- expression %> — Evaluate and output unescaped.
<% code %> — Run JavaScript code without direct output.
EJS templates are rendered server-side, so expressions are executed on the application server. This makes EJS powerful — and potentially dangerous if user input is rendered without restrictions.
SSTI happens when unsanitized user input is inserted into templates that execute code. If the template engine allows arbitrary code execution, an attacker can inject expressions that run on the server, enabling information disclosure, file reads, or remote command execution depending on environment restrictions.
Typical SSTI flow:
Discover that the template engine evaluates expressions.
Execute incremental, low-impact payloads to learn about the environment (cwd, env vars, file list).
Craft a payload to read sensitive files or run commands (only if permitted in the CTF context).
he EJS rendering context is executed as a function with a limited local scope. In that scope, require may not be directly available. However, some global objects (like process) are still accessible. The Node.js runtime exposes an internal reference to the module loader via process.mainModule.require, which allows loading core modules (such as fs and child_process) even when require is not present in the template scope.
Examples:
process.cwd() → works because process is global.
require('fs') → fails: require is not defined in template locals.
global.process.mainModule.require('fs') → works: this uses the main module’s require function reachable through the global process object, bypassing the local scope limitation.
This difference is why the mainModule.require chain is the reliable approach in this challenge.
The most important lesson from this challenge is that attackers rarely depend on a single vulnerability.
Individually, SQL Injection, authentication weaknesses, and Server-Side Template Injection represent distinct security issues. Combined, however, they create a complete attack chain capable of compromising the application.
Designing this challenge reinforced an important principle of offensive security: understanding how vulnerabilities interact is often more valuable than understanding each vulnerability in isolation.
For defenders, this highlights the importance of layered security. Eliminating a single vulnerability may interrupt an attack chain, but designing systems with defense-in-depth ensures that one failure does not immediately result in complete compromise.