BLOG

Neutralinojs Compromised In DPRK Attack

DPRK threat actors compromised Neutralinojs as part of a larger attack that utilizes stolen GitHub credentials to force-push backdated malicious commits

By c0a15726-c5b1-4b0d-85e6-fe15553df9e2 ยท

Neutralinojs Compromised In DPRK Attack

The OpenSourceMalware team has identified a new attack on the popular development framework Neutralinojs. Neutralinojs is a platform that lets developers build lightweight cross-platform desktop applications using JavaScript, HTML and CSS. The project has 8400 stars on GitHub, 488 forks and dozens of contributers. The OSM team immediately removed the malicious code with a GitHub pull request and disclosed the attack to the Neutralinojs maintainers. We are working closely with the maintainers to provide threat intelligence and incident response assistance.

Four repositories belonging to the Neutralinojs GitHub organization -- a popular cross-platform desktop application framework with over 9,000 combined stars and 500+ forks -- have been compromised with malicious JavaScript payloads. The attack occurred on March 2, 2026 at 04:38 UTC, when all four repositories were force-pushed with malicious commits in a single automated 132-second burst. The commits were backdated between 5 and 35 days to blend in with legitimate repository history, making them appear as routine changes from January and February. The attack remained undetected for 3 days until March 5. The attacker used a consistent technique of hiding malicious code injections inside seemingly routine commits by appending obfuscated payloads after hundreds of whitespace characters.

Neutralinojs GitHubUnfortunately while we have moved quickly to remove the malicious code, many users and projects have pulled the malicious versions since the compromise. Two command-and-control servers hosted on Vercel were removed by the Vercel security team after OSM alerted them to the endpoints.

The Background

This incident that affected the Neutralinojs GitHub organization is part of a larger ongoing attack targeting hundreds of open source maintainers and organizations by DPRK. OpenSourceMalware has attributed this new campaign to DPRK by leveraging our threat intelligence to tie the GitHub persona used in this attack to earlier atributed DPRK incidents. Additionally, the OSM team has identified the malware used in the final payload is the most recent Beavertail malware typically used by DPRK in their final payloads.

North Korean threat actors are known for their "Contagious Interview" fake recruiter campaigns, and the earlier "Dream Job" campaign. However, this latest attack is the first time we've seen North Korean threat actors compromise individual GitHub accounts with stolen credentials to poison upstream repositories. This should be concerning to anyone reading this as we don't know how DPRK has gotten these credentials. Do they have some yet unknown way of stealing these from unsuspecting GitHub users?

Our research is ongoing and you can expect more from us on this new broader attack very soon. For now, let's concentrate on Neutralinojs and the hundreds of developers that use and depend on this package every day.

Affected repositories

All four repositories were compromised in a single automated attack on March 2, 2026 between 04:38 and 04:40 UTC. The commit dates shown in the "Spoofed Date" column are the backdated timestamps the attacker embedded in the commits to make them appear as routine historical changes.

Repository

Stars

Actual Push (Mar 2)

Spoofed Date

Malicious Commit

Infected File

neutralinojs/neutralino.js

332

04:38:28 UTC

Feb 26, 2026

90665b4

.env, src/index.ts

neutralinojs/neutralinojs

8,370

04:39:09 UTC

Feb 27, 2026

d62abe0

spec/runner.js

neutralinojs/neutralinojs-cli

113

04:40:04 UTC

Jan 26, 2026

720c0e3

src/constants.js

neutralinojs/neutralinojs.github.io

257

04:40:42 UTC

Feb 21, 2026

6c19a31

babel.config.js

Attack Timeline

How We Uncovered the Real Timeline

When we first examined the malicious commits, they appeared to span over a month -- from January 26 to February 27, 2026. However, git commit timestamps are trivially spoofable. The author.date and committer.date fields are set by the local git client and can be set to any value.

GitHub's server-side data tells a very different story. By examining GitHub Actions triggering_actor fields (which record who actually triggered a workflow run and cannot be forged), PR event logs, and push event timestamps, we discovered that all four repositories were compromised in a single 132-second automated attack on March 2, 2026, by a single GitHub account: alphagamer7.

The commit dates were deliberately backdated -- between 5 and 35 days -- to make the malicious commits blend seamlessly into the existing repository history. This timestamp spoofing technique is what initially led to the incorrect chronology.

The Real Attack: March 2, 2026 -- 04:38 to 04:40 UTC

The GitHub account alphagamer7 (Athif Shaffy), a Sri Lankan software developer who had write access to the neutralinojs organization since 2019, was used to force-push malicious commits across all four repositories in rapid succession. The account is most likely compromised -- and the Neutralinojs maintainers have taken actions to contain this users access while they identify what happened.

The attacker used two identity spoofing techniques: attributing commits to the project maintainer shalithasuranga on three repos, and to github-actions[bot] on the CLI repo. Only GitHub's unspoofable server-side triggering_actor field reveals the true pusher.

04:38:28 -- neutralino.js (spoofed date: February 26)

The client library repository was hit first. Commit `90665b4bed716c05e75ec181ddf7af9345fa1591`, attributed to Shalitha Suranga with the message "Add SendKeyState for computer.sendKey," amended a real feature commit with hidden malicious changes:

  1. .gitignore -- removed .env and .env.test from the ignore list, added config.bat

  2. .env -- new file added containing a base64-encoded C2 URL

  3. src/index.ts, package.json -- malicious code injected alongside legitimate feature code

The .env file contains:

AUTH_API_KEY="aHR0cHM6Ly9hdXRoLXJoby1kdW4udmVyY2VsLmFwcC9hcGk="

Base64-decoding the value reveals a second command-and-control endpoint:

hxxps://auth-rho-dun[.]vercel[.]app/api

This endpoint was live at the time of discovery, serving a 4,777-byte obfuscated JavaScript payload.

The .gitignore change is the attacker's signature: in every compromised repository, the attacker removes .env from the gitignore (allowing environment files containing C2 URLs to be committed) and adds config.bat (hiding their Windows batch tooling from the repository).

04:39:09 -- neutralinojs main repo (spoofed date: February 27)

Thirty-one seconds later, the highest-impact attack targeted the main framework repository (8,370 stars, 490 forks). Commit `d62abe0288901ba91fe7782094342d4ade2492ce`, attributed to Shalitha Suranga with the message "Update CHANGELOG.md," contains:

  1. .gitignore -- added config.bat and node_modules

  2. CHANGELOG.md -- one line added (cover)

  3. spec/runner.js -- payload injected

Neutralinojs Added Line

The spec/runner.js file is a test runner used by the project's CI pipeline. The payload was appended to the closing }; of the module export after a wall of whitespace, changing the file from ~3,000 bytes to over 8,000 bytes.

In addition to the main branch, alphagamer7 force-pushed 7 additional branches (pr1338, imgclipboard, cleanupwv, wincenter, refactunic, winpos, and the Dependabot PR #1580 branch) in 46 seconds -- spreading the malicious commit across as many refs as possible to maximize the chance of infection. These branches have since been deleted during cleanup.

04:40:04 -- neutralinojs-cli (spoofed date: January 26)

The CLI repository attack used the most sophisticated spoofing technique. Commit `720c0e39c02184952d24fb5dabe19d8d83c89f53`, attributed to github-actions[bot] with the message "Update changelog for v11.7.0," was backdated 35 days to January 26.

The commit contains three file changes:

  1. CHANGELOG.md -- two lines added (legitimate-looking)

  2. .gitignore -- removed .env and .env.test from the ignore list, added config.bat

  3. src/constants.js -- one line modified (payload injected)

Neutralinojs GitHub Malicious

This was particularly clever because the repository had legitimate github-actions[bot] commits with the exact same message ("Update changelog for v11.7.0") from the real v11.7.0 release on January 26. Those legitimate commits only modified CHANGELOG.md. The attacker amended this commit type to also inject .gitignore and src/constants.js, making it appear as routine CI output. The 35-day backdate placed it right in the middle of the real release activity.

The modification to src/constants.js appears as a single-line change. Examining the file reveals the malicious payload appended to the end of the legitimate module export, separated by hundreds of space characters:

// Legitimate code at the start of the file:
module.exports = {
  remote: {
    binariesUrl: "https://github.com/neutralinojs/neutralinojs/releases/...",
    // ... normal configuration ...
  }
};                                                          [~500 spaces]  global['!']='9-547-3';var _$_1e42=...

How to distinguish the malicious commit from the legitimate ones:

Commit

Author

Files Modified

Malicious?

ad39386

github-actions[bot]

CHANGELOG.md only

No

7cbcbc8

github-actions[bot]

CHANGELOG.md only

No

`720c0e3`

github-actions[bot]

CHANGELOG.md + .gitignore + src/constants.js

Yes

04:40:42 -- neutralinojs.github.io (spoofed date: February 21)

The final target was the documentation website. Commit `6c19a3106b6f6f2725c530e37bfac7f99c2cc82b`, attributed to Shalitha Suranga with the message "Update docusaurus.config.js," modifies three files:

  1. .gitignore -- removed .env.local from the ignore list, added config.bat

  2. babel.config.js -- payload injected (file grew from ~100 bytes to 5,379 bytes)

  3. docusaurus.config.js -- minor legitimate-looking change (cover)

The babel.config.js file is a textbook example of the injection technique. The legitimate content is just three lines:

module.exports = {
  presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

After the closing semicolon, hundreds of space characters pad the line before the obfuscated payload begins. To a developer reviewing the diff on GitHub, the change appears as a trivial semicolon addition. The payload is only visible by scrolling far to the right, or by examining the raw file size.


Were the Neutralinojs NPM packages affected?

The Neutralinojs team maintains 8 NPM packages. The GitHub token that was compromised did not have access to NPM, and our analysis shows that the NPM packages were not compromised.

Neutralinojs NPMWhat does the malware it do and how does it work?

The malware uses a novel blockchain-based command and control architecture that stores encrypted payloads in Tron, Aptos, and Binance Smart Chain (BSC) transactions, making the C2 infrastructure resistant to takedown.

The chain consists of five stages, each progressively deobfuscated at runtime. We are going to simplify the first four stages of the attack, and then spend more time explaining the final stage.

Stage 0: The GitHub Compromise

The GitHub account alphagamer7, a long-time neutralinojs contributor with organization-level write access, is compromised. The threat actor uses this account to force-push amended commits across four neutralinojs repositories, injecting malicious payloads into spec/runner.js, src/constants.js, src/index.ts, and babel.config.js. The commits are attributed to shalithasuranga (the org owner) and github-actions[bot] to hide the true actor, with timestamps backdated weeks to blend into the repository history.

Stage 1: sfL Obfuscation Layers

  • Size: 4,784 bytes (C2-served variant)

  • Purpose: Decode and execute the blockchain C2 fetcher

  • SHA256 (C2 variant): 904afe0337fbbd79def403b3204f75b4c5fbe4e2271252d22c0307f9cbd14646

The first stage payload is a heavily obfuscated JavaScript file. The payload consists of three nested obfuscation layers, each with a distinct purpose.

Stage 2: Blockchain C2 Fetcher

  • Size: 3,858 bytes (decoded from Stage 1)

  • Purpose: Fetch encrypted payload from blockchain, XOR decrypt, execute

Again, this is a heavily obfuscated JavaScript file. Once decoded, Stage 2 reveals the core innovation of this malware family: a three-layer blockchain C2 architecture.

First, the payload queries the Tron API for the most recent transaction from a hardcoded wallet address. If Tron fails, it falls back to the Aptos blockchain. Finally, the malware uses the resolved transaction hash, and a JSON-RPC call retrieves the BSC transaction's input data. It's a nifty little way to hide payloads in an immutable environment.

Stage 3: Recursive C2 + Environment Configuration

  • Size: 5,895 bytes (Channel 1 XOR-decrypted from BSC)

  • SHA256: 11e87f7f27b3cf1a51e0b4b3903decd8945b5959eedf3cbc6be1920dab3c8823

  • Purpose: Configure C2 infrastructure, set environment variables, recursively fetch Stage 4

Yet again, this is a heavily obfuscated JavaScript payload. This payload makes sure that the node environment and dependencies areprepared to download the final Beavertail malware.

Stage 4: The Final Payload

  • Malware Family: BeaverTail (DPRK Lazarus Group)

  • Campaign: "Contagious Interview" supply chain attack

  • File: stage4-decrypted.js (70,818 bytes)

  • SHA256: d4e269df0f50998c7ebf2bf56945d3d615fd6516702b1da8ac030ffcba735263

This last payload appears to be a new version of the Beavertail malware. The embedded ss_* command prefix in the sample is BeaverTail's unique signature, seen across all known samples from the DPRK "Contagious Interview" campaign.

What It Does

Primary Targets:

  • Discord - Steals authentication tokens and user data

  • VSCode - Compromises extensions (especially @vscode/deviceid)

  • Cursor Editor - Infects application resources

  • Python - Targets development environments

Attack Capabilities:

  1. Credential Theft: Harvests Discord tokens, GitHub credentials, npm tokens, SSH keys, and cryptocurrency wallet data

  2. Persistence: Injects malicious code into node_modules of multiple applications, modifying startup files (main.js, index.js)

  3. Remote Control: Establishes WebSocket-based C2 over HTTPS (port 443) with full remote code execution

  4. Data Exfiltration: Uploads source code, credentials, and system information to attacker infrastructure

  5. Cross-Platform: Operates on Windows, macOS, and Linux

How It Works

Obfuscation:

  • 1,101 RC4-encrypted strings using 72 unique 4-character keys

  • CPU-intensive array shuffle loop (checksum: 0xbe519) to evade analysis

  • 19 wrapper functions obscure string decoder calls

  • Anti-sandbox checks prevent execution in virtualized environments

Command & Control: Uses BeaverTail's signature ss_ command prefix for WebSocket C2:

  • ss_eval64 / ss_eval - Execute arbitrary JavaScript

  • ss_connect - Establish C2 connection

  • ss_info / ss_ip - Collect system information

  • ss_upd / ss_upf - Update payload or upload files

  • ss_dir / ss_fcd - Directory and file operations

  • ss_inz - Initialize persistence

Detection

File Indicators:

*/node_modules/@vscode/deviceid/dist/index.js
*/node_modules/discord_desktop_core*/index.js
*/Cursor.app/Contents/Resources/app/*

Code Signatures:

  • Functions named ss_eval, ss_connect, ss_info

  • Variables: _t_c, _t_s, _t_u (tracking/telemetry)

  • Code names: "antigravity", "cloudchamber"

  • WebSocket patterns: identify, client_id, emit

Command and Control Infrastructure

Two Vercel-hosted C2 endpoints are actively serving payloads:

C2 URL

Status

Payload Size

Variant Marker

hxxps://data-kappa[.]vercel[.]app/

Live

4,784 bytes

global['!']='8'

hxxps://auth-rho-dun[.]vercel[.]app/api

Live

4,777 bytes

global['!']='9'

The C2 payloads also reference blockchain addresses for resilient command delivery:

  • Tron: TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP, TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG

  • Aptos: 0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e

Impact Assessment

OpenSourceMalware gets involved

When our team initially saw that the neutralinojs/neutralinojs repository was compromised, and that it was a popular repository, we decided to get proactive. We authored a GitHub pull request that stripped out the malicious code from the spec/runner.js file.

GitHub Fix PR

One of the neutralinojs maintainers quickly merged the PR and the malicious code was removed from the origin.

Fork Contamination

We checked all 490 forks of the main neutralinojs/neutralinojs repository for the presence of the malicious commit SHA (d62abe0). At the beginning of the incident there were dozens of malicious forks visible on GitHub. As of 11 pm AEST there are only 16 forks remaining that are still malicious.

This is due to GitHub's shared object store architecture -- forks share the underlying git database with their parent repository. When the malicious commit was pushed upstream, it became accessible to every fork automatically. Similarly, when the OSM team pushed the fix PR, that meant that anyone who syncs with origin will get the clean version again.

Download Exposure

The @neutralinojs/neu CLI package receives approximately 7,150 downloads per month from npm. The @neutralinojs/lib client library receives approximately 5,250 downloads per month. While the npm packages themselves were not directly compromised in this attack, developers who clone the repositories directly (a common practice for contributors and those building from source) would receive the malicious payloads.

The Attacker's Playbook

Across all four compromised repositories, the attacker followed an identical operational procedure -- executed simultaneously via automation:

  1. Amend existing commits -- take a real commit by the maintainer or CI bot, add malicious files to it, and force-push the rewritten history

  2. Spoof the authorship -- attribute the amended commits to shalithasuranga or github-actions[bot] so they appear to come from trusted sources

  3. Backdate the timestamps -- set commit dates weeks in the past so the malicious changes blend into existing repository history and don't appear at the top of the commit log

  4. Choose plausible commit messages -- "Update CHANGELOG.md," "Update changelog for v11.7.0," "Add SendKeyState for computer.sendKey" -- messages that would not raise suspicion in a review

  5. Modify `.gitignore` -- remove .env from the ignore list (enabling C2 URL drops via .env files) and add config.bat (hiding Windows tooling)

  6. Inject the payload -- append the obfuscated sfL payload to a JavaScript file after hundreds of whitespace characters, making the injection invisible in standard diff views

  7. Maximize infection surface -- force-push the same malicious commit into multiple branches (8+ branches on the main repo alone, including an open Dependabot PR)

The timestamp spoofing was effective: our initial analysis incorrectly concluded the attacks occurred over a month-long period. Only by examining GitHub's unspoofable server-side data (Actions triggering_actor fields and event timestamps) did we discover the true 132-second attack window.

Detection

The following strings are unique to this obfuscation family and can be used for detection:

# Primary detection -- search for the Layer 1 input string
grep -r 'rmcej%otb%' --include="*.js" --include="*.mjs" --include="*.cjs" --include="*.ts" .

# Secondary detection -- search for the scrambled "constructor" string
grep -r 'wuqktamceigynzbosdctpusocrjhrflovnxrt' --include="*.js" .

# File size anomaly -- config files should not be this large
find . -name "babel.config.js" -o -name "postcss.config.mjs" -o -name "tailwind.config.js" | \
  xargs -I{} sh -c 'size=$(wc -c < "{}"); if [ "$size" -gt 2000 ]; then echo "SUSPICIOUS: {} ($size bytes)"; fi'

GitHub code search:

https://github.com/search?q=rmcej%25otb%25&type=code

This search currently returns 116 infected files across 106+ repositories, indicating the attack extends far beyond the Neutralinojs organization.

Indicators of Compromise

Malicious Commits (Neutralinojs Org)

All commits were force-pushed on 2026-03-02 between 04:38-04:40 UTC by the compromised alphagamer7 account. The "Spoofed Date" column shows the backdated timestamps embedded in the commits.

Repository

SHA

Actual Push (Mar 2)

Spoofed Date

Spoofed Author

neutralino.js

90665b4bed716c05e75ec181ddf7af9345fa1591

04:38:28

2026-02-26

shalithasuranga

neutralinojs

d62abe0288901ba91fe7782094342d4ade2492ce

04:39:09

2026-02-27

shalithasuranga

neutralinojs-cli

720c0e39c02184952d24fb5dabe19d8d83c89f53

04:40:04

2026-01-26

github-actions[bot]

neutralinojs.github.io

6c19a3106b6f6f2725c530e37bfac7f99c2cc82b

04:40:42

2026-02-21

shalithasuranga

C2 URLs

  • hxxps://data-kappa[.]vercel[.]app/

  • hxxps://auth-rho-dun[.]vercel[.]app/api

C2 Payload Hashes (SHA256)

  • 904afe0337fbbd79def403b3204f75b4c5fbe4e2271252d22c0307f9cbd14646 (data-kappa)

  • a507b74b6b1e25444c586bc67ae0244cba3037f2b39f25f7eb507ded97c373c1 (auth-rho-dun)

Blockchain C2 Addresses

  • Tron: TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP

  • Tron: TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG

  • Aptos: 0xbe037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e

  • Aptos: 0x3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3

Obfuscation Signatures

  • Layer 1 input string: rmcej%otb%

  • Layer 1 seed: 2857687

  • sfL seed: 2667686

  • sfL modulus: 4289487

  • Scrambled constructor string: wuqktamceigynzbosdctpusocrjhrflovnxrt

Attacker Artifacts in .gitignore

All compromised commits add config.bat to .gitignore and remove .env / .env.test entries.

Recommendations

For Neutralinojs Users

  1. If you have cloned any Neutralinojs repository since March 2, 2026, check for the presence of the payload: grep -r 'rmcej%otb%' .

  2. If you find the payload on a system where it was executed, assume full compromise -- rotate all credentials, tokens, and SSH keys accessible from that machine

Broader Context

This compromise of the Neutralinojs organization is part of a larger campaign. The same obfuscation family has been identified in 106+ repositories across GitHub, with payloads injected into a wide variety of JavaScript configuration files including tailwind.config.js, postcss.config.mjs, next.config.mjs, eslint.config.mjs, vite.config.ts, and application source files. The attack was carried out using the compromised GitHub account alphagamer7, a long-time neutralinojs contributor whose credentials were likely stolen through social engineering. A full analysis of the broader campaign, including attribution to a DPRK-linked threat actor operating under the GitHub handle "Polin9912," will be published in a subsequent report.