# GHAPPIER Loader: npm Supply-Chain Compromise of @dforge-core/dforge-mcp 0.2.21 Beside DPRK-Linked PolinRider Campaign

> CloudSEK reports GHAPPIER, a previously unreported loader operation spanning at least 65 public repositories, 73 infected files and 22 accounts. On 9 September 2026 an attacker used the maintainer access of npm package @dforge-core/dforge-mcp for 105 minutes, added a remote code loader, modified the release workflow and published malicious version 0.2.21 (latest for 35 min 38 s) with valid npm provenance before the maintainer reverted to clean 0.2.22.

- **Published:** 2026-11-09T05:16:58Z
- **Last reviewed:** 2026-11-09T05:16:58Z
- **Canonical:** https://intel.threadlinqs.com/threat/TL-2026-5524
- **Card image:** https://intel.threadlinqs.com/og/threat/TL-2026-5524.png
- **ID:** TL-2026-5524
- **Severity:** HIGH
- **Category:** SUPPLY_CHAIN
- **Status:** ACTIVE
- **Detections:** 9 (1 shown free below) · **IOCs:** 14

## Description

GHAPPIER is a loader family reported by CloudSEK (Vikas Kundu, 20 Sep 2026) that spans at least 65 public repositories, 73 infected files and 22 accounts. The headline incident is the npm package @dforge-core/dforge-mcp (an MCP server). On 9 September 2026 someone with push access to the package's GitHub repository used the maintainer's access for 105 minutes. CloudSEK could not confirm how access was obtained; it suspects a developer machine infected through a malicious extension or package, and considers PolinRider's documented harvesting of cached git credentials the leading hypothesis.

Sequence of events: the attacker added a remote code loader to the package; changed three lines so that any push to main started the release workflow; roughly 14 minutes later rewrote the workflow so it could publish unattended; and published version 0.2.21. A first malicious release attempt, 0.2.20, failed and broke installation. Version 0.2.21 was the npm 'latest' for 35 minutes 38 seconds before the maintainer published clean 0.2.22.

Version 0.2.21 carried VALID npm provenance: it was built by GitHub Actions through OIDC trusted publishing and its Sigstore attestation names the attacker's commit, so it passed 'npm audit signatures'. Under trusted publishing, push access to the repository was effectively publish access and no npm credential was needed. Provenance shows where an artifact was built, not whether the source was honest.

The payload is a single line at line 3320 of a roughly 99 KB configuration file. It opens a four-stage chain whose final implant is a general-purpose remote shell that deletes itself from disk as it runs. The loader executes when the MCP server is launched, not at install time, so systems that installed 0.2.21 but never started it did not run it. All stages still responded when CloudSEK tested five days after the package was withdrawn.

PolinRider link: in a second victim repository CloudSEK found a second payload family matching PolinRider, a campaign tracked by OpenSourceMalware since March 2026 and described by OpenSourceMalware as DPRK-linked (Lazarus / Contagious Interview cluster). An exact match was still beaconing at time of writing. That payload has no hardcoded C2: the C2 address is read from an Ethereum wallet, with configuration stored in the 20-byte recipient address of an empty transaction costing about $0.20. CloudSEK cites the DPRK attribution (from the researchers behind NullReceiver) rather than asserting it; its one independent check did not confirm it, and CloudSEK gives no attribution of its own for GHAPPIER. OpenSourceMalware's public PolinRider reporting documents config-file payload appending, blockchain dead-drops (TRON, Aptos, BSC), BeaverTail/InvisibleFerret payloads and git history rewriting; those details are PolinRider context and are not confirmed as GHAPPIER behavior.

Impact: CloudSEK reports no evidence of a successful compromise of any organization and describes the exposed population as small. No CVE exists. As of CloudSEK's report no advisory existed in OSV, the GitHub Advisory Database or from the maintainer. The full IOC list (two delivery hostnames, the socket endpoint, Ethereum wallet, hashes) is in CloudSEK's linked PDF, which could not be machine-read during this research, so those values are not included here.

## MITRE ATT&CK

- T1195.002 Compromise Software Supply Chain
- T1078 Valid Accounts
- T1059.007 JavaScript
- T1204.002 Malicious File
- T1059.004 Unix Shell
- T1070.004 File Deletion
- T1027 Obfuscated Files or Information
- T1140 Deobfuscate/Decode Files or Information
- T1102.001 Dead Drop Resolver
- T1555.003 Credentials from Web Browsers
- T1005 Data from Local System

## Free detection (Splunk SPL, Microsoft KQL, Sigma)

**GHAPPIER — Malicious @dforge-core/dforge-mcp 0.2.20/0.2.21 Install or MCP Server Spawning Shell — Exploitation Phase** (`TL-2026-5524-SPL-001`)

TL-2026-5524: detects endpoint execution of the trojanised npm releases @dforge-core/dforge-mcp@0.2.20 / 0.2.21 (GHAPPIER loader, published with valid npm provenance on 2026-09-09), and the behaviour that matters most: a JavaScript runtime launched for dforge-mcp spawning a shell, interpreter or downloader. The loader runs when the MCP server is started, not at install, so the parent/child branch catches hosts that actually executed the payload (T1195.002, T1059.007, T1059.004, T1204.002).

- **Languages:** Splunk SPL, Microsoft KQL, Sigma
- **ATT&CK:** T1195.002, T1059.007, T1059.004, T1204.002
- **Data sources:** sysmon, index=sysmon sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational (EventCode 1), DeviceProcessEvents, linux process_creation
- **Severity:** high
- **Confidence:** high

### Splunk SPL

```spl
index=sysmon sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1 earliest=-24h
| eval img=lower(Image), pimg=lower(ParentImage), cmd=lower(CommandLine), pcmd=lower(ParentCommandLine)
| eval bad_release=if(match(cmd,"@dforge-core/dforge-mcp@0[.]2[.]2[01]([^0-9]|$)"),1,0)
| eval mcp_child=if(match(pimg,"(node|bun|deno)[.]exe$") AND match(pcmd,"dforge-mcp") AND match(img,"(cmd|powershell|pwsh|curl|wscript|cscript|mshta|bash|sh)[.]exe$"),1,0)
| where bad_release=1 OR mcp_child=1
| eval reason=case(bad_release=1 AND mcp_child=1,"malicious release executed and spawned shell",bad_release=1,"malicious dforge-mcp 0.2.20/0.2.21 referenced in command line",true(),"dforge-mcp server spawned shell or downloader")
| stats count earliest(_time) as first_seen latest(_time) as last_seen values(reason) as reasons values(CommandLine) as command_lines values(ParentCommandLine) as parent_command_lines by Computer, User
| convert ctime(first_seen) ctime(last_seen)
```

### Microsoft KQL

```kql
let lookback = 24h;
let bad_pkg = dynamic(["@dforge-core/dforge-mcp@0.2.21", "@dforge-core/dforge-mcp@0.2.20"]);
let runtimes = dynamic(["node", "node.exe", "bun", "bun.exe", "deno", "deno.exe"]);
let install_hits = DeviceProcessEvents
    | where Timestamp > ago(lookback)
    | where ProcessCommandLine has_any (bad_pkg)
    | extend DetectionReason = "Malicious dforge-mcp release referenced in command line";
let mcp_children = DeviceProcessEvents
    | where Timestamp > ago(lookback)
    | where InitiatingProcessFileName in~ (runtimes)
    | where InitiatingProcessCommandLine has "dforge-mcp"
    | where FileName in~ ("cmd.exe", "powershell.exe", "pwsh.exe", "curl.exe", "curl", "wget", "sh", "bash", "dash", "zsh", "mshta.exe", "wscript.exe", "cscript.exe")
    | extend DetectionReason = "dforge-mcp server spawned shell or downloader";
union install_hits, mcp_children
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine, DetectionReason
| order by Timestamp desc
```

### Sigma

```yaml
title: GHAPPIER Malicious dforge-mcp npm Release Execution or Shell Spawn
id: 6b0e3a52-4c1d-4f7e-9a21-5d8c7e1f3b01
status: experimental
description: Detects installation or execution of the trojanised @dforge-core/dforge-mcp 0.2.20/0.2.21 npm releases (GHAPPIER loader) and a Node.js dforge-mcp server spawning a shell or downloader on Linux hosts.
references:
  - https://www.cloudsek.com/blog/ghappier-malware-loader-npm-supply-chain-attack
  - https://orca.security/resources/research/ghappier-loader-npm-supply-chain-attack/
author: Threadlinqs Team
date: 2026/11/09
tags:
  - attack.initial-access
  - attack.t1195.002
  - attack.execution
  - attack.t1059.007
  - attack.t1059.004
  - attack.t1204.002
logsource:
  category: process_creation
  product: linux
detection:
  selection_install:
    CommandLine|contains:
      - '@dforge-core/dforge-mcp@0.2.21'
      - '@dforge-core/dforge-mcp@0.2.20'
  selection_parent:
    ParentImage|endswith: '/node'
    ParentCommandLine|contains: 'dforge-mcp'
  selection_child:
    Image|endswith:
      - '/sh'
      - '/bash'
      - '/dash'
      - '/zsh'
      - '/curl'
      - '/wget'
  condition: selection_install or (selection_parent and selection_child)
falsepositives:
  - Security teams or researchers intentionally installing the malicious version in a sandbox
  - Legitimate dforge-mcp tooling that shells out to helper scripts
level: high
```

**Known false positives:**

- Security researchers or sandboxes deliberately installing 0.2.20/0.2.21 for analysis
- Legitimate MCP server wrappers that launch helper shells from a Node.js parent

**Tuning notes:** Exclude known analysis sandboxes by Computer. The package-reference branch is near-zero noise; if the parent/child branch is noisy, restrict child images to curl/wget/mshta/powershell. Extend the Image list if Sysmon for Linux feeds a separate index. Also hunt lockfiles (package-lock.json, pnpm-lock.yaml, yarn.lock) for 0.2.21 and treat hosts that launched the MCP server as compromised.

### 8 more detection rules (Splunk SPL, Microsoft KQL, Sigma), unlock with TL Creds

- GHAPPIER — dforge-mcp 0.2.21 Execution and PolinRider Forced Git Push Propagation (Defender XDR) — Exploitation Phase (locked: unlock with TL Creds)
- GHAPPIER — Self-Deleting Implant After dforge-mcp Launch (Defender XDR) — Post-Exploitation (locked: unlock with TL Creds)
- GHAPPIER/PolinRider — Node.js Blockchain Dead-Drop and socket.io C2 Connections (Defender XDR) — C2 (locked: unlock with TL Creds)
- GHAPPIER — dforge-mcp Release Execution and Linux Shell Spawn (Sigma) — Exploitation Phase (locked: unlock with TL Creds)
- PolinRider/InvisibleFerret — Node.js Access to Browser Credential Stores (Sigma) — Post-Exploitation (locked: unlock with TL Creds)
- GHAPPIER/PolinRider — Node.js DNS Resolution of Blockchain Dead-Drop Hosts (Sigma) — C2 (locked: unlock with TL Creds)
- GHAPPIER/PolinRider — Obfuscated Inline Node.js Loader Stage Execution — Post-Exploitation (locked: unlock with TL Creds)
- GHAPPIER/PolinRider — Blockchain Dead-Drop C2 Resolution From Node.js or Known Wallet Addresses — C2 (locked: unlock with TL Creds)

Sign up free to unlock the detection rules: https://threadlinqs.com/?modal=signup

## Sources

- [GHAPPIER - One loader, sixty-five repositories, twenty-two accounts: an unreported loader family beside DPRK's PolinRider campaign (CloudSEK)](https://www.cloudsek.com/blog/ghappier-malware-loader-npm-supply-chain-attack)
- [CloudSEK GHAPPIER full report (PDF, contains IOC list)](https://cdn.cloudsek.com/cloudsek-blog-pdfs/ghappier-9o83ocu.pdf)
- [Attackers Abuse npm Trusted Publishing in GHAPPIER Campaign (Infosecurity Magazine)](https://www.infosecurity-magazine.com/news/attackers-abuse-npm-trusted/)
- [GHAPPIER loader npm supply chain attack (Orca Security)](https://orca.security/resources/research/ghappier-loader-npm-supply-chain-attack/)
- [PolinRider jumps the fence (OpenSourceMalware)](https://opensourcemalware.com/blog/polinrider-jumps-the-fence)
- [ChainVeil and ViteVenom: DPRK PolinRider campaign (OpenSourceMalware)](https://opensourcemalware.com/blog/chainveil-and-vitevenom-dprk-polinrider-campaign)
- [North Korean Hackers Target Open-Source Devs With Hidden Malware That Steals Source Code And Credentials (Expert Insights)](https://expertinsights.com/news/north-korean-hackers-target-open-source-supply-chain)
- [PolinRider coverage (devops.com)](https://devops.com/?p=186370)

## Full data

The other 8 detection rules (Splunk SPL / Microsoft KQL / Sigma) unlock with TL Creds; a free account includes daily creds: https://threadlinqs.com/?modal=signup
Indicator values unlock with TL Creds, at a lower price on Red and Purple; AI agents can query rules and IOCs through the Threadlinqs MCP server (Purple): https://intel.threadlinqs.com/mcp

Canonical: https://intel.threadlinqs.com/threat/TL-2026-5524
