Skip to content

Instantly share code, notes, and snippets.

@mmastrac
Created June 27, 2026 15:45
Show Gist options
  • Select an option

  • Save mmastrac/3d70d09e5b0dabd6004b701ad7bdcba0 to your computer and use it in GitHub Desktop.

Select an option

Save mmastrac/3d70d09e5b0dabd6004b701ad7bdcba0 to your computer and use it in GitHub Desktop.
PinPinRAT C2 implant IoC checker
#!/usr/bin/env bash
# check_ioc.sh — PinPinRAT C2 implant IoC checker
# Read-only: prints findings, changes nothing.
# Run with: bash check_ioc.sh
# Requires bash (not sh). Works on macOS and Linux.
set -euo pipefail
RED='\033[0;31m'; YEL='\033[0;33m'; GRN='\033[0;32m'; BLD='\033[1m'; RST='\033[0m'
# HITS is written from subshells via a temp file so counts survive pipe boundaries.
HITFILE="$(mktemp)"
trap 'rm -f "$HITFILE"' EXIT
hit() {
echo -e " ${RED}[HIT]${RST} $*"
echo "x" >> "$HITFILE" # one byte per hit; counted in summary
}
info() { echo -e " ${YEL}[INFO]${RST} $*"; }
ok() { echo -e " ${GRN}[OK]${RST} $*"; }
sep() { echo -e "\n${BLD}── $* ──${RST}"; }
OS="$(uname -s)" # Darwin or Linux
TMPD="${TMPDIR:-/tmp}"
# ─────────────────────────────────────────────────────────────────────────────
sep "1 · Filesystem — runtime artifact directories"
CACHE_DIRS=()
if [ "$OS" = "Darwin" ]; then
CACHE_ROOT="$HOME/Library/Caches/runtime-cache"
if [ -d "$CACHE_ROOT" ]; then
info "runtime-cache dir exists: $CACHE_ROOT"
while IFS= read -r d; do
CACHE_DIRS+=("$d")
hit "Suspect cache dir: $d"
done < <(find "$CACHE_ROOT" -maxdepth 1 -type d -name '.cache-*' 2>/dev/null)
else
ok "~/Library/Caches/runtime-cache not found"
fi
fi
while IFS= read -r d; do
CACHE_DIRS+=("$d")
hit "Suspect tmpdir cache dir: $d"
done < <(find "$TMPD" -maxdepth 1 -type d -name '.cache-*' 2>/dev/null)
[ ${#CACHE_DIRS[@]} -eq 0 ] && ok "No .cache-* dirs found under expected locations"
# ─────────────────────────────────────────────────────────────────────────────
sep "2 · Filesystem — payload and mutex scripts"
for base in "${CACHE_DIRS[@]+"${CACHE_DIRS[@]}"}"; do
for f in payload.js mutex.js; do
fp="$base/$f"
if [ -f "$fp" ]; then
hit "Implant file: $fp ($(wc -c < "$fp" | tr -d ' ') bytes)"
fi
done
done
# Belt-and-suspenders: wider search under HOME in case paths differ
while IFS= read -r f; do
hit "Implant file (HOME search): $f"
done < <(find "$HOME" -maxdepth 6 -type f \
\( -name 'payload.js' -o -name 'mutex.js' \) \
-path '*/.cache-*' 2>/dev/null)
# ─────────────────────────────────────────────────────────────────────────────
sep "3 · Filesystem — poisoned typescript.js in any node_modules"
info "Searching HOME, /tmp, /var — may take a few seconds on large disks"
_check_ts() {
local f="$1"
if grep -qF '12ff4b51' "$f" 2>/dev/null || \
grep -qF 'ticket-harbor-tsc-shim-anchor' "$f" 2>/dev/null; then
hit "Patched typescript file: $f"
fi
}
while IFS= read -r f; do _check_ts "$f"; done < <(
find "$HOME" /tmp 2>/dev/null \
-maxdepth 10 -type f -name 'typescript.js' \
-path '*/typescript/lib/*' 2>/dev/null
)
# /var searched separately; skipped on macOS where it rarely contains npm installs
if [ "$OS" = "Linux" ]; then
while IFS= read -r f; do _check_ts "$f"; done < <(
find /var 2>/dev/null \
-maxdepth 10 -type f -name 'typescript.js' \
-path '*/typescript/lib/*' 2>/dev/null
)
fi
# Also check current working directory tree
while IFS= read -r f; do _check_ts "$f"; done < <(
find . -maxdepth 8 -type f -name 'typescript.js' \
-path '*/typescript/lib/*' 2>/dev/null
)
# ─────────────────────────────────────────────────────────────────────────────
sep "4 · Persistence — crontab"
if command -v crontab &>/dev/null; then
CRON="$(crontab -l 2>/dev/null || true)"
if [ -n "$CRON" ]; then
while IFS= read -r line; do
case "$line" in
*'.cache-'*|*'NODT_PAYLOAD'*|*'mutex.js'*)
hit "Crontab entry: $line" ;;
esac
done <<< "$CRON"
echo "$CRON" | grep -qE '\.cache-|NODT_PAYLOAD|mutex\.js' || \
ok "No suspect crontab entries"
else
ok "No crontab installed"
fi
else
info "crontab not available"
fi
# ─────────────────────────────────────────────────────────────────────────────
sep "5 · Persistence — Windows scheduled task (via schtasks if available)"
if command -v schtasks.exe &>/dev/null 2>&1; then
if schtasks.exe /query /tn "PinpinWrappedJs" &>/dev/null 2>&1; then
hit "Scheduled task exists: PinpinWrappedJs"
else
ok "Scheduled task PinpinWrappedJs not found"
fi
else
info "schtasks not available (not Windows or not in PATH)"
fi
# ─────────────────────────────────────────────────────────────────────────────
sep "6 · Running processes — Node masquerading as WebKit (macOS)"
if [ "$OS" = "Darwin" ]; then
FOUND_MASQ=0
while IFS= read -r line; do
# Legitimate WebKit helper processes always have --type= in their args.
# A Node process using this argv0 as a disguise will not.
if ! echo "$line" | grep -q -- '--type='; then
hit "Suspect Node masquerade (no --type= flag): $line"
FOUND_MASQ=1
else
info "Legitimate WebKit process: $(echo "$line" | awk '{print $1, $2}')"
fi
done < <(ps -axo pid,comm,args 2>/dev/null | \
grep 'com\.apple\.WebKit\.Networking' | grep -v grep || true)
[ "$FOUND_MASQ" -eq 0 ] && ok "No suspect WebKit masquerades found"
fi
# ─────────────────────────────────────────────────────────────────────────────
sep "7 · Running processes — NODT_PAYLOAD env var in any process"
if [ "$OS" = "Linux" ] && [ -d /proc ]; then
# nullglob prevents iterating the literal glob string if /proc is empty
shopt -s nullglob
FOUND_ENV=0
for pid_dir in /proc/[0-9]*/environ; do
pid="${pid_dir//[^0-9]/}"
if grep -qz 'NODT_PAYLOAD' "$pid_dir" 2>/dev/null; then
COMM="$(cat "/proc/$pid/comm" 2>/dev/null || echo '?')"
hit "Process $pid ($COMM) has NODT_PAYLOAD env var"
FOUND_ENV=1
fi
done
shopt -u nullglob
[ "$FOUND_ENV" -eq 0 ] && ok "No process has NODT_PAYLOAD env var"
elif [ "$OS" = "Darwin" ]; then
# ps -axE prints each process with its full environment on the same line.
# Only our own user's processes are visible without sudo.
FOUND_ENV=0
while IFS= read -r line; do
hit "Process with NODT_PAYLOAD in environment: $(echo "$line" | awk '{print $1, $2}')"
FOUND_ENV=1
done < <(ps -axE 2>/dev/null | grep 'NODT_PAYLOAD' | grep -v grep || true)
[ "$FOUND_ENV" -eq 0 ] && ok "No process has NODT_PAYLOAD env var"
fi
# ─────────────────────────────────────────────────────────────────────────────
sep "8 · Running processes — Node processes with files open in .cache- dirs"
if command -v lsof &>/dev/null; then
info "Running lsof — may take a few seconds on a busy machine"
FOUND_LSOF=0
while IFS= read -r line; do
hit "lsof: node has file open in suspect dir: $line"
FOUND_LSOF=1
done < <(lsof -n -c node 2>/dev/null | grep '\.cache-' || true)
[ "$FOUND_LSOF" -eq 0 ] && ok "No node process has files open in .cache- dirs"
else
info "lsof not available — skipping open-file check"
fi
# ─────────────────────────────────────────────────────────────────────────────
sep "9 · Network — active connections to C2 (89.124.107.161)"
C2_IP="89.124.107.161"
FOUND_NET=0
if command -v lsof &>/dev/null; then
while IFS= read -r line; do
hit "lsof: active connection to C2: $line"
FOUND_NET=1
done < <(lsof -n -i "TCP@${C2_IP}:80" 2>/dev/null | grep -v '^COMMAND' || true)
fi
if command -v netstat &>/dev/null; then
while IFS= read -r line; do
hit "netstat: connection involving C2 IP: $line"
FOUND_NET=1
done < <(netstat -an 2>/dev/null | grep "$C2_IP" || true)
fi
if command -v ss &>/dev/null; then
while IFS= read -r line; do
hit "ss: connection involving C2 IP: $line"
FOUND_NET=1
done < <(ss -tn 2>/dev/null | grep "$C2_IP" || true)
fi
[ "$FOUND_NET" -eq 0 ] && ok "No active connections to C2 IP detected"
# ─────────────────────────────────────────────────────────────────────────────
sep "10 · Network — system log references to C2 IP (macOS, last 24h)"
if [ "$OS" = "Darwin" ] && command -v log &>/dev/null; then
info "Scanning unified system log — this can take 30-60 seconds, please wait"
COUNT="$(log show --last 24h \
--predicate "eventMessage CONTAINS '89.124.107.161'" \
2>/dev/null | grep -c '89.124.107.161' || true)"
if [ "${COUNT:-0}" -gt 0 ]; then
hit "System log: $COUNT entries referencing C2 IP in the last 24h"
else
ok "No C2 IP references in system log (last 24h)"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
sep "Summary"
HITS="$(wc -l < "$HITFILE" | tr -d ' ')"
if [ "${HITS:-0}" -gt 0 ]; then
echo -e "\n${RED}${BLD}${HITS} indicator(s) found. See [HIT] lines above.${RST}"
echo -e "${YEL}Do not remove anything — preserve for forensics first.${RST}"
echo -e "Refer to the readout PDF for remediation steps.\n"
else
echo -e "\n${GRN}${BLD}No IoCs found on this machine.${RST}\n"
fi
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment