The inference charge lands on the victim's credit card. When an operator of the NIK C2 framework issues an ai_generate command and supplies no key of their own, the implant reads the OPENAI_API_KEY environment variable off the machine it has already compromised and uses that value to call https://api.openai.com/v1/chat/completions. OpenAI bills the account that owns the key. OpenAI's access logs record the victim's key and the victim's IP address as the source of the request. The operator spends nothing and appears nowhere.
Cisco Talos published the analysis on October 29, written by Chetan Raghuprasad, after watching a live panel in the wild: IP 87.120.196[.]120, domain total-sitevv[.]lol, serving a TLS certificate with the common name nik-panel. That name matches strings compiled into the implant, which is what moved Talos's assessment to a framework somebody is running against real hosts. There is no CVE, no vendor advisory, and nothing to patch. Coverage is one ClamAV signature, Win.Trojan.GoDiscorder-10060526-0, plus the indicator set in the Talos repository. This week's work is inventory and detection.
Who this applies to
The implant is a 64-bit Windows binary written in Go and compiled with Go 1.23.4, cross-compiled from a Linux build host at /opt/nik_panel/data/build_52b79724a1fc4b8f/main.go. If your endpoints are macOS and Linux, you can close this tab. Nothing below runs on them.
If you run Windows endpoints, the stealer and keylogger half of this applies the way any commodity infostealer does, and the controls are the ones you already know. The narrower job is to find every Windows host where somebody exported an OpenAI key into a persistent environment variable: the developer who wired up an internal chatbot last quarter, the data engineer summarizing batches on a desktop, the build agent with a key in its service account profile. On those hosts the key is a line item with a payment method behind it.
Small teams carry more of this risk than large ones, for a dull reason. A company with a secrets platform issues short-lived, project-scoped keys. A six-person shop that shipped an AI feature in March has one key in a User environment variable on one laptop, with no spend cap and no one watching the usage graph.
What the ai_generate handler actually does
The handler takes three fields from the C2 command payload: prompt, api_key, and model. An empty prompt returns an error with no outbound call. A missing api_key triggers the environment-variable fallback. A missing model falls back to a five-character string in the binary's data section that decodes to gpt-4; an operator can pass any valid model identifier and pick a newer model without rebuilding the implant.
The request body is a four-key map: model, messages, temperature, and max_tokens, the last two read from constants in the binary. The messages array holds a 68-byte hardcoded system instruction followed by the operator's prompt. The POST carries a 60-second timeout. The reply is decoded into an untyped map, walked through choices to message to content, and returned to the panel with four fields: status set to generated, code holding the model's text, model naming what answered, and usage carrying the prompt, completion, and total token counts.
That last field is the one I keep coming back to. The operator gets a per-request token meter on an account they do not own and cannot be traced through. Talos frames the wider shift as collapsing collection and exploitation into one session, and the sequencing is the point: a classic stealer ships an archive home and a human reads it hours later, and that delay is the window a defender gets. Here the triage runs on the host while the keylogger is still streaming.
Everything around the AI call is commodity tradecraft
The transport runs two channels at once. A persistent WebSocket over TLS on port 443 is primary; an HTTP loop posting roughly every 250 milliseconds to /api/c2/update is the fallback and keeps working when the socket drops. Every message is AES-256-GCM encrypted under a 32-byte key derived from an obfuscated seed in the binary, wrapped alongside a plaintext agent ID of the form ag_<16 hex chars>. Three prioritized C2 tiers survive takedowns: runtime-updated URLs, an embedded encrypted server list, and a hardcoded fallback. The TLS session pins a certificate instead of validating against the system store, which Talos notes prevents an interception proxy from reading the traffic.
The HTTP endpoints are distinctive enough to be worth a proxy rule on their own: /api/c2/update, /api/c2/result, /api/c2/screen, /api/c2/file_chunk, /api/c2/rproxy_up, and /api/c2/ws.
The collection set
- Browser credential databases and cookies from Chrome, Edge, Brave, Opera, and Firefox, plus Discord tokens from the LevelDB store.
- Saved Wi-Fi network names and plaintext keys, pulled through
netsh. - Desktop wallet paths including
/AppData/Roaming/Exodus/exodus.wallet,/AppData/Roaming/Ethereum/keystore,/AppData/Local/LedgerLive, and Bitcoin Core'swallet.dat, plus extension wallets addressed by extension ID. - A keylogger polling all 256 Windows virtual key codes, streaming each keypress as an encrypted JSON frame and buffering 10,000 entries.
- Two case-insensitive filesystem-wide regular expressions, with matches archived into
browserdata.zip.
Read those two patterns as a defender rather than as a thief:
(?i)(seed|phrase|mnemonic|recovery|backup|wallet).*(\.txt|\.md|\.csv|\.dat)
(?i)(private|privkey|secret).*(\.txt|\.md|\.key|\.pem)
Neither pattern is anchored, so both match anywhere in a path. The first targets cryptocurrency recovery material. The second collects secrets.md, private.key, every .pem in a repository checkout, and the backup-notes.txt on a share. An engineering workstation produces dozens of hits with no wallet on it at all.
Evasion is layered, and every layer leaves an artifact. The binary hides its console, copies itself to %appdata%/Roaming/Microsoft/Network, renames itself csrss.exe in the temp directory, masquerades as svchost.exe or simvol.exe, writes a Run key for autostart, and disables Recent Documents telemetry. It calls os.Exit(0) when IsDebuggerPresent or CheckRemoteDebuggerPresent returns true, leaving no crash record. It escalates silently through a fodhelper.exe COM hijack, writing its path into HKCU\Software\Classes\ms-settings\Shell\Open\command and deleting the key within two seconds. It impairs defenses by patching AmsiInitFailed in System.Management.Automation.dll with a six-byte stub, so AMSI inspects nothing for the rest of that PowerShell session.
One layer changes how you respond, so decide about it before you are at the keyboard. The implant sets ProcessBreakOnTermination through NtSetInformationProcess with parameter 0x1D, so killing it from Task Manager or taskkill bugchecks the host and destroys the memory you wanted. Isolate at the network layer, capture volatile state, then take the machine down on your terms.
The ransomware module never encrypts a file
The destructive stage uses living-off-the-land binaries throughout. PowerShell performs a raw read of \\.\PhysicalDrive0 and writes the master boot record to mbr_backup.bin in the temp directory. The implant builds a self-contained lock_screen.html at runtime, parameterized with operator-supplied Bitcoin and USDT addresses, prices, a contact email, and a timer. It writes lock_start.bat, which calls mshta.exe against that HTML in an infinite loop with a five-second sleep, so a closed window reopens. A reg add writes a Run value named SystemLock pointing at cmd /c on the batch file, and a goroutine relaunches the lock screen if it dies.
Nothing is encrypted. No modification timestamps change in bulk, no encryption markers appear, and the canary files your backup product watches stay untouched, so every mass-file-change detection you own stays quiet. What exists on disk is three files in %TEMP% and one registry value, and a responder who can reach safe mode removes it in five minutes. That combination is cheap to deploy and cheap to survive, which is why a smaller operator reaches for it: no key escrow, no decryptor, no negotiation infrastructure.
The bootkit module is staging work rather than a payload. It parses bcdedit /enum output for EFI, Windows Boot Manager, and Windows Legacy OS Loader to classify the firmware path, runs Confirm-SecureBootUEFI to record Secure Boot state, and drops a 16-byte bootkit.bin in the temp directory containing the ASCII string NIK_BOOTKIT_PREP. The file proves the path is writable and gives a later file-write command a target. Treat it as a statement of intent, not a boot compromise.
Three checks that do not need a signature
Start with the key inventory, because it is read-only, it takes minutes, and it tells you the size of the problem. Report the length of each value, never the value:
# openai-key-exposure.ps1 - where does this host hold an AI provider key?
# Read-only. Run per host, or wrap the body in Invoke-Command for a fleet.
foreach ($scope in 'Machine','User','Process') {
[Environment]::GetEnvironmentVariables($scope).GetEnumerator() |
Where-Object { $_.Key -match '(?i)OPENAI|ANTHROPIC|_API_KEY$' } |
ForEach-Object {
[pscustomobject]@{
Scope = $scope
Name = $_.Key
Length = $_.Value.Length # length only - do not log the secret
}
}
}
# Other profiles on the box: service accounts, build agents, shared logins.
Get-ChildItem 'Registry::HKEY_USERS\*\Environment' -ErrorAction SilentlyContinue |
ForEach-Object {
$sid = ($_.Name -split '\\')[1]
$_.Property |
Where-Object { $_ -match '(?i)OPENAI|ANTHROPIC' } |
ForEach-Object { '{0} {1}' -f $sid, $_ }
}
Second, watch the egress. The ai_generate handler has one destination and no fallback provider, so a call to the inference endpoint from a binary that has no business making one is a high-quality signal. This runs against DeviceNetworkEvents; the same shape works on any proxy log with process attribution. Build the allowlist from your own thirty-day baseline first:
// Calls to the OpenAI inference endpoint from unexpected binaries.
let sanctioned = dynamic(["python.exe","node.exe","code.exe","pwsh.exe",
"powershell.exe","curl.exe","chrome.exe","msedge.exe"]);
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has "api.openai.com"
| where InitiatingProcessFileName !in~ (sanctioned)
| summarize Calls = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp)
by DeviceName, InitiatingProcessFileName, InitiatingProcessFolderPath
| order by Calls desc
Third, hunt the artifacts, which are specific enough to query directly. The SystemLock value, the ms-settings COM key, and the file names from the destructive and staging stages all land in registry and file event tables, and the same fields are available from Sysmon events 11, 12 and 13 if that is what you run:
// NIK C2 persistence, ransomware staging, and bootkit marker artifacts.
union
(DeviceRegistryEvents
| where RegistryValueName == "SystemLock"
or RegistryKey has @"Classes\ms-settings\Shell\Open\command"
| project Timestamp, DeviceName, InitiatingProcessFileName,
Artifact = strcat(RegistryKey, " :: ", RegistryValueName)),
(DeviceFileEvents
| where FileName in~ ("bootkit.bin","mbr_backup.bin","lock_screen.html",
"lock_start.bat","browserdata.zip")
or FolderPath has @"\AppData\Roaming\Microsoft\Network"
| project Timestamp, DeviceName, InitiatingProcessFileName,
Artifact = FolderPath)
| order by Timestamp desc
For the hosts the first query flags, the fix is configuration rather than detection. Move the key out of a persistent environment variable into a store the process reads at runtime, give each key its own project scope, and set a hard monthly spend cap, which OpenAI's production guidance covers. A cap turns theft into an alert on day one instead of an invoice on day thirty. Then block api.openai.com at the egress proxy for every host that is not a developer workstation. Ransomware activity rose again through the third quarter in Talos incident response data, and a screen locker that deploys in seconds fits the economics of that trend.
Find out which of your hosts have OPENAI_API_KEY set
Run the inventory script today. For most small businesses the honest answer is two or three machines, and one belongs to whoever built the chatbot. Rotate those keys into project scopes with spend caps, pull them out of persistent environment variables, add the egress rule, and keep the two hunting queries. No patch exists to schedule and no vendor is coming, so the deadline is the one you set. If a host comes back dirty, isolate it and leave the process running until you have memory. We help small teams build and rehearse the playbook for that morning, before it arrives.
Need an incident response plan before the next attack?
We help organizations build and test incident response playbooks, including the decisions you have to make in the first ten minutes on a host like this one. Book a session with our team.
