So I left a nodered instance running on my VPS the other day wide open to the world with authentication disabled. Big surprise, in a couple weeks the CPU started running at 100% permanently.
After a brief look it seems like my little server joined a botnet and after a whilte started running a banal XMR miner. Since I’m no security researcher, I used a little artificial superior help to investigate.
Incident timeline summary:
Sep 7, 13:39 – A hidden directory /usr/src/node-red/.local/bin/ appears inside the container. Eight minutes later, a SOCKS5 proxy is running: gost -L proxy:stressify228@:1080. It stays alive, uninterrupted, for the next 22 days.
Sep 23, 23:58 – A fake packagekitd process (24 MB) spawns via the same path.
Sep 26, 04:40 – Four Node-RED flows are deployed via the public /flows API in a 3-hour window. The first one is a watchdog (values in Russian: «Запуск при старте» – “Run at start”, «Проверка бота» – “Check bot”): every 60 seconds it checks if the bot.js script is running. The bot fetches its current C2 control domain from the Telegram channel t.me/SorryItsmyjobonlybussinesbot, registers at /api/register, polls /api/c2 every 30 s, and executes whatever command arrives (exec, download, write, delete, kill, upload).
Sep 26, 07:25 – Persistance is installed in a flow node: nohup pidof .trace || $(find / -type f -name .trace …) – hourly check that if the miner isn’t running, start it again.
Sep 26, 07:47 – The dropper from wsend.io/CxQZJldF/comp (7 MB gzip with Base64-embedded XMRig binaries for x86_64 and aarch64) fires. It kills every other miner it finds (kinsing, kdevtmpfsi, watchbog…), wipes /tmp, drops .trace (XMRig 6.25.0) into every writable directory, and starts mining wallet 47Ce53JfHMxh…kEoS9 on c3pool. The container runs as uid 1000 with no cron, no systemd, no SSH.
The code in bot.js script seems to point at a website that looks exactly how an evil hacker website should:

The code in question:
// ============ ОПРЕДЕЛЯЕМ ПАПКУ СКРИПТА ============
// __dirname - папка, где лежит bot.js
const SCRIPT_DIR = __dirname;
const ID_FILE = path.join(SCRIPT_DIR, 'bot_id.txt');
const PID_FILE = path.join(SCRIPT_DIR, '.bot.pid');
console.log('[+] Папка скрипта:', SCRIPT_DIR);
console.log('[+] Файл ID:', ID_FILE);
// ============ КОНФИГУРАЦИЯ ============
const CONFIG = {
telegramUrl: 'https://t.me/SorryItsmyjobonlybussinesbot',
defaultDomain: 'stressify.pro',
checkInterval: 30000,
jitter: 15000,
retryDelay: 15000,
timeout: 10000
};
Not much to write home about. As a test I shamelessly vibecoded a fake bot that tries to register to the C2 server and accept commands. So far no luck but let’s see if it catches anything interesting in the future.